Added Unity project files

This commit is contained in:
2026-06-07 16:58:24 +01:00
parent 3cc05d260b
commit 23bbcab156
3942 changed files with 453676 additions and 0 deletions

View File

@ -0,0 +1,314 @@
using System;
using UnityEngine;
using UnityEngine.EventSystems;
using VRC.SDKBase;
using VRC.Udon.Common;
namespace VRC.SDK3.ClientSim
{
/// <summary>
/// The system responsible for managing the mouse position for both ClientSim raycasting
/// and for Unity's EventSystem to know where to raycast for UI elements.
/// </summary>
/// <remarks>
/// Sends Events:
/// - ClientSimMouseReleasedEvent
/// - ClientSimCurrentHandEvent
/// Listens to Events:
/// - ClientSimMenuStateChangedEvent
/// - ClientSimRaycastHitResultsEvent
/// Listens to Input Events:
/// - Use
/// - ReleaseMouse
/// </remarks>
[AddComponentMenu("")]
// Update FrameTick at the end of frame so that Input from playmode and test can be processed in the same order.
[DefaultExecutionOrder(10000)]
class ClientSimBaseInput : BaseInput, IClientSimMousePositionProvider, IDisposable
{
private IClientSimEventDispatcher _eventDispatcher;
private IClientSimInput _input;
private ClientSimSettings _settings;
private bool _menuIsOpen;
private bool _mouseReleaseKeyIsDown;
private bool _prevMouseReleased;
// Used for interacting with UI
private int _frameTick = 0;
private bool _rightUseDown = false;
private int _rightUseChangeTick = -1;
private bool _leftUseDown = false;
private int _leftUseChangeTick = -1;
private HandType _lastHandUsed = HandType.RIGHT;
private Camera _playerCamera = null;
private bool _uiShapeHit = false;
private Vector3 _raycastMousePosition;
public static Vector2 GetScreenCenter()
{
return new Vector2(Screen.width, Screen.height) * 0.5f;
}
public Vector2 GetMousePosition()
{
if (IsMouseFree())
{
// Due to having multiple inputs enabled or disabled, this method ensures no errors are thrown even
// if setup is incorrect.
#if ENABLE_INPUT_SYSTEM
// TODO if gamepad input, emulate mouse position to allow clicking on menus.
return UnityEngine.InputSystem.Mouse.current?.position.ReadValue() ?? Vector2.zero;
#elif ENABLE_LEGACY_INPUT_MANAGER
return base.mousePosition;
#else
return Vector2.zero;
#endif
}
return GetScreenCenter();
}
protected override void Awake()
{
base.Awake();
this.PreventComponentFromSaving();
}
public void Initialize(
IClientSimEventDispatcher eventDispatcher,
IClientSimInput input,
ClientSimSettings settings)
{
// Do not lock mouse if the player is never spawned.
if (!settings.spawnPlayer)
{
enabled = false;
return;
}
_eventDispatcher = eventDispatcher;
_input = input;
_settings = settings;
_eventDispatcher.Subscribe<ClientSimMenuStateChangedEvent>(SetMenuOpen);
_eventDispatcher.Subscribe<ClientSimRaycastHitResultsEvent>(OnRaycastHit);
// Input will be null with incorrect Unity input project settings.
_input?.SubscribeReleaseMouse(InputMouseReleased);
_input?.SubscribeUse(InputUse);
}
protected override void Start()
{
base.Start();
// TODO properly pass in the camera provider instead of using this method.
_playerCamera = VRC_UiShape.GetEventCamera?.Invoke(this.gameObject);
foreach (var canvas in FindObjectsByType<Canvas>(FindObjectsSortMode.None))
{
if ((canvas.renderMode == RenderMode.WorldSpace) && (canvas.worldCamera == null))
canvas.worldCamera = _playerCamera;
}
}
protected override void OnDestroy()
{
base.OnDestroy();
Dispose();
}
public void Dispose()
{
_eventDispatcher?.Unsubscribe<ClientSimMenuStateChangedEvent>(SetMenuOpen);
_eventDispatcher?.Unsubscribe<ClientSimRaycastHitResultsEvent>(OnRaycastHit);
_input?.UnsubscribeReleaseMouse(InputMouseReleased);
_input?.UnsubscribeUse(InputUse);
}
private void Update()
{
// Update mouse lock every frame to ensure it is always locked when needed.
InternalLockUpdate();
// TODO Move this to input system and support checking when Use input is down or up on the current frame.
++_frameTick;
}
#region Overrides
// Use the screenspace value of the last raycast hit position as the current mouse position.
// Using the raycast position decouples Desktop and VR's input source, allowing both to interact with UI without
// knowing the source of the raycast (mouse vs controller position)
public override Vector2 mousePosition => _raycastMousePosition;
public override bool GetMouseButton(int button)
{
return _lastHandUsed == HandType.RIGHT ? _rightUseDown : _leftUseDown;
}
public override bool GetMouseButtonUp(int button)
{
return
_lastHandUsed == HandType.RIGHT
? (!_rightUseDown && _rightUseChangeTick == _frameTick)
: (!_leftUseDown && _leftUseChangeTick == _frameTick);
}
public override bool GetMouseButtonDown(int button)
{
return
_lastHandUsed == HandType.RIGHT
? (_rightUseDown && _rightUseChangeTick == _frameTick)
: (_leftUseDown && _leftUseChangeTick == _frameTick);
}
// Override mouse scroll method to prevent errors when input settings are incorrectly setup on first import.
public override Vector2 mouseScrollDelta
{
get
{
#if ENABLE_INPUT_SYSTEM
return UnityEngine.InputSystem.Mouse.current?.scroll.ReadValue() ?? Vector2.zero;
#elif ENABLE_LEGACY_INPUT_MANAGER
return base.mouseScrollDelta;
#else
return Vector2.zero;
#endif
}
}
// Override generic axis method to prevent errors when input settings are incorrectly setup on first import.
public override float GetAxisRaw(string axisName)
{
if (axisName == "Horizontal")
{
return _input?.GetMovementHorizontal() ?? 0;
}
if (axisName == "Vertical")
{
return _input?.GetMovementVertical() ?? 0;
}
return 0f;
}
// Override generic button method to prevent errors when input settings are incorrectly setup on first import.
public override bool GetButtonDown(string buttonName)
{
if (buttonName == "Horizontal")
{
return Mathf.Abs(_input?.GetMovementHorizontal() ?? 0) > 0.5;
}
if (buttonName == "Vertical")
{
return Mathf.Abs(_input?.GetMovementVertical() ?? 0) > 0.5;
}
return false;
}
#endregion
#region ClientSim Events
private void SetMenuOpen(ClientSimMenuStateChangedEvent stateChangedEvent)
{
_menuIsOpen = stateChangedEvent.isMenuOpen;
CheckMouseRelease();
}
private void OnRaycastHit(ClientSimRaycastHitResultsEvent hitEvent)
{
if (_lastHandUsed == hitEvent.handType)
{
var hitResults = hitEvent.raycastResults;
_uiShapeHit = hitResults != null && hitResults.uiShape != null;
_raycastMousePosition = GetScreenCenter();
// If there is a player camera and there was a hit point, convert the world point to screen space.
// Transforming it now instead of when requested to ensure that player position updates do not affect
// interacting with the menu.
if (hitResults != null && _playerCamera != null)
{
_raycastMousePosition = _playerCamera.WorldToScreenPoint(hitResults.hitPoint);
}
}
}
#endregion
#region ClientSim Input Events
private void InputMouseReleased(bool value)
{
_mouseReleaseKeyIsDown = value;
CheckMouseRelease();
}
private void InputUse(bool value, HandType handType)
{
if (value)
{
if (_lastHandUsed != handType)
{
_lastHandUsed = handType;
_eventDispatcher.SendEvent(new ClientSimCurrentHandEvent { currentUsedHand = _lastHandUsed });
}
}
if (handType == HandType.RIGHT)
{
_rightUseDown = value;
_rightUseChangeTick = _frameTick;
}
else
{
_leftUseDown = value;
_leftUseChangeTick = _frameTick;
}
}
#endregion
public bool HitUIShape()
{
return _uiShapeHit;
}
private bool IsMouseFree()
{
return _mouseReleaseKeyIsDown || _menuIsOpen;
}
private void CheckMouseRelease()
{
bool released = IsMouseFree();
if (released != _prevMouseReleased)
{
_prevMouseReleased = released;
_eventDispatcher.SendEvent(new ClientSimMouseReleasedEvent { isReleased = released });
}
InternalLockUpdate();
}
private void InternalLockUpdate()
{
// If the menu is open or the tab key is held down, do not lock the mouse and show the cursor.
// TODO Check if TrackingProvider is VR and do not lock mouse.
if (IsMouseFree() || ClientSimRuntimeLoader.IsInUnityTest())
{
Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
}
// Else hide the cursor and lock the cursor to the center of the screen.
else
{
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6b423402f03c4238b2fe097f146f7d01
timeCreated: 1638823270

View File

@ -0,0 +1,29 @@

using UnityEngine;
using VRC.Udon;
namespace VRC.SDK3.ClientSim
{
/// <summary>
/// System responsible for adding objects to the Udon blacklist.
/// </summary>
/// <remarks>Really just a wrapper for calling the blacklist method on UdonManager.</remarks>
public class ClientSimBlacklistManager : IClientSimBlacklistManager
{
public void AddObjectAndChildrenToBlackList(GameObject obj)
{
AddObjectAndChildrenToBlackList(obj, UdonManager.Instance);
}
private void AddObjectAndChildrenToBlackList(GameObject obj, UdonManager udonManager)
{
udonManager.Blacklist(obj);
Transform xform = obj.transform;
for (int child = 0; child < xform.childCount; ++child)
{
AddObjectAndChildrenToBlackList(xform.GetChild(child).gameObject, udonManager);
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a2929dd4844944c799d13b91350be20d
timeCreated: 1638914444

View File

@ -0,0 +1,126 @@

using System.Collections.Generic;
using UnityEngine;
namespace VRC.SDK3.ClientSim
{
/// <summary>
/// System responsible for highlighting objects.
/// </summary>
[AddComponentMenu("")]
public class ClientSimHighlightManager : ClientSimBehaviour, IClientSimHighlightManager
{
[SerializeField]
private Mesh cubeMesh;
[SerializeField]
private Mesh capsuleMesh;
[SerializeField]
private Mesh sphereMesh;
[SerializeField]
private GameObject proxyHighlightPrefab;
// TODO wrap highlightFX to make it easier to test/replace.
private HighlightsFX _highlightsFX;
private readonly Dictionary<GameObject, ClientSimHighlightProxy> _objectRenderProxies =
new Dictionary<GameObject, ClientSimHighlightProxy>();
private readonly Queue<ClientSimHighlightProxy> _proxyMeshQueue = new Queue<ClientSimHighlightProxy>();
public void Initialize(Camera playerCamera)
{
_highlightsFX = playerCamera.gameObject.AddComponent<HighlightsFX>();
}
public void EnableObjectHighlight(GameObject obj)
{
List<Renderer> renderers = GatherRenderers(obj, false);
if (renderers.Count == 0)
{
Renderer rend = GetProxyHighlight(obj);
if (rend != null)
{
renderers.Add(rend);
}
}
foreach (var rend in renderers)
{
EnableObjectHighlight(rend, true);
}
}
public void DisableObjectHighlight(GameObject obj)
{
if (_objectRenderProxies.TryGetValue(obj, out ClientSimHighlightProxy proxy))
{
_objectRenderProxies.Remove(obj);
EnableObjectHighlight(proxy.Renderer, false);
_proxyMeshQueue.Enqueue(proxy);
proxy.DisableProxy();
}
List<Renderer> renderers = GatherRenderers(obj, true);
foreach (var rend in renderers)
{
EnableObjectHighlight(rend, false);
}
}
public void EnableObjectHighlight(Renderer rend, bool isEnabled)
{
_highlightsFX.EnableOutline(rend, isEnabled);
}
private List<Renderer> GatherRenderers(GameObject obj, bool findDisabled)
{
if(obj == null)
{
return new List<Renderer>();
}
List<Renderer> results = new List<Renderer>();
foreach (var rend in obj.GetComponentsInChildren<Renderer>(findDisabled))
{
if (!rend.enabled || rend.isPartOfStaticBatch)
{
continue;
}
MeshFilter filter = rend.GetComponent<MeshFilter>();
if (filter == null || filter.sharedMesh == null)
{
continue;
}
results.Add(rend);
}
return results;
}
private Renderer GetProxyHighlight(GameObject obj)
{
ClientSimHighlightProxy proxy = GetUnusedProxy();
Collider objCollider = obj.GetComponent<Collider>();
_objectRenderProxies.Add(obj, proxy);
proxy.EnableProxy(obj.transform, objCollider);
return proxy.Renderer;
}
private ClientSimHighlightProxy GetUnusedProxy()
{
if (_proxyMeshQueue.Count == 0)
{
GameObject tooltipObj = Instantiate(proxyHighlightPrefab, transform);
ClientSimHighlightProxy tooltip = tooltipObj.GetComponent<ClientSimHighlightProxy>();
tooltip.Initialize(cubeMesh, capsuleMesh, sphereMesh);
return tooltip;
}
return _proxyMeshQueue.Dequeue();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1938047de93c66141923068bf4063db5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,125 @@

using UnityEngine;
namespace VRC.SDK3.ClientSim
{
// This class is used for when an object has no usable mesh renderers to highlight. This will copy the collider on
// the interact instead of any mesh on or under the interact.
// An object will not have usable colliders if:
// - There are no mesh renderers on or as a child of the object.
// - The mesh renderers do not have a mesh
// - The mesh renderers are part of a combined or static batched mesh.
[AddComponentMenu("")]
public class ClientSimHighlightProxy : ClientSimBehaviour
{
public Renderer Renderer { get; private set; }
private Mesh _cubeMesh;
private Mesh _capsuleMesh;
private Mesh _sphereMesh;
private MeshFilter _filter;
private Transform _proxyObject;
private Collider _proxyCollider;
protected override void Awake()
{
base.Awake();
Renderer = GetComponent<Renderer>();
_filter = GetComponent<MeshFilter>();
}
public void Initialize(Mesh cubeMesh, Mesh capsuleMesh, Mesh sphereMesh)
{
_cubeMesh = cubeMesh;
_capsuleMesh = capsuleMesh;
_sphereMesh = sphereMesh;
}
private void OnWillRenderObject()
{
UpdatePosition();
}
private void UpdatePosition()
{
Vector3 position = _proxyObject.position;
Quaternion rotation = _proxyObject.rotation;
Vector3 scale = _proxyObject.lossyScale;
if (_proxyCollider is BoxCollider boxCollider)
{
position += rotation * Vector3.Scale(boxCollider.center, scale);
scale = Vector3.Scale(scale, boxCollider.size);
_filter.sharedMesh = _cubeMesh;
}
else if (_proxyCollider is SphereCollider sphereCollider)
{
position += rotation * Vector3.Scale(sphereCollider.center, scale);
// VRChatBug: Unity will always keep spheres round, but VRChat ignores this for the proxy rendering.
// Sphere colliders are always round. Get the max component as the sphere size.
float sphereScale = sphereCollider.radius * 2 * Mathf.Max(scale.x, scale.y, scale.z);
scale = sphereScale * Vector3.one;
_filter.sharedMesh = _sphereMesh;
}
else if (_proxyCollider is CapsuleCollider capsuleCollider)
{
position += rotation * Vector3.Scale(capsuleCollider.center, scale);
float height = capsuleCollider.height * 0.5f;
float radius = capsuleCollider.radius * 2;
float sphereScale = radius;
switch (capsuleCollider.direction)
{
case 0: // X
rotation *= Quaternion.Euler(0, 0, 90);
sphereScale = Mathf.Max(scale.y, scale.z) * radius;
height *= scale.x;
break;
case 1: // Y
// No need to rotate since mesh is already along y axis.
sphereScale = Mathf.Max(scale.x, scale.z) * radius;
height *= scale.y;
break;
case 2: // Z
rotation *= Quaternion.Euler(90, 0, 0);
sphereScale = Mathf.Max(scale.y, scale.x) * radius;
height *= scale.z;
break;
}
height = Mathf.Max(sphereScale * 0.5f, height);
scale = new Vector3(sphereScale, height, sphereScale);
_filter.sharedMesh = _capsuleMesh;
}
else if (_proxyCollider is MeshCollider meshCollider)
{
_filter.sharedMesh = meshCollider.sharedMesh;
}
transform.SetPositionAndRotation(position, rotation);
transform.localScale = scale;
}
public void EnableProxy(Transform obj, Collider proxyCollider)
{
gameObject.SetActive(true);
_filter.sharedMesh = null;
_proxyObject = obj;
_proxyCollider = proxyCollider;
UpdatePosition();
}
public void DisableProxy()
{
gameObject.SetActive(false);
_filter.sharedMesh = null;
_proxyObject = null;
_proxyCollider = null;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cdc8e1faef31d1d479cb70d50716fc35
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,122 @@
using UnityEngine;
using UnityEngine.EventSystems;
using VRC.SDKBase;
namespace VRC.SDK3.ClientSim
{
/// <summary>
/// System responsible for filtering out UI elements that cannot be interacted with.
/// </summary>
[AddComponentMenu("")]
public class ClientSimInputModule : StandaloneInputModule
{
private IClientSimInteractiveLayerProvider _interactiveLayerProvider;
private ClientSimBaseInput _baseInput;
protected override void Awake()
{
base.Awake();
this.PreventComponentFromSaving();
}
private void DisableOtherEventSystems()
{
// Go through and Disable all other event systems in the scene.
EventSystem thisEventSystem = GetComponent<EventSystem>();
EventSystem[] systems = FindObjectsByType<EventSystem>(FindObjectsSortMode.None);
foreach (EventSystem system in systems)
{
if (system != thisEventSystem)
{
system.enabled = false;
}
}
}
public void Initialize(IClientSimInteractiveLayerProvider interactiveLayerProvider)
{
_interactiveLayerProvider = interactiveLayerProvider;
}
protected override void Start()
{
DisableOtherEventSystems();
// TODO check settings and disable self if player is not spawned to allow normal ui raycasting.
m_InputOverride = _baseInput = GetComponent<ClientSimBaseInput>();
base.Start();
}
// Force processing of mouse events even if the cursors is locked.
public override void Process()
{
CursorLockMode currentLockState = Cursor.lockState;
Cursor.lockState = CursorLockMode.None;
base.Process();
Cursor.lockState = currentLockState;
}
// Prevent clicking on menus that are not currently interactable.
protected override MouseState GetMousePointerEventData(int id)
{
var pointerEventData = base.GetMousePointerEventData(id);
var leftEventData = pointerEventData.GetButtonState(PointerEventData.InputButton.Left).eventData;
var pointerRaycast = leftEventData.buttonData.pointerCurrentRaycast;
// Check if this raycast result is valid. If not, reset the data.
if (!ShouldUseRaycastResult(pointerRaycast))
{
leftEventData.buttonData.pointerCurrentRaycast = new RaycastResult();
}
return pointerEventData;
}
private bool ShouldUseRaycastResult(RaycastResult result)
{
GameObject hitObj = result.gameObject;
if (hitObj == null)
{
return false;
}
// If raycaster did not hit a UI shape, this means some other collider is in front.
if (!_baseInput.HitUIShape())
{
return false;
}
// If there is no UI shape on this object or in its parents, ignore it.
VRC_UiShape shape = hitObj.GetComponentInParent<VRC_UiShape>();
if (shape == null)
{
return false;
}
GameObject shapeObj = shape.gameObject;
// Ignore UI elements not on a currently Interactive layer
if (((1 << shapeObj.layer) & _interactiveLayerProvider.GetInteractiveLayers()) == 0)
{
return false;
}
Vector3 position = result.worldPosition;
// If ray is within any collider on the UIShape, it is valid.
foreach (var shapeCollider in shapeObj.GetComponents<Collider>())
{
// If the closest point is itself, then it is in the collider.
if (Vector3.Distance(shapeCollider.ClosestPoint(position), position) < 0.01f)
{
return true;
}
}
return false;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 848153f45093e01469743624ef9f9391
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,64 @@
using System;
using UnityEngine;
using VRC.SDKBase;
namespace VRC.SDK3.ClientSim
{
/// <summary>
/// System that provides what layers the player can interact with at a given time.
/// Interactive layers will change depending on if the Menu is open or not.
/// </summary>
/// <remarks>
/// Listens to Events:
/// - ClientSimMenuStateChangedEvent
/// </remarks>
public class ClientSimInteractiveLayerProvider : IClientSimInteractiveLayerProvider, IDisposable
{
private const int UI_LAYER = 5;
private const int PLAYER_LOCAL_LAYER = 10;
private const int UI_MENU_LAYER = 12;
private const int MIRROR_REFLECTION_LAYER = 18;
private const int INTERNAL_UI_LAYER = 19;
private const int FIRST_USER_LAYER = 22;
private readonly int _interactiveLayersDefault;
private readonly int _interactiveLayersUI;
private readonly IClientSimEventDispatcher _eventDispatcher;
private bool _menuIsOpen;
public ClientSimInteractiveLayerProvider(IClientSimEventDispatcher eventDispatcher)
{
// Only the UI and UIMenu layers are interactable when the UI is open.
_interactiveLayersUI = (1 << UI_LAYER) | (1 << UI_MENU_LAYER) | (1 << INTERNAL_UI_LAYER);
// When the menu is not open, all layers but UI, UIMenu, PlayerLocal, and MirrorReflection layers are interactable.
_interactiveLayersDefault = ~(1 << UI_LAYER) & ~(1 << UI_MENU_LAYER) & ~(1 << PLAYER_LOCAL_LAYER) & ~(1 << MIRROR_REFLECTION_LAYER);
// If Interaction Passthrough is set, these User Layers will also be ignored.
_interactiveLayersDefault &= ~(VRC_SceneDescriptor.Instance.interactThruLayers << FIRST_USER_LAYER);
_eventDispatcher = eventDispatcher;
_eventDispatcher.Subscribe<ClientSimMenuStateChangedEvent>(SetMenuOpen);
}
~ClientSimInteractiveLayerProvider()
{
Dispose();
}
public void Dispose()
{
_eventDispatcher.Unsubscribe<ClientSimMenuStateChangedEvent>(SetMenuOpen);
}
public LayerMask GetInteractiveLayers()
{
return _menuIsOpen ? _interactiveLayersUI : _interactiveLayersDefault;
}
private void SetMenuOpen(ClientSimMenuStateChangedEvent stateChangedEvent)
{
_menuIsOpen = stateChangedEvent.isMenuOpen;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 341c903cb5594e569aa8b82a45974fcf
timeCreated: 1639013197

View File

@ -0,0 +1,975 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using Cysharp.Threading.Tasks;
using UnityEngine;
using VRC.Core;
using VRC.Economy;
using VRC.SDK3.Components;
using VRC.SDK3.Network;
using VRC.SDK3.Platform;
using VRC.SDK3.Rendering;
using VRC.SDK3.Video.Components.AVPro;
using VRC.SDKBase;
using VRC.SDKBase.Network;
using VRC.SDKBase.Platform;
using VRC.Udon;
using VRC.Udon.Security;
using VRCNetworkBehaviour = VRC.SDK3.Network.VRCNetworkBehaviour;
using VRCStation = VRC.SDKBase.VRCStation;
using VRC.SDK3.UdonNetworkCalling;
using VRC.Utility;
#if VRC_ENABLE_PLAYER_PERSISTENCE
using VRC.SDK3.ClientSim.Persistence;
#endif
namespace VRC.SDK3.ClientSim
{
/// <summary>
/// The main system for ClientSim. This handles the spawning of the ClientSim system and initialization.
/// </summary>
/// <remarks>
/// Sends Events:
/// - ClientSimReadyEvent
/// </remarks>
[AddComponentMenu("")]
public class ClientSimMain : ClientSimBehaviour
{
private static readonly string _systemPrefabPath = Path.Combine("ClientSim", "Prefabs", "ClientSimSystem");
private static ClientSimMain _instance;
[SerializeField]
private ClientSimMenu menu;
[SerializeField]
private ClientSimBaseInput baseInput;
[SerializeField]
private ClientSimInputModule inputModule;
[SerializeField]
private ClientSimSyncedObjectManager syncedObjectManager;
[SerializeField]
private ClientSimInputManager inputManager;
[SerializeField]
private ClientSimUdonInputBehaviour udonInput;
[SerializeField]
private ClientSimHighlightManager highlightManager;
[SerializeField]
private ClientSimTooltipManager tooltipManager;
[SerializeField]
private ClientSimPlayerSpawner playerSpawner;
[SerializeField]
private ClientSimStackedVRCameraSystem stackedCameraSystem;
[SerializeField]
private ClientSimStoreManager storeManager;
[SerializeField]
private GameObject proxyObjectPrefab;
private ClientSimProxyObjects _proxyObject;
private IClientSimEventDispatcher _eventDispatcher;
private ClientSimPlayerManager _playerManager;
private ClientSimUdonManager _udonManager;
private ClientSimSceneManager _sceneManager;
private ClientSimBlacklistManager _blacklistManager;
private ClientSimInteractiveLayerProvider _interactiveLayerProvider;
private IClientSimSessionState _sessionState;
private IClientSimUdonEventSender _udonEventSender;
// public so that height can be adjusted via the settings window
public ClientSimPlayerHeightManager HeightManager { get; private set; }
private ClientSimSettings _settings;
private ClientSimPlayer _player;
private bool _isReady;
public static void CreateInstance(
ClientSimSettings settings,
IClientSimEventDispatcher eventDispatcher = null)
{
if (HasInstance())
{
throw new ClientSimException("Cannot create an instance of ClientSim while one already exists.");
}
// Configure Delegates for Allowlisted URL Validation
VRCUrl.DomainExplicitAllowlistDelegate = () => UrlAllowlistConfig.DomainExplicitAllowlist;
VRCUrl.DomainWildcardAllowlistDelegate = () => UrlAllowlistConfig.DomainWildcardAllowlist;
GameObject systemPrefab = Resources.Load<GameObject>(_systemPrefabPath);
if (systemPrefab == null)
{
throw new ClientSimException("Failed to start Client Sim! Main system prefab was not found.");
}
GameObject systemInstance = Instantiate(systemPrefab);
systemInstance.name = $"__{systemPrefab.name}";
ClientSimMain main = systemInstance.GetComponent<ClientSimMain>();
if (main == null)
{
throw new ClientSimException("Failed to start Client Sim! Main system component not found.");
}
try
{
main.Initialize(settings, eventDispatcher);
}
catch (ClientSimException e)
{
#if UNITY_EDITOR
// Tests expect certain exceptions, don't exit play mode
if (ClientSimRuntimeLoader.IsInUnityTest())
{
throw e;
}
else
{
Debug.LogError($"Play mode Stopped because: {e.Message}");
UnityEditor.EditorApplication.isPlaying = false;
}
#else
Debug.LogError($"Tried to initialize ClientSim outside of the Unity Editor: {e.Message}");
#endif
}
}
public static bool HasInstance()
{
return _instance;
}
internal static bool TryGetInstance(out ClientSimMain instance)
{
if (_instance != null)
{
instance = _instance;
return true;
}
instance = null;
return false;
}
public static void SpawnRemotePlayer(string name = null)
{
if (HasInstance())
{
_instance.SpawnRemotePlayerAndInitialize(name);
}
}
public static void RemovePlayer(VRCPlayerApi player)
{
if (HasInstance())
{
_instance._playerManager.RemovePlayer(player);
}
}
// Used in tests
public static void RemoveInstance()
{
if (HasInstance())
{
DestroyImmediate(_instance.gameObject);
}
}
protected override void Awake()
{
base.Awake();
if (_instance)
{
DestroyImmediate(this);
throw new ClientSimException("Multiple instances of ClientSim running!");
}
_instance = this;
this.Log("Starting ClientSim");
DontDestroyOnLoad(this);
SpawnProxyObjects();
}
private void OnDestroy()
{
if (_proxyObject)
{
_proxyObject.DestroyProxy();
}
_playerManager?.Dispose();
if (_instance != this)
{
return;
}
RemoveSDKLinks();
_instance = null;
}
private void Initialize(
ClientSimSettings settings,
IClientSimEventDispatcher eventDispatcher)
{
_sceneManager = new ClientSimSceneManager();
if (!_sceneManager.HasSceneDescriptor())
{
_instance = null;
Destroy(gameObject);
Debug.LogWarning("Cannot start ClientSim if there is no scene descriptor!");
return;
}
ClientSimNetworkingUtilities.ConfigureNetworkOnScene(VRC_SceneDescriptor.Instance);
_settings = settings;
// Event Dispatcher is provided during tests to listen and send events before everything has been initialized.
_eventDispatcher = eventDispatcher ?? new ClientSimEventDispatcher();
_blacklistManager = new ClientSimBlacklistManager();
_blacklistManager.AddObjectAndChildrenToBlackList(gameObject);
UdonManager.Instance.LightReservedLayerMask = LayerHelper.Mask_ReservedRenderLayers.value;
Texture2DDefaultTextureHolder.BlacklistDefaultTextures(UdonManager.Instance);
_interactiveLayerProvider = new ClientSimInteractiveLayerProvider(_eventDispatcher);
_sessionState = new ClientSimSessionState();
inputManager.Initialize(_settings);
IClientSimInput input = inputManager.GetInput();
_udonEventSender = new ClientSimUdonManagerEventSender(UdonManager.Instance);
HeightManager = new ClientSimPlayerHeightManager(_eventDispatcher, _udonEventSender);
udonInput.Initialize(_eventDispatcher, input);
menu.Initialize(_eventDispatcher, input, _settings, _sessionState, HeightManager);
baseInput.Initialize(_eventDispatcher, input, _settings);
inputModule.Initialize(_interactiveLayerProvider);
_playerManager = new ClientSimPlayerManager(_eventDispatcher, HeightManager);
// ObjectManager must be initialized before UdonManager to ensure object ownership for leaving players
// is handled first.
syncedObjectManager.Initialize(_eventDispatcher, _sceneManager, _playerManager);
_udonManager = new ClientSimUdonManager(_eventDispatcher, syncedObjectManager, _udonEventSender);
playerSpawner.Initialize(_sceneManager, _playerManager, _blacklistManager, _eventDispatcher, null);
// Option to allow for spawning remote players first to prevent the local player from being master.
// TODO replace this with networking test system to save previous player count in the instance.
if (!_settings.localPlayerIsMaster)
{
SpawnRemotePlayerAndInitialize();
}
// Spawn player controller to ensure that Local Player specific methods initialize properly.
// This included getting the Player's camera.
SpawnLocalPlayer(_settings.customLocalPlayerName);
_player.Initialize(
_eventDispatcher,
input,
_settings,
highlightManager,
tooltipManager,
_interactiveLayerProvider,
baseInput,
_sceneManager,
_proxyObject,
_udonEventSender,
_blacklistManager,
_udonManager,
syncedObjectManager,
_playerManager,
HeightManager);
Camera playerCamera = _player.GetCameraProvider().GetCamera();
tooltipManager.Initialize(_settings, _player.GetTrackingProvider());
highlightManager.Initialize(playerCamera);
stackedCameraSystem.Initialize(playerCamera, menu, _eventDispatcher);
storeManager = new ClientSimStoreManager(_eventDispatcher, new ClientSimUdonManagerEventSender(UdonManager.Instance), _playerManager);
// Initialize SDK links after everything has been created and initialized.
SetupSDKLinks();
}
#region Platform Management
private Vector2 CurrentResolution;
private VRCOrientation CurrentOrientation;
private const int ScreenChangePollRate = 100;
private async UniTaskVoid CheckForScreenChange()
{
while (_isReady)
{
if ((int)CurrentResolution.x != UnityEngine.Device.Screen.width || (int)CurrentResolution.y != UnityEngine.Device.Screen.height)
{
CurrentResolution = new Vector2(UnityEngine.Device.Screen.width, UnityEngine.Device.Screen.height);
_udonEventSender.RunEvent("_onVRCCameraSettingsChanged", ("camera", VRCCameraSettings.ScreenCamera));
}
var currentOrientation = DetermineOrientation();
if (currentOrientation != CurrentOrientation)
{
CurrentOrientation = DetermineOrientation();
// send ScreenUpdate event
var screenUpdateEvent = new ClientSimScreenUpdateEvent()
{
data = new ScreenUpdateData()
{
type = ScreenUpdateType.OrientationChanged,
orientation = CurrentOrientation,
resolution = CurrentResolution
}
};
_eventDispatcher.SendEvent(screenUpdateEvent);
}
await UniTask.Delay(ScreenChangePollRate);
}
}
private VRCOrientation DetermineOrientation()
{
return Screen.width < Screen.height ? VRCOrientation.Portrait : VRCOrientation.Landscape;
}
#endregion
private void Start()
{
if (!_sceneManager.HasSceneDescriptor())
{
throw new ClientSimException("Cannot start ClientSim if there is no world descriptor!");
}
if (_settings.setTargetFrameRate)
{
Application.targetFrameRate = Mathf.Max(_settings.targetFrameRate, 1);
Time.fixedDeltaTime = 1f / Application.targetFrameRate;
}
else
{
Application.targetFrameRate = -1;
}
StartCoroutine(InitializeClientSim());
}
private IEnumerator InitializeClientSim()
{
// VRChatBug: VRChat does not initialize SDK components immediately. This delay is to provide Unity
// components time to work before Udon starts to simulate how it is in client.
float startDelay = Mathf.Max(_settings.initializationDelay, 0);
yield return new WaitForSeconds(startDelay);
_isReady = true;
// Enable the player if set to spawn in the settings.
if (_settings.spawnPlayer)
{
// Player can be invalid if we weren't able to spawn it
if (_player)
{
_player.isInstanceOwner = _settings.isInstanceOwner;
_player.isVRCPlus = _settings.isVRCPlus;
_sceneManager.ResetSpawnOrder(); // Avoids any spawn offsets from pre-initialization of players
_player.EnablePlayer(_sceneManager.GetSpawnPoint(false), _sceneManager.GetSpawnRadius());
}
}
// Notify UdonManager that ClientSim is ready. This will then notify all registered UdonBehaviours that
// they can begin running. Udon will initialize in the next frame in the next Update call.
yield return _udonManager.OnClientSimReady();
// Notify PlayerManager to send OnPlayerJoined events.
// This must happen after Udon has been initialized to ensure UdonBehaviours are active to receive the event.
// This must happen before ClientSimReadyEvent as some listeners depend on this.
_playerManager.OnClientSimReady();
// Send event indicating ClientSim is initialized and ready.
_eventDispatcher.SendEvent(new ClientSimReadyEvent());
stackedCameraSystem.Ready();
CheckForScreenChange().Forget();
this.Log("ClientSim Initialized");
}
private void SpawnProxyObjects()
{
if (!proxyObjectPrefab)
{
throw new ClientSimException("Failed to start Client Sim! Proxy object prefab was not found.");
}
GameObject proxyInstance = Instantiate(proxyObjectPrefab);
proxyInstance.name = "__" + proxyObjectPrefab.name;
DontDestroyOnLoad(proxyInstance);
_proxyObject = proxyInstance.GetComponent<ClientSimProxyObjects>();
if (!_proxyObject)
{
throw new ClientSimException("Failed to start Client Sim! Proxy object script was not found.");
}
// Do not add this object to the blacklist since proxy objects are not blacklisted in udon.
}
private void SpawnLocalPlayer(string playerName = "")
{
_player = playerSpawner.SpawnPlayer(playerName, true);
}
private void SpawnRemotePlayerAndInitialize(string playerName = "")
{
playerSpawner.SpawnPlayer(playerName, false);
}
private bool IsObjectReady(GameObject obj)
{
return _isReady;
}
public bool IsNetworkReady()
{
return _isReady;
}
private string GetUniqueStringForObject(GameObject obj)
{
return obj.GetInstanceID().ToString();
}
#if VRC_ENABLE_PLAYER_PERSISTENCE
private GameObject[] GetPlayerPersistence(VRCPlayerApi playerApi)
{
ClientSimPlayer player = playerApi.GetClientSimPlayer();
if (player == null || player.PlayerPersistenceRootObjects == null)
return System.Array.Empty<GameObject>();
return player.PlayerPersistenceRootObjects;
}
private Component FindComponentInPlayerObjects(VRCPlayerApi target, Component referenceComponent)
{
if (referenceComponent == null)
throw new ArgumentNullException(nameof(referenceComponent));
VRCPlayerObject sourceObject = referenceComponent.GetComponentInParent<VRC.SDK3.Components.VRCPlayerObject>(true);
if (sourceObject == null)
throw new ArgumentException($"{nameof(referenceComponent)} must be on a player object.");
GameObject networkGameObject = ((Component)referenceComponent.gameObject.GetComponentInParent<INetworkID>(true)).gameObject;
VRCSceneDescriptor sdk3Descriptor = (VRCSceneDescriptor)VRC_SceneDescriptor.Instance;
int idx = sdk3Descriptor.NetworkIDCollection.FindIndex((x) => x.gameObject == networkGameObject);
if (idx < 0)
throw new ArgumentException($"{nameof(referenceComponent)} must be on a player object in the NetworkIDCollection.");
VRC.SDKBase.Network.NetworkIDPair networkIdPair = sdk3Descriptor.NetworkIDCollection[idx];
// See ClientSimPlayer.SetupPlayerPersistence for how the ID is calculated
int desiredID = ClientSimNetworkingUtilities.FirstPlayerPersistenceID
+ networkIdPair.ID;
GameObject[] objs = GetPlayerPersistence(target);
if (objs == null)
{
Debug.LogError("Player persistence is missing for " + target.displayName);
return null;
}
var possibleMatches = VRC.Tools.FindComponentInPossibleClones(sourceObject.gameObject, referenceComponent, objs);
Debug.Log($"Found {possibleMatches.Count()} possible matches for {referenceComponent.name} in player objects for {target.displayName}");;
foreach (Component possibleMatch in possibleMatches)
{
if (!possibleMatch) continue;
ClientSimNetworkIdHolder networkIdHolder = possibleMatch.GetComponentInParent<ClientSimNetworkIdHolder>(true);
if (!networkIdHolder)
continue;
int thisID = networkIdHolder.GetNetworkView()?.GetNetworkId() ?? 0;
if (thisID <= 0)
{
Debug.LogError($"Network ID is invalid for {possibleMatch.name}");
continue;
}
int matchID = ClientSimNetworkingUtilities.FlattenPlayerViewId(thisID);
if (matchID == desiredID) return possibleMatch;
Debug.LogError($"Network ID {matchID} for {possibleMatch.name} does not match desired ID {desiredID}");
}
Debug.LogError($"Failed to find component {referenceComponent.GetType().Name} in player objects for {target.displayName}");
return null;
}
public void EnablePlayerObjects()
{
_player.EnablePlayerObjects();
}
public int GetPlayerDataUsageLimit() => 1024 * 100;
public int GetPlayerObjectsUsageLimit() => 1024 * 100;
public int GetPlayerDataUsage(VRCPlayerApi player)
{
Debug.LogWarning("Player Data usage is not accurate in ClientSim.");
return ClientSimPlayerDataWrapper.GetUsage(player);
}
public int GetPlayerObjectsUsage(VRCPlayerApi player)
{
Debug.LogWarning("Player Object usage is not accurate in ClientSim.");
ClientSimPlayer simPlayer = player.GetClientSimPlayer();
return simPlayer.PlayerObjectData == null ? 0 : simPlayer.PlayerObjectData.Size;
}
public void RequestStorageUsageUpdate()
{
UdonManager.Instance.RunEvent(UdonManager.UDON_EVENT_ONPERSISTENCEUSAGEUPDATED);
}
#endif
private static void CallNetworkConfigure(VRCNetworkBehaviour behaviour)
{
behaviour.NetworkConfigure();
}
private static bool IsSuffering() => false; // we did it chat, no more suffering
private static void SetShadowDistance(float low, float medium, float high, float mobile) => QualitySettings.shadowDistance = high;
private static void SetShadowDistanceAll(float distance) => QualitySettings.shadowDistance = distance;
private static void ResetShadowDistance() => QualitySettings.shadowDistance = 150; // TODO: base on project quality settings?
private void OnCameraSettingsChanged(Camera camera)
{
camera.farClipPlane = Mathf.Max(camera.farClipPlane, camera.nearClipPlane + 0.1f);
// don't allow turning off MirrorReflection or stacked layers (InternalUI)
camera.cullingMask &= ~(1 << LayerMask.NameToLayer("MirrorReflection"));
camera.cullingMask &= ~stackedCameraSystem.RequestedStackLayers;
}
private static void OpenAvatarListing(string id) => Debug.Log($"opening avatar listing for {id}");
#region VRChat SDK Links
// If adding to this list, be sure to also remove the link in the RemoveSDKLinks method
private void SetupSDKLinks()
{
UdonBehaviour.OnInit += _udonManager.InitUdon;
Networking._IsMaster += _playerManager.IsLocalPlayerMaster;
Networking._LocalPlayer += _playerManager.LocalPlayer;
Networking._GetOwner += _playerManager.GetOwner;
Networking._IsOwner += _playerManager.IsOwner;
Networking._SetOwner += ClientSimPlayerManager.SetOwner;
Networking._GetMaster += _playerManager.GetMaster;
Networking._GetInstanceOwner += _playerManager.GetInstanceOwner;
Networking._IsInstanceOwner += _playerManager.IsInstanceOwner;
Networking._IsObjectReady += IsObjectReady;
Networking._IsNetworkSettled += IsNetworkReady;
Networking._IsSuffering += IsSuffering;
Networking._GetUniqueName += GetUniqueStringForObject;
#if VRC_ENABLE_PLAYER_PERSISTENCE
ClientSimPlayerDataWrapper.ConfigureSDK();
Networking._GetPlayerPersistence += GetPlayerPersistence;
Networking._GetPlayerDataStorageUsage += GetPlayerDataUsage;
Networking._GetPlayerObjectStorageUsage += GetPlayerObjectsUsage;
Networking._GetPlayerDataStorageLimit += GetPlayerDataUsageLimit;
Networking._GetPlayerObjectStorageLimit += GetPlayerObjectsUsageLimit;
Networking._FindComponentInPlayerObjects += FindComponentInPlayerObjects;
#endif
#if VRC_ENABLE_PLAYER_PERSISTENCE || VRC_ENABLE_INSTANCE_PERSISTENCE
Networking._RequestStorageUsageUpdate += RequestStorageUsageUpdate;
#endif
VRCNetworkBehaviour.OnNetworkBehaviourAwake += CallNetworkConfigure;
VRCStation.Initialize += ClientSimStationHelper.InitializeStations;
VRCStation.useStationDelegate += ClientSimStationHelper.UseStation;
VRCStation.exitStationDelegate += ClientSimStationHelper.ExitStation;
VRCPlayerApi._UseAttachedStation += ClientSimStationHelper.UseAttachedStation;
if (_player != null)
{
VRC_UiShape.GetEventCamera += _player.GetCameraProvider().GetCameraForObject;
VRCCameraSettings.ScreenCameraRef = _player.GetCameraProvider().GetCamera();
VRCCameraSettings.ScreenCamera.GetCameraMode = () => VRCCameraMode.Screen;
}
VRC_Pickup.OnAwake += ClientSimPickupHelper.InitializePickup;
VRC_Pickup.ForceDrop += ClientSimPickupHelper.ForceDrop;
VRC_Pickup._GetCurrentPlayer += ClientSimPickupHelper.GetCurrentPlayer;
VRC_Pickup._GetPickupHand += ClientSimPickupHelper.GetPickupHand;
VRC_Pickup.OnDestroyed += ClientSimPickupHelper.PickupDestroy;
VRC_Pickup.HapticEvent += ClientSimPickupHelper.PlayHapticForPickup;
Components.VRCObjectPool.OnInit += syncedObjectManager.InitializeObjectPool;
Components.VRCObjectPool.OnReturn += ClientSimObjectPoolHelper.OnReturn;
Components.VRCObjectPool.OnSpawn += ClientSimObjectPoolHelper.OnSpawn;
Components.VRCObjectSync.OnAwake += syncedObjectManager.InitializeObjectSync;
Components.VRCObjectSync.FlagDiscontinuityHook += ClientSimObjectSyncHelper.FlagDiscontinuityHook;
Components.VRCObjectSync.RespawnHandler += ClientSimObjectSyncHelper.RespawnObject;
Components.VRCObjectSync.TeleportHandler += ClientSimObjectSyncHelper.TeleportTo;
Components.VRCObjectSync.SetGravityHook += ClientSimObjectSyncHelper.SetUseGravity;
Components.VRCObjectSync.SetKinematicHook += ClientSimObjectSyncHelper.SetIsKinematic;
VRCPlayerApi._GetPlayerId += _playerManager.GetPlayerID;
VRCPlayerApi._GetPlayerById += _playerManager.GetPlayerByID;
VRCPlayerApi._isMasterDelegate += _playerManager.IsMaster;
VRCPlayerApi._isSuspendedDelegate += _playerManager.IsSuspended;
VRCPlayerApi._isVRCPlusDelegate += _playerManager.IsVRCPlus;
VRCPlayerApi._TakeOwnership += ClientSimPlayerManager.SetOwner;
VRCPlayerApi._IsOwner += _playerManager.IsOwner;
VRCPlayerApi._isInstanceOwnerDelegate += _playerManager.IsInstanceOwner;
VRCPlayerApi._EnablePickups += ClientSimPlayerManager.EnablePickups;
VRCPlayerApi._Immobilize += ClientSimPlayerManager.Immobilize;
VRCPlayerApi._TeleportTo += ClientSimPlayerManager.TeleportTo;
VRCPlayerApi._TeleportToOrientation += ClientSimPlayerManager.TeleportToOrientation;
VRCPlayerApi._TeleportToOrientationLerp += ClientSimPlayerManager.TeleportToOrientationLerp;
VRCPlayerApi._PlayHapticEventInHand += ClientSimPlayerManager.PlayHapticEventInHand;
VRCPlayerApi._GetPlayerByGameObject += ClientSimPlayerManager.GetPlayerByGameObject;
VRCPlayerApi._GetPickupInHand += ClientSimPlayerManager.GetPickupInHand;
VRCPlayerApi._GetTrackingData += ClientSimPlayerManager.GetTrackingData;
VRCPlayerApi._GetBonePosition += ClientSimPlayerManager.GetBonePosition;
VRCPlayerApi._GetBoneRotation += ClientSimPlayerManager.GetBoneRotation;
VRCPlayerApi._ClearPlayerTags += ClientSimPlayerManager.ClearPlayerTags;
VRCPlayerApi._SetPlayerTag += ClientSimPlayerManager.SetPlayerTag;
VRCPlayerApi._GetPlayerTag += ClientSimPlayerManager.GetPlayerTag;
VRCPlayerApi._GetPlayersWithTag += ClientSimPlayerManager.GetPlayersWithTag;
VRCPlayerApi._SetSilencedToTagged += ClientSimPlayerManager.SetSilencedToTagged;
VRCPlayerApi._SetSilencedToUntagged += ClientSimPlayerManager.SetSilencedToUntagged;
VRCPlayerApi._ClearSilence += ClientSimPlayerManager.ClearSilence;
VRCPlayerApi._IsUserInVR += ClientSimPlayerManager.IsUserInVR;
VRCPlayerApi._GetRunSpeed += ClientSimPlayerManager.GetRunSpeed;
VRCPlayerApi._SetRunSpeed += ClientSimPlayerManager.SetRunSpeed;
VRCPlayerApi._GetWalkSpeed += ClientSimPlayerManager.GetWalkSpeed;
VRCPlayerApi._SetWalkSpeed += ClientSimPlayerManager.SetWalkSpeed;
VRCPlayerApi._GetJumpImpulse += ClientSimPlayerManager.GetJumpImpulse;
VRCPlayerApi._SetJumpImpulse += ClientSimPlayerManager.SetJumpImpulse;
VRCPlayerApi._GetStrafeSpeed += ClientSimPlayerManager.GetStrafeSpeed;
VRCPlayerApi._SetStrafeSpeed += ClientSimPlayerManager.SetStrafeSpeed;
VRCPlayerApi._GetVelocity += ClientSimPlayerManager.GetVelocity;
VRCPlayerApi._SetVelocity += ClientSimPlayerManager.SetVelocity;
VRCPlayerApi._GetPosition += ClientSimPlayerManager.GetPosition;
VRCPlayerApi._GetRotation += ClientSimPlayerManager.GetRotation;
VRCPlayerApi._GetGravityStrength += ClientSimPlayerManager.GetGravityStrength;
VRCPlayerApi._SetGravityStrength += ClientSimPlayerManager.SetGravityStrength;
VRCPlayerApi.IsGrounded += ClientSimPlayerManager.IsGrounded;
VRCPlayerApi._UseLegacyLocomotion += ClientSimPlayerManager.UseLegacyLocomotion;
VRCPlayerApi._Respawn += ClientSimPlayerManager.Respawn;
VRCPlayerApi._RespawnWithIndex += ClientSimPlayerManager.RespawnWithIndex;
VRCPlayerApi._GetManualAvatarScalingAllowed += ClientSimPlayerManager.GetManualAvatarScalingAllowed;
VRCPlayerApi._SetManualAvatarScalingAllowed += ClientSimPlayerManager.SetManualAvatarScalingAllowed;
VRCPlayerApi._GetAvatarEyeHeightMinimumAsMeters += ClientSimPlayerManager.GetAvatarEyeHeightMinimumAsMeters;
VRCPlayerApi._GetAvatarEyeHeightMaximumAsMeters += ClientSimPlayerManager.GetAvatarEyeHeightMaximumAsMeters;
VRCPlayerApi._SetAvatarEyeHeightMinimumByMeters += ClientSimPlayerManager.SetAvatarEyeHeightMinimumByMeters;
VRCPlayerApi._SetAvatarEyeHeightMaximumByMeters += ClientSimPlayerManager.SetAvatarEyeHeightMaximumByMeters;
VRCPlayerApi._GetAvatarEyeHeightAsMeters += ClientSimPlayerManager.GetAvatarEyeHeightAsMeters;
VRCPlayerApi._SetAvatarEyeHeightByMeters += ClientSimPlayerManager.SetAvatarEyeHeightByMeters;
VRCPlayerApi._SetAvatarEyeHeightByMultiplier += ClientSimPlayerManager.SetAvatarEyeHeightByMultiplier;
VRCPlayerApi._CombatSetup += ClientSimCombatSystemHelper.CombatSetup;
VRCPlayerApi._CombatSetMaxHitpoints += ClientSimCombatSystemHelper.CombatSetMaxHitpoints;
VRCPlayerApi._CombatGetCurrentHitpoints += ClientSimCombatSystemHelper.CombatGetCurrentHitpoints;
VRCPlayerApi._CombatSetRespawn += ClientSimCombatSystemHelper.CombatSetRespawn;
VRCPlayerApi._CombatSetDamageGraphic += ClientSimCombatSystemHelper.CombatSetDamageGraphic;
VRCPlayerApi._CombatGetDestructible += ClientSimCombatSystemHelper.CombatGetDestructible;
VRCPlayerApi._CombatSetCurrentHitpoints += ClientSimCombatSystemHelper.CombatSetCurrentHitpoints;
VRCPlayerApi._SetAvatarAudioVolumetricRadius += ClientSimPlayerManager.SetAvatarAudioVolumetricRadius;
VRCPlayerApi._SetAvatarAudioNearRadius += ClientSimPlayerManager.SetAvatarAudioNearRadius;
VRCPlayerApi._SetAvatarAudioFarRadius += ClientSimPlayerManager.SetAvatarAudioFarRadius;
VRCPlayerApi._SetAvatarAudioGain += ClientSimPlayerManager.SetAvatarAudioGain;
VRCPlayerApi._SetAvatarAudioForceSpatial += ClientSimPlayerManager.SetAvatarAudioForceSpatial;
VRCPlayerApi._SetAvatarAudioCustomCurve += ClientSimPlayerManager.SetAvatarAudioCustomCurve;
VRCPlayerApi._SetVoiceGain += ClientSimPlayerManager.SetVoiceGain;
VRCPlayerApi._SetVoiceDistanceNear += ClientSimPlayerManager.SetVoiceDistanceNear;
VRCPlayerApi._SetVoiceDistanceFar += ClientSimPlayerManager.SetVoiceDistanceFar;
VRCPlayerApi._SetVoiceVolumetricRadius += ClientSimPlayerManager.SetVoiceVolumetricRadius;
VRCPlayerApi._SetVoiceLowpass += ClientSimPlayerManager.SetVoiceLowpass;
VRCPlayerApi._GetVoiceGain += ClientSimPlayerManager.GetVoiceGain;
VRCPlayerApi._GetVoiceDistanceNear += ClientSimPlayerManager.GetVoiceDistanceNear;
VRCPlayerApi._GetVoiceDistanceFar += ClientSimPlayerManager.GetVoiceDistanceFar;
VRCPlayerApi._GetVoiceVolumetricRadius += ClientSimPlayerManager.GetVoiceVolumetricRadius;
VRCPlayerApi._GetVoiceLowpass += ClientSimPlayerManager.GetVoiceLowpass;
VRCPlayerApi._GetCurrentLanguage += ClientSimPlayerManager.GetCurrentLanguage;
VRCPlayerApi._GetAvailableLanguages += ClientSimPlayerManager.GetAvailableLanguages;
VRC_SpatialAudioSource.Initialize += ClientSimSpatialAudioHelper.InitializeAudio;
VRCAVProVideoPlayer.Initialize += ClientSimAVProVideoStub.InitializePlayer;
InputManager._EnableObjectHighlight += highlightManager.EnableObjectHighlight;
InputManager._GetLastUsedInputMethod += inputManager.GetLastUsedInputMethod;
Store._sendProductEvent += ClientSimStoreManager.SendProductEvent;
Store._listPurchases += ClientSimStoreManager.ListPurchases;
Store._listAvailableProducts += ClientSimStoreManager.ListAvailableProducts;
Store._doesPlayerOwnProduct += ClientSimStoreManager.DoesPlayerOwnProduct;
Store._doesAnyPlayerOwnProduct += ClientSimStoreManager.DoesAnyPlayerOwnProduct;
Store._getPlayersWhoOwnProduct += ClientSimStoreManager.GetPlayersWhoOwnProduct;
Store._listProductOwners += ClientSimStoreManager.ListProductOwners;
VRCQualitySettings.SetShadowDistanceInternal += SetShadowDistance;
VRCQualitySettings.SetShadowDistanceInternalAll += SetShadowDistanceAll;
VRCQualitySettings.ResetShadowDistanceInternal += ResetShadowDistance;
VRCCameraSettings.OnCameraSettingsChanged += OnCameraSettingsChanged;
NetworkCalling.GetAllQueuedEventsProxy += ClientSimNetworkCalling.GetAllQueuedEventsProxy;
NetworkCalling.GetQueuedEventsProxy += ClientSimNetworkCalling.GetQueuedEventsProxy;
NetworkCalling.SendCustomNetworkEventProxy += ClientSimNetworkCalling.SendCustomNetworkEventProxy;
VRC.SDK3.Components.VRCOpenMenu._OpenAvatarListingDelegate = OpenAvatarListing;
}
private void RemoveSDKLinks()
{
UdonBehaviour.OnInit -= _udonManager.InitUdon;
Networking._IsMaster -= _playerManager.IsLocalPlayerMaster;
Networking._LocalPlayer -= _playerManager.LocalPlayer;
Networking._GetOwner -= _playerManager.GetOwner;
Networking._IsOwner -= _playerManager.IsOwner;
Networking._SetOwner -= ClientSimPlayerManager.SetOwner;
Networking._IsInstanceOwner -= _playerManager.IsInstanceOwner;
Networking._IsObjectReady -= IsObjectReady;
Networking._IsNetworkSettled -= IsNetworkReady;
Networking._IsSuffering -= IsSuffering;
Networking._GetUniqueName -= GetUniqueStringForObject;
#if VRC_ENABLE_PLAYER_PERSISTENCE
Networking._GetPlayerPersistence -= GetPlayerPersistence;
Networking._GetPlayerDataStorageUsage -= GetPlayerDataUsage;
Networking._GetPlayerObjectStorageUsage -= GetPlayerObjectsUsage;
Networking._GetPlayerDataStorageLimit -= GetPlayerDataUsageLimit;
Networking._GetPlayerObjectStorageLimit -= GetPlayerObjectsUsageLimit;
Networking._FindComponentInPlayerObjects -= FindComponentInPlayerObjects;
#endif
#if VRC_ENABLE_PLAYER_PERSISTENCE || VRC_ENABLE_INSTANCE_PERSISTENCE
Networking._RequestStorageUsageUpdate -= RequestStorageUsageUpdate;
#endif
VRCNetworkBehaviour.OnNetworkBehaviourAwake -= CallNetworkConfigure;
VRCStation.Initialize -= ClientSimStationHelper.InitializeStations;
VRCStation.useStationDelegate -= ClientSimStationHelper.UseStation;
VRCStation.exitStationDelegate -= ClientSimStationHelper.ExitStation;
VRCPlayerApi._UseAttachedStation -= ClientSimStationHelper.UseAttachedStation;
// Player can be invalid if we weren't able to spawn
if (_player)
{
VRC_UiShape.GetEventCamera -= _player.GetCameraProvider().GetCameraForObject;
}
VRC_Pickup.OnAwake -= ClientSimPickupHelper.InitializePickup;
VRC_Pickup.ForceDrop -= ClientSimPickupHelper.ForceDrop;
VRC_Pickup._GetCurrentPlayer -= ClientSimPickupHelper.GetCurrentPlayer;
VRC_Pickup._GetPickupHand -= ClientSimPickupHelper.GetPickupHand;
VRC_Pickup.OnDestroyed -= ClientSimPickupHelper.PickupDestroy;
VRC_Pickup.HapticEvent -= ClientSimPickupHelper.PlayHapticForPickup;
Components.VRCObjectPool.OnInit -= syncedObjectManager.InitializeObjectPool;
Components.VRCObjectPool.OnReturn -= ClientSimObjectPoolHelper.OnReturn;
Components.VRCObjectPool.OnSpawn -= ClientSimObjectPoolHelper.OnSpawn;
Components.VRCObjectSync.OnAwake -= syncedObjectManager.InitializeObjectSync;
Components.VRCObjectSync.FlagDiscontinuityHook -= ClientSimObjectSyncHelper.FlagDiscontinuityHook;
Components.VRCObjectSync.RespawnHandler -= ClientSimObjectSyncHelper.RespawnObject;
Components.VRCObjectSync.TeleportHandler -= ClientSimObjectSyncHelper.TeleportTo;
Components.VRCObjectSync.SetGravityHook -= ClientSimObjectSyncHelper.SetUseGravity;
Components.VRCObjectSync.SetKinematicHook -= ClientSimObjectSyncHelper.SetIsKinematic;
VRCPlayerApi._GetPlayerId -= _playerManager.GetPlayerID;
VRCPlayerApi._GetPlayerById -= _playerManager.GetPlayerByID;
VRCPlayerApi._isMasterDelegate -= _playerManager.IsMaster;
VRCPlayerApi._isSuspendedDelegate -= _playerManager.IsSuspended;
VRCPlayerApi._isVRCPlusDelegate -= _playerManager.IsVRCPlus;
VRCPlayerApi._TakeOwnership -= ClientSimPlayerManager.SetOwner;
VRCPlayerApi._IsOwner -= _playerManager.IsOwner;
VRCPlayerApi._isInstanceOwnerDelegate -= _playerManager.IsInstanceOwner;
VRCPlayerApi._EnablePickups -= ClientSimPlayerManager.EnablePickups;
VRCPlayerApi._Immobilize -= ClientSimPlayerManager.Immobilize;
VRCPlayerApi._TeleportTo -= ClientSimPlayerManager.TeleportTo;
VRCPlayerApi._TeleportToOrientation -= ClientSimPlayerManager.TeleportToOrientation;
VRCPlayerApi._TeleportToOrientationLerp -= ClientSimPlayerManager.TeleportToOrientationLerp;
VRCPlayerApi._PlayHapticEventInHand -= ClientSimPlayerManager.PlayHapticEventInHand;
VRCPlayerApi._GetPlayerByGameObject -= ClientSimPlayerManager.GetPlayerByGameObject;
VRCPlayerApi._GetPickupInHand -= ClientSimPlayerManager.GetPickupInHand;
VRCPlayerApi._GetTrackingData -= ClientSimPlayerManager.GetTrackingData;
VRCPlayerApi._GetBonePosition -= ClientSimPlayerManager.GetBonePosition;
VRCPlayerApi._GetBoneRotation -= ClientSimPlayerManager.GetBoneRotation;
VRCPlayerApi._ClearPlayerTags -= ClientSimPlayerManager.ClearPlayerTags;
VRCPlayerApi._SetPlayerTag -= ClientSimPlayerManager.SetPlayerTag;
VRCPlayerApi._GetPlayerTag -= ClientSimPlayerManager.GetPlayerTag;
VRCPlayerApi._GetPlayersWithTag -= ClientSimPlayerManager.GetPlayersWithTag;
VRCPlayerApi._SetSilencedToTagged -= ClientSimPlayerManager.SetSilencedToTagged;
VRCPlayerApi._SetSilencedToUntagged -= ClientSimPlayerManager.SetSilencedToUntagged;
VRCPlayerApi._ClearSilence -= ClientSimPlayerManager.ClearSilence;
VRCPlayerApi._IsUserInVR -= ClientSimPlayerManager.IsUserInVR;
VRCPlayerApi._GetRunSpeed -= ClientSimPlayerManager.GetRunSpeed;
VRCPlayerApi._SetRunSpeed -= ClientSimPlayerManager.SetRunSpeed;
VRCPlayerApi._GetWalkSpeed -= ClientSimPlayerManager.GetWalkSpeed;
VRCPlayerApi._SetWalkSpeed -= ClientSimPlayerManager.SetWalkSpeed;
VRCPlayerApi._GetJumpImpulse -= ClientSimPlayerManager.GetJumpImpulse;
VRCPlayerApi._SetJumpImpulse -= ClientSimPlayerManager.SetJumpImpulse;
VRCPlayerApi._GetStrafeSpeed -= ClientSimPlayerManager.GetStrafeSpeed;
VRCPlayerApi._SetStrafeSpeed -= ClientSimPlayerManager.SetStrafeSpeed;
VRCPlayerApi._GetVelocity -= ClientSimPlayerManager.GetVelocity;
VRCPlayerApi._SetVelocity -= ClientSimPlayerManager.SetVelocity;
VRCPlayerApi._GetPosition -= ClientSimPlayerManager.GetPosition;
VRCPlayerApi._GetRotation -= ClientSimPlayerManager.GetRotation;
VRCPlayerApi._GetGravityStrength -= ClientSimPlayerManager.GetGravityStrength;
VRCPlayerApi._SetGravityStrength -= ClientSimPlayerManager.SetGravityStrength;
VRCPlayerApi.IsGrounded -= ClientSimPlayerManager.IsGrounded;
VRCPlayerApi._UseLegacyLocomotion -= ClientSimPlayerManager.UseLegacyLocomotion;
VRCPlayerApi._Respawn -= ClientSimPlayerManager.Respawn;
VRCPlayerApi._RespawnWithIndex -= ClientSimPlayerManager.RespawnWithIndex;
VRCPlayerApi._GetManualAvatarScalingAllowed -= ClientSimPlayerManager.GetManualAvatarScalingAllowed;
VRCPlayerApi._SetManualAvatarScalingAllowed -= ClientSimPlayerManager.SetManualAvatarScalingAllowed;
VRCPlayerApi._GetAvatarEyeHeightMinimumAsMeters -= ClientSimPlayerManager.GetAvatarEyeHeightMinimumAsMeters;
VRCPlayerApi._GetAvatarEyeHeightMaximumAsMeters -= ClientSimPlayerManager.GetAvatarEyeHeightMaximumAsMeters;
VRCPlayerApi._SetAvatarEyeHeightMinimumByMeters -= ClientSimPlayerManager.SetAvatarEyeHeightMinimumByMeters;
VRCPlayerApi._SetAvatarEyeHeightMaximumByMeters -= ClientSimPlayerManager.SetAvatarEyeHeightMaximumByMeters;
VRCPlayerApi._GetAvatarEyeHeightAsMeters -= ClientSimPlayerManager.GetAvatarEyeHeightAsMeters;
VRCPlayerApi._SetAvatarEyeHeightByMeters -= ClientSimPlayerManager.SetAvatarEyeHeightByMeters;
VRCPlayerApi._SetAvatarEyeHeightByMultiplier -= ClientSimPlayerManager.SetAvatarEyeHeightByMultiplier;
VRCPlayerApi._CombatSetup -= ClientSimCombatSystemHelper.CombatSetup;
VRCPlayerApi._CombatSetMaxHitpoints -= ClientSimCombatSystemHelper.CombatSetMaxHitpoints;
VRCPlayerApi._CombatGetCurrentHitpoints -= ClientSimCombatSystemHelper.CombatGetCurrentHitpoints;
VRCPlayerApi._CombatSetRespawn -= ClientSimCombatSystemHelper.CombatSetRespawn;
VRCPlayerApi._CombatSetDamageGraphic -= ClientSimCombatSystemHelper.CombatSetDamageGraphic;
VRCPlayerApi._CombatGetDestructible -= ClientSimCombatSystemHelper.CombatGetDestructible;
VRCPlayerApi._CombatSetCurrentHitpoints -= ClientSimCombatSystemHelper.CombatSetCurrentHitpoints;
VRCPlayerApi._SetAvatarAudioVolumetricRadius -= ClientSimPlayerManager.SetAvatarAudioVolumetricRadius;
VRCPlayerApi._SetAvatarAudioNearRadius -= ClientSimPlayerManager.SetAvatarAudioNearRadius;
VRCPlayerApi._SetAvatarAudioFarRadius -= ClientSimPlayerManager.SetAvatarAudioFarRadius;
VRCPlayerApi._SetAvatarAudioGain -= ClientSimPlayerManager.SetAvatarAudioGain;
VRCPlayerApi._SetAvatarAudioForceSpatial -= ClientSimPlayerManager.SetAvatarAudioForceSpatial;
VRCPlayerApi._SetAvatarAudioCustomCurve -= ClientSimPlayerManager.SetAvatarAudioCustomCurve;
VRCPlayerApi._SetVoiceLowpass -= ClientSimPlayerManager.SetVoiceLowpass;
VRCPlayerApi._SetVoiceVolumetricRadius -= ClientSimPlayerManager.SetVoiceVolumetricRadius;
VRCPlayerApi._SetVoiceDistanceFar -= ClientSimPlayerManager.SetVoiceDistanceFar;
VRCPlayerApi._SetVoiceDistanceNear -= ClientSimPlayerManager.SetVoiceDistanceNear;
VRCPlayerApi._SetVoiceGain -= ClientSimPlayerManager.SetVoiceGain;
VRC_SpatialAudioSource.Initialize -= ClientSimSpatialAudioHelper.InitializeAudio;
VRCAVProVideoPlayer.Initialize -= ClientSimAVProVideoStub.InitializePlayer;
InputManager._EnableObjectHighlight -= highlightManager.EnableObjectHighlight;
Store._sendProductEvent -= ClientSimStoreManager.SendProductEvent;
Store._listPurchases -= ClientSimStoreManager.ListPurchases;
Store._listAvailableProducts -= ClientSimStoreManager.ListAvailableProducts;
Store._doesPlayerOwnProduct -= ClientSimStoreManager.DoesPlayerOwnProduct;
Store._doesAnyPlayerOwnProduct -= ClientSimStoreManager.DoesAnyPlayerOwnProduct;
Store._getPlayersWhoOwnProduct -= ClientSimStoreManager.GetPlayersWhoOwnProduct;
VRCQualitySettings.SetShadowDistanceInternal -= SetShadowDistance;
VRCQualitySettings.SetShadowDistanceInternalAll -= SetShadowDistanceAll;
VRCQualitySettings.ResetShadowDistanceInternal -= ResetShadowDistance;
VRCCameraSettings.OnCameraSettingsChanged -= OnCameraSettingsChanged;
NetworkCalling.GetAllQueuedEventsProxy -= ClientSimNetworkCalling.GetAllQueuedEventsProxy;
NetworkCalling.GetQueuedEventsProxy -= ClientSimNetworkCalling.GetQueuedEventsProxy;
NetworkCalling.SendCustomNetworkEventProxy -= ClientSimNetworkCalling.SendCustomNetworkEventProxy;
VRC.SDK3.Components.VRCOpenMenu._OpenAvatarListingDelegate -= OpenAvatarListing;
}
#endregion
#region Test Accessors
internal ClientSimProxyObjects GetProxyObjects()
{
return _proxyObject;
}
internal ClientSimMenu GetMenu()
{
return menu;
}
internal IClientSimEventDispatcher GetEventDispatcher()
{
return _eventDispatcher;
}
internal IClientSimUdonEventSender GetUdonEventSender()
{
return _udonEventSender;
}
internal IClientSimBlacklistManager GetBlacklistManager()
{
return _blacklistManager;
}
internal IClientSimUdonManager GetUdonManager()
{
return _udonManager;
}
internal IClientSimSyncedObjectManager GetSyncedObjectManager()
{
return syncedObjectManager;
}
internal IClientSimPlayerManager GetPlayerManager()
{
return _playerManager;
}
#endregion
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f8dfd7f39e425354dbab5d2c2064fe0a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,576 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using UnityEngine;
using UnityEngine.UI;
using VRC.SDKBase;
using VRC.Udon.Common;
namespace VRC.SDK3.ClientSim
{
/// <summary>
/// System responsible for displaying the menu to the players and updating settings.
/// </summary>
/// <remarks>
/// Sends Events:
/// - ClientSimMenuStateChangedEvent
/// - ClientSimMenuRespawnClickedEvent
/// - ClientSimOnPlayerHeightUpdateEvent
/// Listens to Events:
/// - ClientSimReadyEvent
/// - ClientSimOnPlayerMovedEvent
/// - ClientSimOnNewMasterEvent
/// - ClientSimOnPlayerJoinedEvent
/// Listens to Input Events:
/// - ToggleMenu
/// </remarks>
[AddComponentMenu("")]
public class ClientSimMenu : ClientSimBehaviour, IDisposable
{
// Property name on UI shaders to set the ZTest mode. Used to make the menu appear on top of everything.
private const string GUI_ZTEST_MODE_PROPERTY_NAME = "unity_GUIZTestMode";
private const string HAS_USER_ACCEPTED_WARNING = "accepted_warning";
public enum ClientSimDisplayedPage
{
PAUSE_MENU,
WARNING_PAGE,
INVALID_SETTINGS_PAGE,
DELAYED_START_PAGE,
}
// The method to open the settings window is set from Editor context.
// This hook is set on playmode start in ClientSimEditorRuntimeLinker.cs
internal static Action openSettingsHook;
// This method allows the menu to check the editor only method if ClientSim has all settings properly set
internal static Func<bool> checkValidSettingsHook;
[SerializeField]
private GameObject menu;
public float menuScaleFactor = 0.0035f;
[SerializeField]
private GameObject pauseMenu;
[SerializeField]
private GameObject warningsPage;
[SerializeField]
private GameObject invalidSettingsPage;
[SerializeField]
private GameObject delayStartPage;
[SerializeField]
private Toggle tooltipsToggle;
[SerializeField]
private Toggle reticleToggle;
[SerializeField]
private Toggle invertMouseToggle;
[SerializeField]
private Toggle consoleLoggingToggle;
[SerializeField]
private Slider playerHeightSlider;
[SerializeField]
private Text playerHeightText;
[SerializeField]
private Text playerNameText;
[SerializeField]
private Text playerIdText;
[SerializeField]
private Toggle isMasterToggle;
[SerializeField]
private Toggle isInstanceOwnerToggle;
[SerializeField]
private Toggle isVRCPlusToggle;
private IClientSimEventDispatcher _eventDispatcher;
private IClientSimInput _input;
private ClientSimSettings _settings;
private IClientSimSessionState _sessionState;
private IClientSimPlayerHeightManager _heightManager;
private ClientSimDisplayedPage _displayedPage = ClientSimDisplayedPage.WARNING_PAGE;
private bool _menuIsActive;
private bool _stackedCameraReady = false;
private float _playerHeightOriginalMaxvalue;
private Canvas _menuCanvas;
public void SetCanvasCamera(Camera cam)
{
_menuCanvas.worldCamera = cam;
}
protected override void Awake()
{
base.Awake();
_menuCanvas = menu.GetComponent<Canvas>();
_playerHeightOriginalMaxvalue = playerHeightSlider.maxValue;
}
public void Initialize(
IClientSimEventDispatcher eventDispatcher,
IClientSimInput input,
ClientSimSettings settings,
IClientSimSessionState sessionState,
IClientSimPlayerHeightManager heightManager)
{
_eventDispatcher = eventDispatcher;
_input = input;
_settings = settings;
_sessionState = sessionState;
_heightManager = heightManager;
// Input will be null with incorrect Unity input project settings.
_input?.SubscribeToggleMenu(HandleInputMenuToggle);
_eventDispatcher.Subscribe<ClientSimOnPlayerJoinedEvent>(OnPlayerJoined);
_eventDispatcher.Subscribe<ClientSimOnNewMasterEvent>(OnMasterChange);
_eventDispatcher.Subscribe<ClientSimReadyEvent>(OnReady);
_eventDispatcher.Subscribe<ClientSimOnPlayerHeightUpdateEvent>(OnPlayerHeightUpdate);
_eventDispatcher.Subscribe<ClientSimOnToggleManualScalingEvent>(OnManualScalingToggled);
_eventDispatcher.Subscribe<ClientSimStackedCameraReadyEvent>(OnStackedCameraReady);
playerNameText.text = "";
playerIdText.text = "";
isMasterToggle.isOn = false;
isInstanceOwnerToggle.isOn = settings.isInstanceOwner;
isVRCPlusToggle.isOn = settings.isVRCPlus;
isVRCPlusToggle.onValueChanged.AddListener(OnVRCPlusToggleChanged);
UpdateValuesFromSettings();
#if UNITY_EDITOR
UnityEditor.SceneVisibilityManager.instance.Hide(gameObject, true);
#endif
}
public ClientSimDisplayedPage GetDisplayedPage()
{
return _displayedPage;
}
private void Start()
{
SetUIOverlayMaterial();
bool shouldShowMenu = true;
if (checkValidSettingsHook?.Invoke() == false)
{
// Force open the ClientSim settings window.
// When first importing ClientSim and disabling legacy input, all UI menus fail to react to the mouse,
// preventing users from clicking the settings window button.
OpenSettings();
SetDisplayedPage(ClientSimDisplayedPage.INVALID_SETTINGS_PAGE);
}
else if (_settings.initializationDelay > 0)
{
SetDisplayedPage(ClientSimDisplayedPage.DELAYED_START_PAGE);
}
else if (_settings.spawnPlayer)
{
DisplayInitialPageForPlayer();
}
else
{
shouldShowMenu = false;
}
ToggleMenu(shouldShowMenu);
}
private void OnDestroy()
{
Dispose();
}
public void Dispose()
{
_input?.UnsubscribeToggleMenu(HandleInputMenuToggle);
_eventDispatcher?.Unsubscribe<ClientSimOnPlayerJoinedEvent>(OnPlayerJoined);
_eventDispatcher?.Unsubscribe<ClientSimOnNewMasterEvent>(OnMasterChange);
_eventDispatcher?.Unsubscribe<ClientSimReadyEvent>(OnReady);
_eventDispatcher?.Unsubscribe<ClientSimOnPlayerMovedEvent>(OnPlayerMoved);
_eventDispatcher?.Unsubscribe<ClientSimOnToggleManualScalingEvent>(OnManualScalingToggled);
}
private void Update()
{
// Only update the menu settings while the menu is displayed.
if (_menuIsActive)
{
UpdateValuesFromSettings();
}
}
private void SetUIOverlayMaterial()
{
int propertyId = Shader.PropertyToID(GUI_ZTEST_MODE_PROPERTY_NAME);
// Solution provided from Unity forums:
// https://answers.unity.com/questions/878667/world-space-canvas-on-top-of-everything.html
Graphic[] graphics = GetComponentsInChildren<Graphic>(true);
Dictionary<Material, Material> newMaterialMapping = new Dictionary<Material, Material>();
foreach (var graphic in graphics)
{
Material mat = graphic.materialForRendering;
if (mat == null)
{
continue;
}
if (!newMaterialMapping.TryGetValue(mat, out Material updatedMaterial))
{
updatedMaterial = new Material(mat);
newMaterialMapping.Add(mat, updatedMaterial);
}
updatedMaterial.SetInt(propertyId, (int)UnityEngine.Rendering.CompareFunction.Always);
graphic.material = updatedMaterial;
}
}
private void SetDisplayedPage(ClientSimDisplayedPage page)
{
_displayedPage = page;
pauseMenu.SetActive(_displayedPage == ClientSimDisplayedPage.PAUSE_MENU);
warningsPage.SetActive(_displayedPage == ClientSimDisplayedPage.WARNING_PAGE);
delayStartPage.SetActive(_displayedPage == ClientSimDisplayedPage.DELAYED_START_PAGE);
invalidSettingsPage.SetActive(_displayedPage == ClientSimDisplayedPage.INVALID_SETTINGS_PAGE);
}
private void DisplayInitialPageForPlayer()
{
SetDisplayedPage(_sessionState.GetBool(HAS_USER_ACCEPTED_WARNING)
? ClientSimDisplayedPage.PAUSE_MENU
: ClientSimDisplayedPage.WARNING_PAGE);
}
private void UpdateValuesFromSettings()
{
tooltipsToggle.isOn = _settings.showTooltips;
reticleToggle.isOn = _settings.showDesktopReticle;
invertMouseToggle.isOn = _settings.invertMouseLook;
consoleLoggingToggle.isOn = _settings.displayLogs;
float playerHeight = _heightManager.GetAvatarEyeHeightAsMetersClamped();
if (!Mathf.Approximately(playerHeight, playerHeightSlider.value))
{
// the player height slider obeys manual scale restrictions as if through the radial menu
ClampPlayerHeightSliderBounds();
playerHeightSlider.value = playerHeight;
playerHeightText.text = playerHeight.ToString("F2");
_eventDispatcher.SendEvent(new ClientSimOnPlayerHeightUpdateEvent { playerHeight = playerHeight });
}
}
private void ClampPlayerHeightSliderBounds()
{
playerHeightSlider.minValue = _heightManager.GetAvatarEyeHeightMinimumAsMeters();
playerHeightSlider.maxValue = _heightManager.GetAvatarEyeHeightMaximumAsMeters();
}
private void SaveSettings()
{
#if UNITY_EDITOR
ClientSimSettings.SaveSettings(_settings);
#endif
}
private void ToggleMenu(bool isActive)
{
// Don't allow the menu to be closed if the stacked camera system is not ready.
if(!isActive && !_stackedCameraReady)
{
return;
}
_menuIsActive = isActive;
menu.SetActive(isActive);
// toggle internal UI camera stack to improve menu performance
if (_menuCanvas.worldCamera != null)
_menuCanvas.worldCamera.enabled = isActive;
_eventDispatcher.SendEvent(new ClientSimMenuStateChangedEvent { isMenuOpen = _menuIsActive });
if (_menuIsActive)
{
_eventDispatcher.Subscribe<ClientSimOnPlayerMovedEvent>(OnPlayerMoved);
UpdateCanvasLocation();
// If the user sets the player height value too large through the Settings window,
// toggling the menu will clamp the max range to make it more usable again.
ClampPlayerHeightSliderBounds();
}
else
{
_eventDispatcher.Unsubscribe<ClientSimOnPlayerMovedEvent>(OnPlayerMoved);
}
}
// TODO update position based on tracking type. Desktop should always be in front of the camera and VR
// should be stationary relative to the playspace position.
private void UpdateCanvasLocation()
{
Camera cam = _menuCanvas.worldCamera;
// Always use main camera to position the menu if the camera is missing or not enabled.
if (cam == null || !cam.enabled || !cam.gameObject.activeInHierarchy)
{
cam = Camera.main;
}
// TODO handle missing camera better
if (cam == null)
{
return;
}
Transform camTransform = cam.transform;
menu.transform.localScale = camTransform.lossyScale * menuScaleFactor;
Vector3 position = camTransform.TransformPoint(Vector3.forward * 2);
menu.transform.SetPositionAndRotation(position, camTransform.rotation);
}
#region ClientSim Input
private void HandleInputMenuToggle(bool value, HandType hand)
{
// Only handle menu input down, and not on release.
if (!value)
{
return;
}
// Users can only change the enabled state when the current page is the normal pause menu.
if (_displayedPage != ClientSimDisplayedPage.PAUSE_MENU)
{
return;
}
// Player is not active, do not allow changing the state of the menu.
if (!_settings.spawnPlayer)
{
return;
}
#if ENABLE_INPUT_SYSTEM
// Ignore pressing escape to toggle the menu off. Due to Unity using the escape key as a special key to
// remove focus from the game window, it is impossible to recapture it.
if (_menuIsActive && (UnityEngine.InputSystem.Keyboard.current?.escapeKey.wasPressedThisFrame ?? false))
{
return;
}
#endif
ToggleMenu(!_menuIsActive);
}
#endregion
#region ClientSim Events
private void OnReady(ClientSimReadyEvent readyEvent)
{
// Disable the menu if the player is not spawned, and the displayed page isn't invalid.
if (!_settings.spawnPlayer && _displayedPage != ClientSimDisplayedPage.INVALID_SETTINGS_PAGE)
{
ToggleMenu(false);
return;
}
if (_displayedPage == ClientSimDisplayedPage.DELAYED_START_PAGE)
{
DisplayInitialPageForPlayer();
UpdateCanvasLocation();
}
}
private void OnStackedCameraReady(ClientSimStackedCameraReadyEvent readyEvent)
{
_stackedCameraReady = true;
_eventDispatcher.Unsubscribe<ClientSimStackedCameraReadyEvent>(OnStackedCameraReady);
if(_settings.hideMenuOnLaunch && _sessionState.GetBool(HAS_USER_ACCEPTED_WARNING))
ToggleMenu(false);
}
private void OnPlayerJoined(ClientSimOnPlayerJoinedEvent joinEvent)
{
VRCPlayerApi player = joinEvent.player;
if (!player.isLocal)
{
return;
}
isMasterToggle.isOn = player.isMaster;
playerNameText.text = player.displayName;
playerIdText.text = player.playerId.ToString();
}
private void OnMasterChange(ClientSimOnNewMasterEvent masterEvent)
{
isMasterToggle.isOn = Networking.IsMaster;
}
private void OnVRCPlusToggleChanged(bool value)
{
var localPlayer = Networking.LocalPlayer;
if (localPlayer != null)
{
localPlayer.GetClientSimPlayer().isVRCPlus = value;
}
}
private void OnPlayerMoved(ClientSimOnPlayerMovedEvent movedEvent)
{
UpdateCanvasLocation();
}
private void OnPlayerHeightUpdate(ClientSimOnPlayerHeightUpdateEvent heightEvent)
{
// if height was set programatically and exceeds a manual scaling limit, set slider to the exceeded limit
if (heightEvent.exceedsManualScalingMinimum)
playerHeightSlider.SetValueWithoutNotify(_heightManager.GetAvatarEyeHeightMinimumAsMeters());
else if (heightEvent.exceedsManualScalingMaximum)
playerHeightSlider.SetValueWithoutNotify(_heightManager.GetAvatarEyeHeightMaximumAsMeters());
else
playerHeightSlider.SetValueWithoutNotify(heightEvent.playerHeight);
playerHeightText.text = heightEvent.playerHeight.ToString("F2");
ClientSimSettings.Instance.SetInitialPlayerHeight(heightEvent.playerHeight);
}
private void OnManualScalingToggled(ClientSimOnToggleManualScalingEvent toggleEvent)
{
playerHeightSlider.interactable = toggleEvent.manualScalingAllowed;
}
#endregion
#region UI Hooks
[PublicAPI]
public void WarningAccepted()
{
CloseMenu();
SetDisplayedPage(ClientSimDisplayedPage.PAUSE_MENU);
_sessionState.SetBool(HAS_USER_ACCEPTED_WARNING, true);
}
[PublicAPI]
public void OpenMenu()
{
ToggleMenu(true);
}
[PublicAPI]
public void CloseMenu()
{
ToggleMenu(false);
}
[PublicAPI]
public void Respawn()
{
CloseMenu();
_eventDispatcher.SendEvent(new ClientSimMenuRespawnClickedEvent());
}
[PublicAPI]
public void OpenSettings()
{
openSettingsHook?.Invoke();
}
[PublicAPI]
public void ExitPlaymode()
{
#if UNITY_EDITOR
UnityEditor.EditorApplication.ExitPlaymode();
#endif
}
[PublicAPI]
public void SpawnRemotePlayer()
{
ClientSimMain.SpawnRemotePlayer();
}
[PublicAPI]
public void UpdatePlayerHeight(float playerHeight)
{
if (!_heightManager.GetManualAvatarScalingAllowed())
{
return;
}
if (Mathf.Approximately(_heightManager.GetAvatarEyeHeightAsMetersClamped(), playerHeight))
{
return;
}
_heightManager.SetAvatarEyeHeightByMeters(playerHeight, true);
SaveSettings();
}
[PublicAPI]
public void UpdateShowTooltips(bool showTooltips)
{
if (_settings.showTooltips == showTooltips)
{
return;
}
_settings.showTooltips = showTooltips;
SaveSettings();
}
[PublicAPI]
public void UpdateShowReticle(bool showReticle)
{
if (_settings.showDesktopReticle == showReticle)
{
return;
}
_settings.showDesktopReticle = showReticle;
SaveSettings();
}
[PublicAPI]
public void UpdateInvertMouseLook(bool invertMouseLook)
{
if (_settings.invertMouseLook == invertMouseLook)
{
return;
}
_settings.invertMouseLook = invertMouseLook;
SaveSettings();
}
[PublicAPI]
public void UpdateConsoleLogging(bool consoleLogging)
{
if (_settings.displayLogs == consoleLogging)
{
return;
}
_settings.displayLogs = consoleLogging;
SaveSettings();
}
#endregion
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: fcf28ff06eb64c8aa55abe665a48a198
timeCreated: 1638825107

View File

@ -0,0 +1,107 @@
using UnityEngine;
using VRC.SDKBase;
namespace VRC.SDK3.ClientSim
{
public class ClientSimPlayerHeightManager : IClientSimPlayerHeightManager
{
// used when scale is adjusted via Udon
private const float SYSTEM_EYE_HEIGHT_MIN = 0.1f;
private const float SYSTEM_EYE_HEIGHT_MAX = 100.0f;
// used when the user adjusts scale manually
private float userEyeHeightMin = 0.2f;
private float userEyeHeightMax = 5f;
// Default avatar height is 1.9 units tall
private float playerHeight = ClientSimSettings.Instance.playerStartHeight;
private float playerSpawnHeight = ClientSimSettings.Instance.playerStartHeight;
private bool manualScalingAllowed = true;
private IClientSimEventDispatcher eventDispatcher;
private IClientSimUdonEventSender udonEventSender;
public ClientSimPlayerHeightManager(IClientSimEventDispatcher eventDispatcher, IClientSimUdonEventSender udonEventSender)
{
this.eventDispatcher = eventDispatcher;
this.udonEventSender = udonEventSender;
eventDispatcher.Subscribe<ClientSimOnPlayerJoinedEvent>(OnPlayerJoined);
}
~ClientSimPlayerHeightManager()
{
Dispose();
}
public void Dispose()
{
eventDispatcher?.Unsubscribe<ClientSimOnPlayerJoinedEvent>(OnPlayerJoined);
eventDispatcher = null;
udonEventSender = null;
}
private void OnPlayerJoined(ClientSimOnPlayerJoinedEvent joinEvent)
{
udonEventSender.RunEvent("_onAvatarChanged", ("player", joinEvent.player));
udonEventSender.RunEvent("_onAvatarEyeHeightChanged",
("player", joinEvent.player),
("prevEyeHeightAsMeters", 0f));
}
public bool GetManualAvatarScalingAllowed() => manualScalingAllowed;
public void SetManualAvatarScalingAllowed(bool value)
{
manualScalingAllowed = value;
eventDispatcher?.SendEvent(new ClientSimOnToggleManualScalingEvent { manualScalingAllowed = value });
}
public float GetAvatarEyeHeightMinimumAsMeters() => userEyeHeightMin;
public void SetAvatarEyeHeightMinimumByMeters(float value)
{
// min can't go above max, and can't pass system thresholds
userEyeHeightMin = Mathf.Clamp(
Mathf.Min(value, userEyeHeightMax),
SYSTEM_EYE_HEIGHT_MIN,
SYSTEM_EYE_HEIGHT_MAX);
if (playerHeight < userEyeHeightMin)
{
SetAvatarEyeHeightByMeters(userEyeHeightMin);
}
}
public float GetAvatarEyeHeightMaximumAsMeters() => userEyeHeightMax;
public void SetAvatarEyeHeightMaximumByMeters(float value)
{
// max can't go below min, and can't pass system thresholds
userEyeHeightMax = Mathf.Clamp(
Mathf.Max(value, userEyeHeightMin),
SYSTEM_EYE_HEIGHT_MIN,
SYSTEM_EYE_HEIGHT_MAX);
if (playerHeight > userEyeHeightMax)
{
SetAvatarEyeHeightByMeters(userEyeHeightMax);
}
}
public float GetAvatarEyeHeightAsMeters() => playerHeight;
public float GetAvatarEyeHeightAsMetersClamped() => Mathf.Clamp(playerHeight, userEyeHeightMin, userEyeHeightMax);
public void SetAvatarEyeHeightByMultiplier(float multiplier) => SetAvatarEyeHeightByMeters(playerSpawnHeight * multiplier);
public void SetAvatarEyeHeightByMeters(float newHeight, bool isManual = false)
{
float previousHeight = playerHeight;
playerHeight = isManual
? Mathf.Clamp(newHeight, userEyeHeightMin, userEyeHeightMax)
: Mathf.Clamp(newHeight, SYSTEM_EYE_HEIGHT_MIN, SYSTEM_EYE_HEIGHT_MAX);
eventDispatcher?.SendEvent(new ClientSimOnPlayerHeightUpdateEvent
{
playerHeight = playerHeight,
exceedsManualScalingMinimum = !isManual && playerHeight < userEyeHeightMin,
exceedsManualScalingMaximum = !isManual && playerHeight > userEyeHeightMax
});
udonEventSender.RunEvent("_onAvatarEyeHeightChanged",
("player", Networking.LocalPlayer),
("prevEyeHeightAsMeters", previousHeight));
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9fa1dfce2cf44ccf84bee1e4679f0404
timeCreated: 1706291242

View File

@ -0,0 +1,787 @@
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
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8313fb7d292ed7c40a0f0e6e06a652df
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,93 @@
using UnityEngine;
using VRC.SDKBase;
namespace VRC.SDK3.ClientSim
{
/// <summary>
/// System that handles spawning new players for ClientSim.
/// New players will then be initialized through the PlayerManager.
/// </summary>
[AddComponentMenu("")]
public class ClientSimPlayerSpawner : ClientSimBehaviour
{
[SerializeField]
private GameObject localPlayerPrefab;
[SerializeField]
private GameObject remotePlayerPrefab;
private IClientSimSceneManager _sceneManager;
private IClientSimPlayerManager _playerManager;
private IClientSimBlacklistManager _blacklistManager;
private IClientSimEventDispatcher _eventDispatcher;
private Transform _parent;
public void Initialize(
IClientSimSceneManager sceneManager,
IClientSimPlayerManager playerManager,
IClientSimBlacklistManager blacklistManager,
IClientSimEventDispatcher eventDispatcher,
Transform parent)
{
_sceneManager = sceneManager;
_playerManager = playerManager;
_blacklistManager = blacklistManager;
_eventDispatcher = eventDispatcher;
_parent = parent;
}
public static Vector3 GetRandomPositionAroundSpawn(Vector3 spawnPosition, float radius)
{
Vector2 randomPosition = Random.insideUnitCircle * radius;
return new Vector3(randomPosition.x + spawnPosition.x, spawnPosition.y, randomPosition.y + spawnPosition.z);
}
public ClientSimPlayer SpawnPlayer(string playerName, bool isLocal)
{
if (!_sceneManager.HasSceneDescriptor())
{
throw new ClientSimException("Cannot spawn player if there is no world descriptor!");
}
GameObject playerPrefab = isLocal ? localPlayerPrefab : remotePlayerPrefab;
if (playerPrefab == null)
{
throw new ClientSimException("Failed to spawn player! Player prefab was not found.");
}
Transform spawn = _sceneManager.GetSpawnPoint(!isLocal);
float radius = _sceneManager.GetSpawnRadius();
Vector3 position = GetRandomPositionAroundSpawn(spawn.position, radius);
Quaternion rotation = Quaternion.Euler(0, spawn.rotation.eulerAngles.y, 0);
GameObject playerInstance = Instantiate(playerPrefab, position, rotation, _parent);
if (_parent == null)
{
DontDestroyOnLoad(playerInstance);
}
_blacklistManager.AddObjectAndChildrenToBlackList(playerInstance);
ClientSimPlayer player = playerInstance.GetComponent<ClientSimPlayer>();
if (player == null)
{
throw new ClientSimException("Failed to spawn player! ClientSimPlayer script was not found.");
}
// PlayerManager will automatically handle sending player join event
_playerManager.CreateNewPlayer(isLocal, player, playerName);
if (isLocal)
{
// Disable player controller until ClientSim is initialized, which is when the player should be able to gain control.
playerInstance.SetActive(false);
}
player.SetEventDispatcher(_eventDispatcher);
return player;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 79164cc4f26063f4db9016f803a7263d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,21 @@
using UnityEngine;
namespace VRC.SDK3.ClientSim
{
/// <summary>
/// Manager class for holding any proxy object. Currently only the camera proxy provided for the combat system.
/// </summary>
[AddComponentMenu("")]
public class ClientSimProxyObjects : ClientSimBehaviour, IClientSimProxyObjectProvider
{
[SerializeField]
private Transform cameraProxy;
public Transform CameraProxy() => cameraProxy;
public void DestroyProxy()
{
Destroy(gameObject);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a70a3c6d92e24dd489cdd7b4d138a17e
timeCreated: 1638830019

View File

@ -0,0 +1,190 @@
using UnityEngine;
using VRC.SDKBase;
using System.Reflection;
using UnityEngine.Rendering.PostProcessing;
namespace VRC.SDK3.ClientSim
{
/// <summary>
/// Wrapper for the VRC_SceneDescriptor.
/// </summary>
public class ClientSimSceneManager : IClientSimSceneManager
{
// Using a variable here to allow for testability when not on android platform.
private bool _isAndroid =
#if UNITY_ANDROID
true;
#else
false;
#endif
// Only used for tests to verify android path in copy camera.
internal static ClientSimSceneManager CreateTestInstance(bool isAndroid)
{
return new ClientSimSceneManager { _isAndroid = isAndroid };
}
private int _spawnOrder = 0;
private VRC_SceneDescriptor _descriptor => VRC_SceneDescriptor.Instance;
public bool HasSceneDescriptor()
{
return _descriptor != null;
}
public void SetupCamera(Camera camera)
{
if (!HasSceneDescriptor())
{
throw new ClientSimException("Cannot get reference camera when there is no scene descriptor.");
}
Camera refCamera = null;
if (_descriptor.ReferenceCamera != null)
{
refCamera = _descriptor.ReferenceCamera.GetComponent<Camera>();
}
if (refCamera != null)
{
CopyCameraValues(refCamera, camera);
}
else
{
this.LogWarning("Reference camera is not set in the SceneDescriptor!");
// Force camera near settings
camera.nearClipPlane = Mathf.Clamp(camera.nearClipPlane, 0.01f, 0.05f);
}
}
public float GetRespawnHeight()
{
if (!HasSceneDescriptor())
{
throw new ClientSimException("Cannot get respawn height when there is no scene descriptor.");
}
return _descriptor.RespawnHeightY;
}
public bool ShouldObjectsDestroyAtRespawnHeight()
{
if (!HasSceneDescriptor())
{
throw new ClientSimException("Cannot get respawn height setting when there is no scene descriptor.");
}
return _descriptor.ObjectBehaviourAtRespawnHeight == VRC_SceneDescriptor.RespawnHeightBehaviour.Destroy;
}
public void ResetSpawnOrder()
{
_spawnOrder = 0;
}
public Transform GetSpawnPoint(bool remote = false)
{
if (!HasSceneDescriptor())
{
throw new ClientSimException("Trying to get a Spawn Point but there is no Scene Descriptor. Add a SceneDescriptor or the VRCWorldPrefab to your scene.");
}
if (_descriptor.spawns.Length == 0 || _descriptor.spawns[0] == null)
{
throw new ClientSimException("Trying to get a Spawn Point but the Scene Descriptor doesn't have one. Add a Transform to the 'Spawns' array in the VRC Scene Descriptor component.");
}
// Remote players always restart the list, so for now, only first spawn
if (_descriptor.spawnOrder == VRC_SceneDescriptor.SpawnOrder.First ||
_descriptor.spawnOrder == VRC_SceneDescriptor.SpawnOrder.Demo ||
remote)
{
return _descriptor.spawns[0];
}
if (_descriptor.spawnOrder == VRC_SceneDescriptor.SpawnOrder.Random)
{
int spawn = Random.Range(0, _descriptor.spawns.Length);
return _descriptor.spawns[spawn];
}
if (_descriptor.spawnOrder == VRC_SceneDescriptor.SpawnOrder.Sequential)
{
Transform spawn = _descriptor.spawns[_spawnOrder];
_spawnOrder = (_spawnOrder + 1) % _descriptor.spawns.Length;
return spawn;
}
// Fallback to first spawn point
return _descriptor.spawns[0];
}
public Transform GetSpawnPoint(int index)
{
if (!HasSceneDescriptor())
{
throw new ClientSimException("Cannot get spawn point when there is no scene descriptor.");
}
if (index < 0 || index >= _descriptor.spawns.Length || _descriptor.spawns[index] == null)
{
this.LogWarning($"Using spawn point 0 instead of {index}, which is invalid.");
index = 0;
}
// Fallback to first spawn point
return _descriptor.spawns[index];
}
public float GetSpawnRadius()
{
if (!HasSceneDescriptor())
{
throw new ClientSimException("Cannot get spawn radius when there is no scene descriptor.");
}
return _descriptor.spawnRadius;
}
private void CopyCameraValues(Camera refCamera, Camera camera)
{
if (refCamera == null)
{
return;
}
camera.farClipPlane = refCamera.farClipPlane;
camera.nearClipPlane = Mathf.Clamp(refCamera.nearClipPlane, 0.01f, 0.05f);
camera.clearFlags = refCamera.clearFlags;
camera.backgroundColor = refCamera.backgroundColor;
camera.allowHDR = refCamera.allowHDR;
PostProcessLayer refPostProcessLayer = refCamera.GetComponent<PostProcessLayer>();
if (refPostProcessLayer != null)
{
if (_isAndroid)
{
Debug.LogWarning("Post processing is not supported on Android");
}
else
{
// VRChatBug: VRChat ignores if the PostProcessingLayer is disabled on the reference camera and always
// enables it for the player's camera.
PostProcessLayer postProcessLayer = camera.gameObject.AddComponent<PostProcessLayer>();
postProcessLayer.volumeLayer = refPostProcessLayer.volumeLayer;
postProcessLayer.volumeTrigger = refPostProcessLayer.volumeTrigger != refPostProcessLayer.transform
? refPostProcessLayer.volumeTrigger
: camera.transform;
// Use reflection to copy over resources : https://github.com/Unity-Technologies/PostProcessing/issues/467
FieldInfo resourcesInfo = typeof(PostProcessLayer).GetField("m_Resources", BindingFlags.NonPublic | BindingFlags.Instance);
PostProcessResources postProcessResources = resourcesInfo.GetValue(refPostProcessLayer) as PostProcessResources;
postProcessLayer.Init(postProcessResources);
}
}
refCamera.gameObject.SetActive(false);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1763d0603be7460583cf4e71634eea83
timeCreated: 1638892281

View File

@ -0,0 +1,28 @@
namespace VRC.SDK3.ClientSim
{
public class ClientSimSessionState : IClientSimSessionState
{
private const string SESSION_KEY_PREFIX = "com.vrchat.clientsim.session";
private string GetSessionKey(string key)
{
return $"{SESSION_KEY_PREFIX}.{key}";
}
public bool GetBool(string key)
{
#if UNITY_EDITOR
return UnityEditor.SessionState.GetBool(GetSessionKey(key), false);
#else
return false;
#endif
}
public void SetBool(string key, bool value)
{
#if UNITY_EDITOR
UnityEditor.SessionState.SetBool(GetSessionKey(key), value);
#endif
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 3f545fcbb2b04e7480e53fd98ae6fbed
timeCreated: 1643144734

View File

@ -0,0 +1,246 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using VRC.Udon.Common.Interfaces;
using VRC.Economy;
using VRC.Udon;
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine.SceneManagement;
using UnityEngine.UIElements;
using VRC.Economy.Editor;
#endif
namespace VRC.SDK3.ClientSim
{
public class ClientSimStoreManager : IDisposable
{
private IClientSimEventDispatcher _eventDispatcher;
private static IClientSimUdonEventSender _udonEventSender;
private static IClientSimPlayerManager _playerManager;
public static List<IProduct> WorldProducts;
public static Dictionary<string, IProduct> OwnedProducts;
public ClientSimStoreManager(IClientSimEventDispatcher eventDispatcher, IClientSimUdonEventSender udonEventSender, IClientSimPlayerManager playerManager)
{
_eventDispatcher = eventDispatcher;
_udonEventSender = udonEventSender;
_playerManager = playerManager;
Subscribe();
WorldProducts = new List<IProduct>();
OwnedProducts = new Dictionary<string, IProduct>();
#if UNITY_EDITOR
// Fetch UdonProducts scriptable objects to add to WorldProducts list
UdonProduct[] products = Resources.FindObjectsOfTypeAll<UdonProduct>();
if (products.Length != 0)
{
Dictionary<string, UdonProduct> productsDict = new Dictionary<string, UdonProduct>();
foreach (UdonProduct prod in products)
{
productsDict.Add(AssetDatabase.GetAssetPath(prod), prod);
}
var dependencies = AssetDatabase.GetDependencies(SceneManager.GetActiveScene().path);
foreach (string s in dependencies)
{
productsDict.TryGetValue(s, out UdonProduct prod);
if (prod != null) WorldProducts.Add(prod);
}
}
#endif
}
~ClientSimStoreManager()
{
Dispose();
}
private void Subscribe()
{
_eventDispatcher.Subscribe<ClientSimOnPlayerJoinedEvent>(OnPlayerJoined);
}
public void Dispose()
{
_eventDispatcher.Unsubscribe<ClientSimOnPlayerJoinedEvent>(OnPlayerJoined);
WorldProducts.Clear();
OwnedProducts.Clear();
}
private void OnPlayerJoined(ClientSimOnPlayerJoinedEvent joinEvent)
{
if (joinEvent.player.isLocal)
{
if (WorldProducts.Count == 0) return;
List<UdonBehaviour> udonBehaviours = UdonManager.Instance.GetUdonBehavioursInScene();
foreach (UdonProduct p in WorldProducts)
{
if (p.Purchased)
{
UdonProductStub stub = new UdonProductStub(p, _playerManager.LocalPlayer());
OwnedProducts.Add(p.ID, stub);
foreach (UdonBehaviour ub in udonBehaviours)
{
ub.RunEvent("_onPurchaseConfirmed", ("result", stub), ("player", _playerManager.LocalPlayer()), ("purchasedNow", false));
}
}
}
foreach (UdonBehaviour ub in UdonManager.Instance.GetUdonBehavioursInScene())
{
ub.RunEvent("_onPurchasesLoaded", ("result", OwnedProducts.Values.ToArray()), ("player", _playerManager.LocalPlayer()));
}
}
}
#if UNITY_EDITOR
// This section hooks into the UdonProductEditor.OnDrawProductGUI event
// to modify the custom ScriptableObject editor if the user is running ClientSim
[InitializeOnLoadMethod]
public static void ModifyUdonProductGUI()
{
UdonProductEditor.OnDrawProductGUI += OnDrawProductGUI;
UdonProductElement.OnCustomGUI += OnDrawProductElementGUI;
}
public static event EventHandler<UdonProduct> RefreshProductElementButton;
private const string stringExpireProduct = "Expire product";
private const string stringPurchaseProduct = "Purchase product";
private const string stringPurchaseTooltip = "This product (if used in any UdonBehaviours in the scene) will act as if it has already been bought prior to launching playmode, firing the OnPurchaseConfirmed & OnPurchasesLoaded events accordingly.";
private const string stringNotUsedInScene = "This product is not used in this scene and therefore cannot be bought or expired.";
private static bool IsProductUsedInScene(UdonProduct product)
{
return product != null && WorldProducts != null && WorldProducts.Contains(product);
}
private static void OnDrawProductElementGUI(object sender, VisualElement ve)
{
UdonProductElement element = (UdonProductElement)sender;
UdonProduct product = element.UdonProduct;
ve.style.display = DisplayStyle.Flex;
ve.SetEnabled(true);
EditorApplication.playModeStateChanged += change =>
{
RefreshProductElementButton?.Invoke(sender, product);
};
Button button = new Button();
button.text = product.Purchased ? stringExpireProduct : stringPurchaseProduct;
button.tooltip = product.Purchased ? stringPurchaseTooltip : string.Empty;
button.clicked += () =>
{
product.Purchased = !product.Purchased;
if (EditorApplication.isPlaying && IsProductUsedInScene(product))
{
if (product.Purchased) PurchaseConfirmed(product, true);
else PurchaseExpired(product);
}
RefreshProductElementButton?.Invoke(sender, product);
};
ve.Add(button);
RefreshProductElementButton += (o, args) =>
{
if (args != product) return;
button.text = product.Purchased ? stringExpireProduct : stringPurchaseProduct;
if(product.Purchased) UdonProductsManagerWindow.ShowInfoBox(UdonProductsManagerWindow.InfoBoxStatus.Info, stringPurchaseTooltip);
button.tooltip = product.Purchased ? stringPurchaseTooltip : string.Empty;
ve.SetEnabled(!EditorApplication.isPlaying || IsProductUsedInScene(product));
};
}
private static void OnDrawProductGUI(object sender, UdonProduct e)
{
bool isUsedInScene = e != null && WorldProducts != null && EditorApplication.isPlaying && WorldProducts.Contains(e);
GUILayout.Label("ClientSim functions", EditorStyles.boldLabel);
using (new EditorGUI.DisabledGroupScope(EditorApplication.isPlaying && !isUsedInScene))
{
if (GUILayout.Button(e.Purchased ? stringExpireProduct : stringPurchaseProduct))
{
e.Purchased = !e.Purchased;
if (EditorApplication.isPlaying && isUsedInScene)
{
if (e.Purchased) PurchaseConfirmed(e, true);
else PurchaseExpired(e);
}
}
if (!EditorApplication.isPlaying && e.Purchased)
EditorGUILayout.HelpBox(stringPurchaseTooltip, MessageType.Info);
if (EditorApplication.isPlaying && !isUsedInScene) EditorGUILayout.HelpBox(stringNotUsedInScene, MessageType.Warning);
}
}
#endif
public static void PurchaseConfirmed(IProduct product, bool purchasedNow)
{
UdonProductStub stub = new UdonProductStub(product, _playerManager.LocalPlayer());
OwnedProducts.Add(product.ID, stub);
foreach (UdonBehaviour ub in UdonManager.Instance.GetUdonBehavioursInScene())
{
ub.RunEvent("_onPurchaseConfirmed", ("result", stub), ("player", _playerManager.LocalPlayer()), ("purchasedNow", purchasedNow));
}
}
public static void PurchaseExpired(IProduct product)
{
IProduct ownedProduct = OwnedProducts[product.ID];
OwnedProducts.Remove(product.ID);
_udonEventSender.RunEvent("_onPurchaseExpired", ("result", ownedProduct), ("player", _playerManager.LocalPlayer()));
}
public static void SendProductEvent(IUdonEventReceiver behaviour, IProduct product)
{
if (OwnedProducts.ContainsKey(product.ID))
{
behaviour.RunEvent("_onProductEvent", ("result", OwnedProducts[product.ID]), ("player", OwnedProducts[product.ID].Buyer));
}
}
public static void ListPurchases(IUdonEventReceiver behaviour, VRC.SDKBase.VRCPlayerApi player)
{
behaviour.RunEvent("_onListPurchases", ("result", OwnedProducts.Values.ToArray()), ("player", _playerManager.LocalPlayer()));
}
public static void ListAvailableProducts(IUdonEventReceiver behaviour)
{
behaviour.RunEvent("_onListAvailableProducts", ("result", WorldProducts.ToArray()));
}
public static bool DoesPlayerOwnProduct(VRC.SDKBase.VRCPlayerApi player, IProduct product)
{
return OwnedProducts.ContainsKey(product.ID);
}
public static bool DoesAnyPlayerOwnProduct(IProduct product)
{
return OwnedProducts.ContainsKey(product.ID);
}
public static VRC.SDKBase.VRCPlayerApi[] GetPlayersWhoOwnProduct(IProduct product)
{
VRC.SDKBase.VRCPlayerApi[] playerApis;
if (OwnedProducts.ContainsKey(product.ID))
{
playerApis = new VRC.SDKBase.VRCPlayerApi[1] { _playerManager.LocalPlayer() };
}
else
{
playerApis = Array.Empty<VRC.SDKBase.VRCPlayerApi>();
}
return playerApis;
}
public static void ListProductOwners(IUdonEventReceiver behaviour, IProduct product)
{
behaviour.RunEvent("_onListProductOwners", ("result", product), ("owners", new string[] {"VRCat", "Fred", "VRRat"}));
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2d076b4f4b164393ac22400d6bd84118
timeCreated: 1692235633

View File

@ -0,0 +1,213 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using VRC.SDK3.Components;
using VRC.SDKBase;
namespace VRC.SDK3.ClientSim
{
/// <summary>
/// System that keeps track of all synced objects. Handles changing ownership when players leave
/// and also checks for if position synced objects fall below the respawn height.
/// </summary>
/// <remarks>
/// Listens to Events:
/// - ClientSimOnPlayerLeftEvent
/// </remarks>
[AddComponentMenu("")]
public class ClientSimSyncedObjectManager : ClientSimBehaviour, IClientSimSyncedObjectManager, IDisposable
{
// Collection of all synced objects initialized in the scene.
private readonly ClientSimObjectCollection<IClientSimSyncable> _syncedObjects =
new ClientSimObjectCollection<IClientSimSyncable>();
// Collection of all position synced objects initialized in the scene.
private readonly ClientSimObjectCollection<IClientSimPositionSyncable> _positionSyncedObjects =
new ClientSimObjectCollection<IClientSimPositionSyncable>();
// Collection of all synced playerObjects initialized in the scene.
private readonly ClientSimObjectCollection<IClientSimSyncable> _playerObjects =
new ClientSimObjectCollection<IClientSimSyncable>();
// TODO add generic system for saving synced data and restoring it to simulate late joining into a world.
// TODO add system to manage udon synced data
private IClientSimEventDispatcher _eventDispatcher;
private IClientSimSceneManager _sceneManager;
private IClientSimPlayerManager _playerManager;
public void Initialize(
IClientSimEventDispatcher eventDispatcher,
IClientSimSceneManager sceneManager,
IClientSimPlayerManager playerManager)
{
_eventDispatcher = eventDispatcher;
_sceneManager = sceneManager;
_playerManager = playerManager;
_eventDispatcher.Subscribe<ClientSimOnPlayerLeftEvent>(OnPlayerLeft);
}
private void OnDestroy()
{
Dispose();
}
public void Dispose()
{
_eventDispatcher?.Unsubscribe<ClientSimOnPlayerLeftEvent>(OnPlayerLeft);
}
public void InitializeObjectSync(VRCObjectSync sync)
{
// Only allow one object sync helper per object.
if (sync.TryGetComponent(out ClientSimObjectSyncHelper syncHelper))
{
syncHelper.Initialize(sync,this);
}
else
{
ClientSimObjectSyncHelper helper = sync.gameObject.AddComponent<ClientSimObjectSyncHelper>();
helper.Initialize(sync,this);
}
}
public void InitializeObjectPool(VRCObjectPool objectPool)
{
objectPool.gameObject.AddComponent<ClientSimObjectPoolHelper>().Initialize(objectPool, this);
}
private void LateUpdate()
{
ProcessPositionSyncedObjects();
}
private void ProcessPositionSyncedObjects()
{
_positionSyncedObjects.ProcessAddedAndRemovedObjects();
// TODO space this out so that there are only x number per frame instead of all every time
List<GameObject> objsToDestroy = new List<GameObject>();
foreach (IClientSimPositionSyncable sync in _positionSyncedObjects.GetObjects())
{
if (sync == null)
{
_positionSyncedObjects.ShouldVerifyObjects();
continue;
}
if (!sync.SyncPosition)
{
continue;
}
// VRChatBug: The following method will enforce users to use VRCObjectSync's methods for setting
// useGravity and isKinematic, but the current SDK does not support the hook to allow this.
// Verify Sync properties, eg check if useGravity and isKinematic is properly set.
// sync.UpdatePositionSync();
// Verify if the object is below respawn.
Transform syncTransform = sync.GetTransform();
if (syncTransform.position.y < _sceneManager.GetRespawnHeight())
{
if (_sceneManager.ShouldObjectsDestroyAtRespawnHeight())
{
objsToDestroy.Add(syncTransform.gameObject);
}
else
{
sync.Respawn();
}
}
}
foreach (var obj in objsToDestroy)
{
Destroy(obj);
}
}
#region ClientSim Events
// Handle updating object ownership for all objects the leaving player previously owned.
private void OnPlayerLeft(ClientSimOnPlayerLeftEvent leftEvent)
{
VRCPlayerApi leftPlayer = leftEvent.player;
int leftPlayerId = leftPlayer.playerId;
VRCPlayerApi masterPlayer = _playerManager.GetMaster();
if (masterPlayer == null)
{
return;
}
_syncedObjects.ProcessAddedAndRemovedObjects();
foreach (IClientSimSyncable sync in _syncedObjects.GetObjects())
{
if (sync == null)
{
continue;
}
if (sync is Component syncComp)
{
if(syncComp == null) continue;
GameObject syncObj = syncComp.gameObject;
if (Networking.GetOwner(syncObj)?.playerId == leftPlayerId)
{
Networking.SetOwner(masterPlayer, syncObj);
}
}
else
{
if (sync.GetOwner() == leftPlayerId)
{
sync.SetOwner(masterPlayer.playerId);
}
}
}
}
#endregion
#region IClientSimSyncedObjectManager
public void AddSyncedObject(IClientSimSyncable sync)
{
_syncedObjects.AddObject(sync);
_syncedObjects.ProcessAddedAndRemovedObjects();
if (sync is IClientSimPositionSyncable posSync && posSync.SyncPosition)
{
_positionSyncedObjects.AddObject(posSync);
_positionSyncedObjects.ProcessAddedAndRemovedObjects();
}
if((sync as MonoBehaviour).GetComponentInParent<VRCPlayerObject>() != null)
{
_playerObjects.AddObject(sync);
_playerObjects.ProcessAddedAndRemovedObjects();
}
}
public void RemoveSyncedObject(IClientSimSyncable sync)
{
_syncedObjects.RemoveObject(sync);
_syncedObjects.ProcessAddedAndRemovedObjects();
if (sync is IClientSimPositionSyncable posSync && posSync.SyncPosition)
{
_positionSyncedObjects.RemoveObject(posSync);
_positionSyncedObjects.ProcessAddedAndRemovedObjects();
}
if((sync as MonoBehaviour).GetComponentInParent<VRCPlayerObject>() != null)
{
_playerObjects.RemoveObject(sync);
_playerObjects.ProcessAddedAndRemovedObjects();
}
}
#endregion
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f0bd9a5f625c4c04bd9862b62b57f431
timeCreated: 1638897183

View File

@ -0,0 +1,84 @@
using UnityEngine;
namespace VRC.SDK3.ClientSim
{
[AddComponentMenu("")]
public class ClientSimTooltip : ClientSimBehaviour
{
[SerializeField]
private TextMesh tooltipText;
public IClientSimInteractable Interactable { get; private set; }
// Both Udon Interacts and Pickups display tooltips at the same location.
// Moved here as a helper method to not repeat code.
// Tooltip is displayed at the top bound of the object's first collider.
public static Vector3 GetToolTipPosition(GameObject obj)
{
// VRChatBug: Tooltips always ignore the tooltipPlacement transform and instead place the tooltip at the top
// of the first collider on the object.
Collider interactCollider = obj.GetComponent<Collider>();
if (interactCollider == null)
{
return obj.transform.position;
}
// Note due to colliders not updating their bounds property until after a physics update, to prevent extra
// calls to Physics.SyncTransforms, the following code approximates the updated center and top extends
// position based on the type of collider.
Transform transform = obj.transform;
Vector3 position = transform.position;
Quaternion rotation = transform.rotation;
Vector3 scale = transform.lossyScale;
Vector3 colliderCenter = Vector3.zero;
if (interactCollider is BoxCollider boxCollider)
{
colliderCenter = boxCollider.center;
}
else if (interactCollider is SphereCollider sphereCollider)
{
colliderCenter = sphereCollider.center;
}
else if (interactCollider is CapsuleCollider capsuleCollider)
{
colliderCenter = capsuleCollider.center;
}
else if (interactCollider is MeshCollider meshCollider)
{
colliderCenter = meshCollider.sharedMesh.bounds.center;
}
position += rotation * Vector3.Scale(colliderCenter, scale);
return position + new Vector3(0, interactCollider.bounds.extents.y, 0);
}
public void UpdateTooltip(Vector3 playerPos, Vector3 up)
{
Vector3 position = Interactable.GetInteractTextPlacement();
// Rotate to look towards the player while keeping the proper up direction.
// VRChatBug: Build 1160 has this broken again so that rotating the player through stations does not properly rotate tooltips.
Quaternion rotation = Quaternion.LookRotation(Vector3.ProjectOnPlane(playerPos - position, up), up);
transform.SetPositionAndRotation(position, rotation);
}
public void EnableTooltip(IClientSimInteractable interactable)
{
gameObject.SetActive(true);
Interactable = interactable;
tooltipText.text = Interactable.GetInteractText();
}
public void DisableTooltip()
{
gameObject.SetActive(false);
Interactable = null;
tooltipText.text = "";
transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 99cce027a08452740befb63a4966f0f9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,105 @@
using System.Collections.Generic;
using UnityEngine;
using VRC.SDKBase;
namespace VRC.SDK3.ClientSim
{
/// <summary>
/// Display a tooltip above an interactable.
/// </summary>
[AddComponentMenu("")]
[DefaultExecutionOrder(30001)] // Ensure that all tooltip positions are updated at the end of frame.
public class ClientSimTooltipManager : ClientSimBehaviour, IClientSimTooltipManager
{
[SerializeField]
private GameObject tooltipPrefab;
private IClientSimTrackingProvider _trackingProvider;
private ClientSimSettings _settings;
private readonly Queue<ClientSimTooltip> _unusedTooltips = new Queue<ClientSimTooltip>();
private readonly List<ClientSimTooltip> _displayedTooltips = new List<ClientSimTooltip>();
public void Initialize(ClientSimSettings settings, IClientSimTrackingProvider trackingProvider)
{
_trackingProvider = trackingProvider;
_settings = settings;
}
// TODO expand this to allow passing in any object to display tooltips
// (Allow for pickup use text without changing the interactable since the other hand still needs the original text)
public void DisplayTooltip(IClientSimInteractable interact)
{
if (!_settings.showTooltips)
{
return;
}
ClientSimTooltip tooltip = GetUnusedTooltip();
tooltip.EnableTooltip(interact);
_displayedTooltips.Add(tooltip);
}
public void DisableTooltip(IClientSimInteractable interact)
{
// Loop through list backwards to ensure removing doesn't change the order.
for (int cur = _displayedTooltips.Count - 1; cur >= 0; --cur)
{
ClientSimTooltip tooltip = _displayedTooltips[cur];
if (tooltip.Interactable == interact)
{
_displayedTooltips.RemoveAt(cur);
DisableTooltip(tooltip);
}
}
}
private ClientSimTooltip GetUnusedTooltip()
{
if (_unusedTooltips.Count == 0)
{
GameObject tooltipObj = Instantiate(tooltipPrefab, transform);
return tooltipObj.GetComponent<ClientSimTooltip>();
}
return _unusedTooltips.Dequeue();
}
private void DisableTooltip(ClientSimTooltip tooltip)
{
tooltip.DisableTooltip();
_unusedTooltips.Enqueue(tooltip);
}
private void LateUpdate()
{
if (_trackingProvider == null)
{
return;
}
Transform playerHead = _trackingProvider.GetTrackingTransform(VRCPlayerApi.TrackingDataType.Head);
Vector3 playerPos = playerHead.position;
Vector3 playerUp = playerHead.up;
// Use playspace up to prevent billboard effect in vr.
if (_trackingProvider.IsVR())
{
playerUp = _trackingProvider.GetTrackingTransform(VRCPlayerApi.TrackingDataType.Origin).up;
}
for (int cur = _displayedTooltips.Count - 1; cur >= 0; --cur)
{
ClientSimTooltip tooltip = _displayedTooltips[cur];
// Remove tooltips with destroyed interactables.
if (tooltip.Interactable == null)
{
_displayedTooltips.RemoveAt(cur);
DisableTooltip(tooltip);
continue;
}
tooltip.UpdateTooltip(playerPos, playerUp);
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a3368d7496423784180eac62c9b35e56
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,163 @@

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;
using VRC.Udon.Common.Interfaces;
namespace VRC.SDK3.ClientSim
{
/// <summary>
/// System to hold all the current initialized Udon in the scene
/// </summary>
/// <remarks>
/// Listens to Events:
/// - ClientSimOnPlayerJoinedEvent
/// - ClientSimOnPlayerLeftEvent
/// - ClientSimOnPlayerRespawnEvent
/// </remarks>
public class ClientSimUdonManager : IClientSimUdonManager, IDisposable
{
private IClientSimEventDispatcher _eventDispatcher;
private IClientSimSyncedObjectManager _syncedObjectManager;
private IClientSimUdonEventSender _udonEventSender;
private readonly ClientSimObjectCollection<UdonBehaviour> _udonBehaviours =
new ClientSimObjectCollection<UdonBehaviour>();
private bool _isReady = false;
public ClientSimUdonManager(
IClientSimEventDispatcher eventDispatcher,
IClientSimSyncedObjectManager syncedObjectManager,
IClientSimUdonEventSender udonEventSender)
{
_eventDispatcher = eventDispatcher;
_syncedObjectManager = syncedObjectManager;
_udonEventSender = udonEventSender;
Subscribe();
}
~ClientSimUdonManager()
{
Dispose();
}
private void Subscribe()
{
_eventDispatcher.Subscribe<ClientSimOnPlayerJoinedEvent>(OnPlayerJoined);
_eventDispatcher.Subscribe<ClientSimOnPlayerLeftEvent>(OnPlayerLeft);
_eventDispatcher.Subscribe<ClientSimOnPlayerRespawnEvent>(OnPlayerRespawn);
_eventDispatcher.Subscribe<ClientSimScreenUpdateEvent>(OnScreenUpdate);
_eventDispatcher.Subscribe<ClientSimOnVRCPlusMassGift>(OnVRCPlusMassGift);
}
public void Dispose()
{
_eventDispatcher.Unsubscribe<ClientSimOnPlayerJoinedEvent>(OnPlayerJoined);
_eventDispatcher.Unsubscribe<ClientSimOnPlayerLeftEvent>(OnPlayerLeft);
_eventDispatcher.Unsubscribe<ClientSimOnPlayerRespawnEvent>(OnPlayerRespawn);
_eventDispatcher.Unsubscribe<ClientSimScreenUpdateEvent>(OnScreenUpdate);
_eventDispatcher.Unsubscribe<ClientSimOnVRCPlusMassGift>(OnVRCPlusMassGift);
}
public void InitUdon(UdonBehaviour behaviour, IUdonProgram program)
{
ClientSimUdonHelper[] helpers = behaviour.gameObject.GetComponents<ClientSimUdonHelper>();
foreach (ClientSimUdonHelper helper in helpers)
{
if(helper.GetUdonBehaviour() == behaviour)
{
return;
}
}
ClientSimUdonHelper helperAdded = behaviour.gameObject.AddComponent<ClientSimUdonHelper>();
helperAdded.Initialize(behaviour, this, _syncedObjectManager, _isReady);
}
public IEnumerator OnClientSimReady()
{
_isReady = true;
_udonBehaviours.ProcessAddedAndRemovedObjects();
HashSet<GameObject> objs = new HashSet<GameObject>();
foreach (var udonBehavior in _udonBehaviours.GetObjects())
{
if (udonBehavior == null || objs.Contains(udonBehavior.gameObject))
{
continue;
}
objs.Add(udonBehavior.gameObject);
foreach (var helper in udonBehavior.GetComponents<ClientSimUdonHelper>())
{
try
{
helper.OnReady();
}
catch (Exception e)
{
this.LogError($"{e.Message}\n{e.StackTrace}");
this.LogWarning($"Failed to set ready for object: {Tools.GetGameObjectPath(helper.gameObject)}");
}
}
}
// Wait one frame for all active UdonBehaviours to properly run start.
yield return null;
}
#region ClientSimEvent handling
private void OnPlayerJoined(ClientSimOnPlayerJoinedEvent joinEvent)
{
_udonEventSender.RunEvent(UdonManager.UDON_EVENT_ONINPUTMETHODCHANGED, ("inputMethod", VRCInputMethod.Keyboard));
_udonEventSender.RunEvent(UdonManager.UDON_EVENT_ONLANGUAGECHANGED, ("language", ClientSimSettings.Instance.currentLanguage));
_udonEventSender.RunEvent("_onPlayerJoined", ("player", joinEvent.player));
}
private void OnPlayerLeft(ClientSimOnPlayerLeftEvent leftEvent)
{
_udonEventSender.RunEvent("_onPlayerLeft", ("player", leftEvent.player));
}
private void OnPlayerRespawn(ClientSimOnPlayerRespawnEvent respawnEvent)
{
_udonEventSender.RunEvent("_onPlayerRespawn", ("player", respawnEvent.player));
}
private void OnScreenUpdate(ClientSimScreenUpdateEvent screenUpdateEvent)
{
_udonEventSender.RunEvent(UdonManager.UDON_EVENT_ONSCREENUPDATE, ("data", screenUpdateEvent.data));
}
private void OnVRCPlusMassGift(ClientSimOnVRCPlusMassGift giftEvent)
{
_udonEventSender.RunEvent(UdonManager.UDON_EVENT_ONVRCPLUSMASSGIFT,
("gifter", giftEvent.gifter),
("numGifts", giftEvent.numGifts));
}
#endregion
#region IClientSimUdonManager
public void AddUdonBehaviour(UdonBehaviour udonBehaviour)
{
_udonBehaviours.AddObject(udonBehaviour);
}
public void RemoveUdonBehaviour(UdonBehaviour udonBehaviour)
{
_udonBehaviours.RemoveObject(udonBehaviour);
}
#endregion
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 340d1d7e8555a8643b0ab107b14e2a78
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,19 @@
using VRC.Udon;
namespace VRC.SDK3.ClientSim
{
public class ClientSimUdonManagerEventSender : IClientSimUdonEventSender
{
private readonly UdonManager _udonManager;
public ClientSimUdonManagerEventSender(UdonManager udonManager)
{
_udonManager = udonManager;
}
public void RunEvent(string eventName, params (string, object)[] programVariables)
{
_udonManager.RunEvent(eventName, programVariables);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 39cba4c4dd6f4c9686a240bd330a777c
timeCreated: 1640270545

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2e3fb64cb2751c84fafc391a3fd5c06f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,9 @@
using UnityEngine;
namespace VRC.SDK3.ClientSim
{
public interface IClientSimBlacklistManager
{
void AddObjectAndChildrenToBlackList(GameObject obj);
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d7e78ffeb0b3def46a3a33bcb52b1ccc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,10 @@
using UnityEngine;
namespace VRC.SDK3.ClientSim
{
public interface IClientSimHighlightManager
{
void EnableObjectHighlight(GameObject obj);
void DisableObjectHighlight(GameObject obj);
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0f56beeb09982c24c9b7164de3faccc4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,9 @@
using UnityEngine;
namespace VRC.SDK3.ClientSim
{
public interface IClientSimInteractiveLayerProvider
{
LayerMask GetInteractiveLayers();
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c004ca50a1e6fdf478a55c1a4e619f98
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,10 @@

using UnityEngine;
namespace VRC.SDK3.ClientSim
{
public interface IClientSimMousePositionProvider
{
Vector2 GetMousePosition();
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 61b8e9d915d2b1245875c146dfa8ab96
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,18 @@
using System;
namespace VRC.SDK3.ClientSim
{
public interface IClientSimPlayerHeightManager : IDisposable
{
bool GetManualAvatarScalingAllowed();
float GetAvatarEyeHeightMinimumAsMeters();
float GetAvatarEyeHeightMaximumAsMeters();
float GetAvatarEyeHeightAsMeters();
float GetAvatarEyeHeightAsMetersClamped();
void SetManualAvatarScalingAllowed(bool value);
void SetAvatarEyeHeightMinimumByMeters(float value);
void SetAvatarEyeHeightMaximumByMeters(float value);
void SetAvatarEyeHeightByMeters(float newHeight, bool isManual = false);
void SetAvatarEyeHeightByMultiplier(float multiplier);
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ff98d79f6c0f40e2a46e3102881d394b
timeCreated: 1706291260

View File

@ -0,0 +1,25 @@

using UnityEngine;
using VRC.SDK3.Components;
using VRC.SDKBase;
namespace VRC.SDK3.ClientSim
{
public interface IClientSimPlayerManager
{
VRCPlayerApi CreateNewPlayer(bool local, ClientSimPlayer player, string name = null);
void RemovePlayer(VRCPlayerApi player);
int GetMasterID();
VRCPlayerApi GetMaster();
VRCPlayerApi LocalPlayer();
VRCPlayerApi GetPlayerByID(int playerID);
int GetPlayerID(VRCPlayerApi player);
bool IsMaster(VRCPlayerApi player);
bool IsInstanceOwner(VRCPlayerApi player);
bool IsInstanceOwner();
bool IsLocalPlayerMaster();
VRCPlayerApi GetOwner(GameObject obj);
bool IsOwner(VRCPlayerApi player, GameObject obj);
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c93456661823b2a4dbab7894ba0c027e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,9 @@
using UnityEngine;
namespace VRC.SDK3.ClientSim
{
public interface IClientSimProxyObjectProvider
{
Transform CameraProxy();
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5c782b965c37d2f47a0f17914838fc16
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,15 @@
using UnityEngine;
namespace VRC.SDK3.ClientSim
{
public interface IClientSimSceneManager
{
bool HasSceneDescriptor();
Transform GetSpawnPoint(bool remote);
Transform GetSpawnPoint(int index);
float GetSpawnRadius();
void SetupCamera(Camera camera);
float GetRespawnHeight();
bool ShouldObjectsDestroyAtRespawnHeight();
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7243937d85f17c34ebb98ba8023553a2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
namespace VRC.SDK3.ClientSim
{
public interface IClientSimSessionState
{
bool GetBool(string key);
void SetBool(string key, bool value);
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6b70f8e88d954ae397f2fd3f09c9ac49
timeCreated: 1643145513

View File

@ -0,0 +1,13 @@
using VRC.SDK3.Components;
namespace VRC.SDK3.ClientSim
{
public interface IClientSimSyncedObjectManager
{
void AddSyncedObject(IClientSimSyncable sync);
void RemoveSyncedObject(IClientSimSyncable sync);
void InitializeObjectSync(VRCObjectSync sync);
void InitializeObjectPool(VRCObjectPool objectPool);
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 46580ba70cfd92e479b924bd1a469c07
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,9 @@

namespace VRC.SDK3.ClientSim
{
public interface IClientSimTooltipManager
{
void DisplayTooltip(IClientSimInteractable interact);
void DisableTooltip(IClientSimInteractable interact);
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b94475b7722141843ba4e3e8795ab65f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,11 @@
namespace VRC.SDK3.ClientSim
{
/// <summary>
/// Wrapper for sending events to all udon programs.
/// Helps in tests without directly referencing UdonManager.
/// </summary>
public interface IClientSimUdonEventSender
{
void RunEvent(string eventName, params (string, object)[] programVariables);
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7dc60a559e4c412db2226c17f35e1a66
timeCreated: 1640270507

View File

@ -0,0 +1,10 @@
using VRC.Udon;
namespace VRC.SDK3.ClientSim
{
public interface IClientSimUdonManager
{
void AddUdonBehaviour(UdonBehaviour udon);
void RemoveUdonBehaviour(UdonBehaviour udon);
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 60d81ad6ae86c9d4db21f724007b16cd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: