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,495 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using VRC.SDK3.Components;
using VRC.SDKBase;
using VRC.Udon;
using VRC.Utility;
using Object = UnityEngine.Object;
using VRCStation = VRC.SDK3.Components.VRCStation;
#if VRC_ENABLE_PLAYER_PERSISTENCE
using VRC.SDK3.ClientSim.Persistence;
using VRC.SDKBase.Network;
#endif
namespace VRC.SDK3.ClientSim
{
/// <summary>
/// ClientSimPlayer is the container class for all the player related systems.
/// </summary>
/// <remarks>
/// When the user clicks on the player in the inspector, this class also allows you to edit settings,
/// such as locomotion and audio values.
/// </remarks>
// TODO split into local and remote versions
[AddComponentMenu("")]
[SelectionBase]
public class ClientSimPlayer : ClientSimBehaviour, IClientSimPlayerApiProvider
{
[SerializeField]
private ClientSimPlayerController playerController;
[SerializeField]
private ClientSimPlayerStationManager stationManager;
[SerializeField]
private ClientSimPlayerRaycaster playerRaycaster;
[SerializeField]
private ClientSimTrackingProviderBase playerTrackingData;
[SerializeField]
private ClientSimPlayerAvatarManager playerAvatar;
[SerializeField]
private ClientSimReticle reticle;
private ClientSimCombatSystemHelper _combatSystemHelper;
private IClientSimEventDispatcher _eventDispatcher;
private IClientSimPlayerManager _playerManager;
private ClientSimInteractManager _interactManager;
private IClientSimSceneManager _sceneManager;
private IClientSimProxyObjectProvider _proxyProvider;
private IClientSimUdonEventSender _udonEventSender;
private ClientSimSettings _settings;
public VRCPlayerApi Player { get; private set; }
public bool IsUserVR { get; private set; }
public bool isInstanceOwner;
public bool isSuspended;
public bool isVRCPlus;
// Public to allow users to edit values in editor.
public ClientSimPlayerLocomotionData locomotionData = new();
public ClientSimPlayerPickupData pickupData = new();
public ClientSimPlayerAudioData audioData = new();
public ClientSimPlayerTagsData tagData = new();
#if VRC_ENABLE_PLAYER_PERSISTENCE
public GameObject[] PlayerPersistenceObjects = Array.Empty<GameObject>();
public GameObject[] PlayerPersistenceRootObjects = Array.Empty<GameObject>();
public ClientSimPlayerDataStorage PlayerDataPrefab;
internal ClientSimPlayerDataStorage PlayerDataObject;
internal ClientSimPlayerObjectStorage PlayerObjectData;
private ClientSimPlayerRestoredStatus playerRestoredStatus = new();
#endif
public void SetPlayer(VRCPlayerApi player)
{
Player = player;
}
public void Initialize(
IClientSimEventDispatcher eventDispatcher,
IClientSimInput input,
ClientSimSettings settings,
IClientSimHighlightManager highlightManager,
IClientSimTooltipManager tooltipManager,
IClientSimInteractiveLayerProvider interactiveLayerProvider,
IClientSimMousePositionProvider mousePositionProvider,
IClientSimSceneManager sceneManager,
IClientSimProxyObjectProvider proxyProvider,
IClientSimUdonEventSender udonEventSender,
IClientSimBlacklistManager blacklistManager,
IClientSimUdonManager udonManager,
IClientSimSyncedObjectManager syncedObjectManager,
IClientSimPlayerManager playerManager,
IClientSimPlayerHeightManager heightManager)
{
_eventDispatcher = eventDispatcher;
_settings = settings;
_sceneManager = sceneManager;
_proxyProvider = proxyProvider;
_playerManager = playerManager;
_udonEventSender = udonEventSender;
// TODO take settings and spawn desktop vs vr tracking data
playerTrackingData.Initialize(eventDispatcher, input, settings, heightManager);
IsUserVR = playerTrackingData.IsVR();
_interactManager = new ClientSimInteractManager(playerTrackingData, pickupData);
playerRaycaster.Initialize(
eventDispatcher,
input,
this,
pickupData,
highlightManager,
tooltipManager,
interactiveLayerProvider,
playerTrackingData,
mousePositionProvider,
_interactManager,
playerTrackingData,
stationManager);
stationManager.Initialize(eventDispatcher, this);
playerController.Initialize(
eventDispatcher,
input,
this,
locomotionData,
sceneManager,
proxyProvider,
playerTrackingData,
stationManager);
playerAvatar.Initialize(eventDispatcher);
if (settings.spawnPlayer)
{
if (!IsUserVR)
{
reticle.Initialize(_eventDispatcher, _settings, mousePositionProvider);
}
// TODO initialize VR raycast visualizers
}
}
public void SetEventDispatcher(IClientSimEventDispatcher eventDispatcher)
{
_eventDispatcher = eventDispatcher;
}
public void InitializeCombat()
{
if (_combatSystemHelper != null)
{
return;
}
_combatSystemHelper = gameObject.AddComponent<ClientSimCombatSystemHelper>();
_combatSystemHelper.Initialize(Player, _eventDispatcher, _proxyProvider, playerController);
}
#if VRC_ENABLE_PLAYER_PERSISTENCE
internal void SetupPlayerPersistence(IClientSimEventDispatcher eventDispatcher, IClientSimUdonEventSender udonEventSender, IClientSimBlacklistManager blacklistManager, IClientSimUdonManager udonManager, IClientSimSyncedObjectManager syncedObjectManager,IClientSimPlayerManager playerManager)
{
_eventDispatcher = eventDispatcher;
_udonEventSender = udonEventSender;
eventDispatcher.Subscribe<ClientSimOnPlayerDataDecodedEvent>(OnPlayerDataDecoded);
eventDispatcher.Subscribe<ClientSimOnPlayerObjectsDecodedEvent>(OnPlayerObjectsDecoded);
GameObject dataObject = Instantiate(PlayerDataPrefab.gameObject);
int playerId = playerManager.GetPlayerID(Player);
PlayerDataObject = dataObject.GetComponent<ClientSimPlayerDataStorage>();
PlayerDataObject.Init(Player, udonEventSender, eventDispatcher);
PlayerDataObject.gameObject.hideFlags = HideFlags.HideInHierarchy | HideFlags.HideInInspector;
PlayerObjectData = dataObject.GetComponent<ClientSimPlayerObjectStorage>();
PlayerObjectData.Init(Player, udonEventSender, eventDispatcher);
blacklistManager.AddObjectAndChildrenToBlackList(PlayerDataObject.gameObject);
// spawning PlayerObjects
VRCPlayerObject[] playerObjects = ClientSimNetworkingUtilities.GetPlayerObjectList();
VRCSceneDescriptor sdk3Descriptor = (VRCSceneDescriptor)VRC_SceneDescriptor.Instance;
List<GameObject> playerObjectInstances = new List<GameObject>();
int baseId = (playerId * ClientSimNetworkingUtilities.MaxID) +
ClientSimNetworkingUtilities.FirstPlayerPersistenceID;
for (int i = 0; i < playerObjects.Length; i++)
{
VRCPlayerObject playerObject = playerObjects[i];
GameObject playerObjectGameObject = playerObject.gameObject;
Transform playerObjectTransform = playerObjectGameObject.transform;
playerObjectGameObject.SetActive(false);
GameObject instance = Object.Instantiate(playerObjectGameObject, playerObjectTransform.parent, true);
instance.transform.localScale = playerObjectTransform.localScale;
instance.transform.localPosition = playerObjectTransform.localPosition;
instance.transform.localRotation = playerObjectTransform.localRotation;
playerObjectInstances.Add(instance);
instance.transform.name = playerObjectTransform.name + " [" + playerId + "]";
INetworkID[] networkIds = instance.GetComponentsInChildren<INetworkID>(true);
foreach (INetworkID networkId in networkIds)
{
Component component = networkId as Component;
string path = component.transform.Path(instance.transform);
GameObject ppOriginal = playerObjectTransform.Find(path).gameObject;
int indexNetworkObject = sdk3Descriptor.NetworkIDCollection.FindIndex((x) => x.gameObject == ppOriginal);
if (indexNetworkObject == -1)
{
this.LogError($"Failed to locate player persistence view ID for {playerObjectTransform.name}/{path}");
continue;
}
NetworkIDPair networkIdPair = sdk3Descriptor.NetworkIDCollection[indexNetworkObject];
int viewId = ClientSimNetworkingUtilities.FlattenPlayerViewId(networkIdPair.ID) + baseId;
ConfigureObject(component.gameObject, viewId, playerId, networkId, null, udonManager, syncedObjectManager);
viewId++;
if (viewId >= (playerId * ClientSimNetworkingUtilities.MaxID) +ClientSimNetworkingUtilities.MaxPlayerPersistenceID)
{
this.LogError("Ran out of player persistence view IDs.");
break;
}
}
foreach (VRCStation station in instance.GetComponentsInChildren<VRCStation>()){
ClientSimStationHelper.InitializeStations(station);
}
instance.SetActive(true);
}
IEnumerable<GameObject> withChildren = playerObjectInstances.SelectMany(obj => obj.GetComponentsInChildren<Transform>(true)).Select(t => t.gameObject);
PlayerPersistenceObjects = withChildren.ToArray();
PlayerPersistenceRootObjects = playerObjectInstances.ToArray();
}
private static void ConfigureObject(
GameObject obj,
int viewId,
int playerId,
INetworkID networkId,
string objectName = null,
IClientSimUdonManager udonManager = null,
IClientSimSyncedObjectManager syncedObjectManager = null)
{
VRCEnablePersistence enablePersistence = obj.GetComponentInParent<VRCEnablePersistence>(true);
bool SavePersistence = enablePersistence != null;
if (!obj.TryGetComponent(out ClientSimNetworkingView MainView))
{
MainView = obj.AddComponent<ClientSimNetworkingView>();
}
MainView.SetNetworkId(viewId);
MainView.SetPlayerId(playerId);
MainView.SetPersist(SavePersistence);
InitilizeNetworkHolder(obj, MainView);
switch (networkId)
{
case VRCObjectPool vrcop:
{
if (vrcop)
{
vrcop.NetworkConfigure();
syncedObjectManager.InitializeObjectPool(vrcop);
}
break;
}
case VRCObjectSync vrcos:
{
if (vrcos)
{
vrcos.NetworkConfigure();
// the player initialization happens before the sdk has set the synced object callbacks so we need to do it here
if(VRCObjectSync.OnAwake == null)
syncedObjectManager.InitializeObjectSync(vrcos);
}
break;
}
case VRC.SDK3.Network.VRCNetworkBehaviour vrcnb3:
{
if (vrcnb3)
{
vrcnb3.NetworkConfigure();
}
break;
}
case UdonBehaviour udon:
{
if (udon)
{
if (UdonManager.Instance.HasLoaded)
{
udon.IsNetworkingSupported = true;
UdonManager.Instance.RegisterUdonBehaviour(udon);
}
}
break;
}
case VRCPickup vrcPickup:
{
if (vrcPickup)
{
ClientSimPickupHelper.InitializePickup(vrcPickup);
}
break;
}
}
IClientSimSyncable[] syncables = obj.GetComponentsInChildren<IClientSimSyncable>(true);
foreach (IClientSimSyncable syncable in syncables)
{
syncable.SetOwner(playerId);
}
}
private static void InitilizeNetworkHolder(GameObject obj, ClientSimNetworkingView mainView)
{
if (!obj.TryGetComponent<ClientSimNetworkIdHolder>(out var networkIdHolder))
{
networkIdHolder = obj.AddComponent<ClientSimNetworkIdHolder>();
networkIdHolder.SetNetworkView(mainView);
networkIdHolder.SetNetworkComponents();
mainView.AddNetworkedObject(networkIdHolder);
}
}
private void RemovePlayerPersistenceObjects()
{
for (int i = 0; i < PlayerPersistenceObjects.Length; i++)
{
Object.Destroy(PlayerPersistenceObjects[i]);
}
}
private void CheckPlayerRestored()
{
if (playerRestoredStatus.HasDecodedPlayerData &&
playerRestoredStatus.HasDecodedPlayerObjects &&
!playerRestoredStatus.PlayerRestored)
{
playerRestoredStatus.PlayerRestored = true;
_udonEventSender.RunEvent(UdonManager.UDON_EVENT_ONPLAYERRESTORED, ("player", Player));
_eventDispatcher.SendEvent(new ClientSimOnPlayerRestoredEvent
{
player = Player
});
}
}
private void OnPlayerDataDecoded(ClientSimOnPlayerDataDecodedEvent payload)
{
if (payload.player.playerId != Player.playerId) return;
playerRestoredStatus.HasDecodedPlayerData = true;
CheckPlayerRestored();
}
private void OnPlayerObjectsDecoded(ClientSimOnPlayerObjectsDecodedEvent payload)
{
if (payload.player.playerId != Player.playerId) return;
playerRestoredStatus.HasDecodedPlayerObjects = true;
CheckPlayerRestored();
}
private void OnDestroy()
{
_eventDispatcher?.Unsubscribe<ClientSimOnPlayerDataDecodedEvent>(OnPlayerDataDecoded);
_eventDispatcher?.Unsubscribe<ClientSimOnPlayerObjectsDecodedEvent>(OnPlayerObjectsDecoded);
RemovePlayerPersistenceObjects();
}
public void EnablePlayerObjects()
{
for (int i = 0; i < PlayerPersistenceRootObjects.Length; i++)
{
PlayerPersistenceRootObjects[i].SetActive(true);
}
}
#endif
private void Start()
{
if (!Player.isLocal)
{
return;
}
Camera playerCamera = playerTrackingData.GetCamera();
if (playerCamera != null)
{
_sceneManager.SetupCamera(playerCamera);
}
}
public void EnablePlayer(Transform spawnPoint, float spawnRadius)
{
Vector3 position = ClientSimPlayerSpawner.GetRandomPositionAroundSpawn(spawnPoint.position, spawnRadius);
playerController.Teleport(position, spawnPoint.rotation, false);
gameObject.SetActive(true);
}
public ClientSimPlayerController GetPlayerController()
{
return playerController;
}
public IClientSimPlayerCameraProvider GetCameraProvider()
{
return playerTrackingData;
}
public IClientSimTrackingProvider GetTrackingProvider()
{
return playerTrackingData;
}
public IClientSimPlayerStationManager GetStationHandler()
{
return stationManager;
}
public IClientSimPlayerAvatarDataProvider GetAvatarDataProvider()
{
return playerAvatar;
}
public ClientSimCombatSystemHelper GetCombatHelper()
{
return _combatSystemHelper;
}
public Vector3 GetPosition()
{
if (playerController != null)
{
return playerController.GetPosition();
}
return transform.position;
}
public Quaternion GetRotation()
{
if (playerController != null)
{
return playerController.GetRotation();
}
return transform.rotation;
}
public void SimulateVRCPlusGift()
{
_eventDispatcher?.SendEvent(new ClientSimOnVRCPlusMassGift
{
gifter = Player,
numGifts = 10,
});
}
private class ClientSimPlayerRestoredStatus
{
public bool HasDecodedPlayerData = false;
public bool HasDecodedPlayerObjects = false;
public bool PlayerRestored = false;
}
}
}

View File

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

View File

@ -0,0 +1,76 @@

using UnityEngine;
namespace VRC.SDK3.ClientSim
{
// Listens to Events:
// - ClientSimOnTrackingScaleUpdateEvent
// TODO split into local and remote versions
[AddComponentMenu("")]
public class ClientSimPlayerAvatarManager : ClientSimBehaviour, IClientSimPlayerAvatarDataProvider
{
[SerializeField]
private Animator avatarAnimator;
private IClientSimEventDispatcher _eventDispatcher;
// TODO initialize with option for Generic or Humanoid
// TODO better initialization options for Local vs Remote
public void Initialize(IClientSimEventDispatcher eventDispatcher)
{
_eventDispatcher = eventDispatcher;
_eventDispatcher.Subscribe<ClientSimOnTrackingScaleUpdateEvent>(OnTrackingScaleUpdate);
}
private void OnDestroy()
{
_eventDispatcher?.Unsubscribe<ClientSimOnTrackingScaleUpdateEvent>(OnTrackingScaleUpdate);
}
#region ClientSim Events
private void OnTrackingScaleUpdate(ClientSimOnTrackingScaleUpdateEvent trackingEvent)
{
transform.localScale = trackingEvent.trackingScale * Vector3.one;
}
#endregion
#region IClientSimPlayerAvatarDataProvider
public Transform GetBoneTransform(HumanBodyBones bone)
{
if (avatarAnimator == null)
{
return null;
}
return avatarAnimator.GetBoneTransform(bone);
}
public Quaternion GetBoneRotation(HumanBodyBones bone)
{
if (avatarAnimator == null)
{
return Quaternion.identity;
}
Transform boneTransform = GetBoneTransform(bone);
return boneTransform ? boneTransform.rotation : Quaternion.identity;
}
public Vector3 GetBonePosition(HumanBodyBones bone)
{
if (avatarAnimator == null)
{
return Vector3.zero;
}
Transform boneTransform = GetBoneTransform(bone);
return boneTransform ? boneTransform.position : Vector3.zero;
}
#endregion
}
}

View File

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

View File

@ -0,0 +1,481 @@

using System;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon.Common;
namespace VRC.SDK3.ClientSim
{
// Sends Events:
// - ClientSimOnPlayerTeleportedEvent
// - ClientSimOnPlayerRespawnEvent
// - ClientSimOnPlayerMovedEvent
// Listens to Events:
// - ClientSimMenuStateChangedEvent
// - ClientSimMenuRespawnClickedEvent
// - ClientSimMouseReleasedEvent
// - ClientSimPlayerDeathStatusChangedEvent
// - ClientSimOnTrackingScaleUpdateEvent
// Listens to Input Events:
// - Jump
// - Run
[AddComponentMenu("")]
[DefaultExecutionOrder(-3000)] // Update before player raycasting
public class ClientSimPlayerController : ClientSimBehaviour, IDisposable
{
private const float CROUCH_SPEED_MULTIPLIER = 0.35f;
private const float PRONE_SPEED_MULTIPLIER = 0.15f;
private const float STICK_TO_GROUND_FORCE = 2f;
private const float RATE_OF_AIR_ACCELERATION = 5f;
private IClientSimPlayerStationManager _stationManager;
private IClientSimPlayerLocomotionData _playerLocomotionData;
private IClientSimPlayerApiProvider _playerApi;
private IClientSimSceneManager _sceneManager;
private IClientSimEventDispatcher _eventDispatcher;
private IClientSimInput _input;
private IClientSimTrackingProvider _trackingProvider;
private CharacterController _characterController;
private Transform _cameraProxyObject;
private bool _isDead;
private bool _isWalking = true; // Player defaults to walking
private bool _jump;
private Vector2 _prevInput;
// Check if the directionality changed to apply "stutter stepping" for legacy locomotion.
private bool _directionChanged;
private bool _velSet;
// TODO fix handling of SetVelocity as retaining the velocity causes strange bugs due to ignoring collisions
// that normally would stop the player.
private Vector3 _playerRetainedVelocity;
private bool _menuIsOpen;
private bool _mouseReleased;
protected override void Awake()
{
base.Awake();
_characterController = GetComponent<CharacterController>();
}
public void Initialize(
IClientSimEventDispatcher eventDispatcher,
IClientSimInput input,
IClientSimPlayerApiProvider playerApiProvider,
IClientSimPlayerLocomotionData locomotionData,
IClientSimSceneManager sceneManager,
IClientSimProxyObjectProvider proxyProvider,
IClientSimTrackingProvider trackingProvider,
IClientSimPlayerStationManager stationManager)
{
_eventDispatcher = eventDispatcher;
_input = input;
_playerApi = playerApiProvider;
_playerLocomotionData = locomotionData;
_sceneManager = sceneManager;
_trackingProvider = trackingProvider;
_stationManager = stationManager;
_cameraProxyObject = proxyProvider.CameraProxy().transform;
Subscribe();
}
private void Start()
{
NotifyPlayerMoved();
}
private void OnDestroy()
{
Dispose();
}
private void Subscribe()
{
// Input will be null with incorrect Unity input project settings.
_input?.SubscribeJump(JumpInput);
_input?.SubscribeRun(RunInput);
_eventDispatcher.Subscribe<ClientSimMenuStateChangedEvent>(SetMenuOpen);
_eventDispatcher.Subscribe<ClientSimMenuRespawnClickedEvent>(MenuRespawnEvent);
_eventDispatcher.Subscribe<ClientSimMouseReleasedEvent>(MouseReleasedEvent);
_eventDispatcher.Subscribe<ClientSimPlayerDeathStatusChangedEvent>(CombatStatusEvent);
_eventDispatcher.Subscribe<ClientSimOnTrackingScaleUpdateEvent>(OnTrackingScaleUpdated);
}
public void Dispose()
{
_input?.UnsubscribeJump(JumpInput);
_input?.UnsubscribeRun(RunInput);
_eventDispatcher.Unsubscribe<ClientSimMenuStateChangedEvent>(SetMenuOpen);
_eventDispatcher.Unsubscribe<ClientSimMenuRespawnClickedEvent>(MenuRespawnEvent);
_eventDispatcher.Unsubscribe<ClientSimMouseReleasedEvent>(MouseReleasedEvent);
_eventDispatcher.Unsubscribe<ClientSimPlayerDeathStatusChangedEvent>(CombatStatusEvent);
_eventDispatcher.Unsubscribe<ClientSimOnTrackingScaleUpdateEvent>(OnTrackingScaleUpdated);
}
#region Input Events
private void JumpInput(bool value, HandType hand)
{
// Only handle on down, and not on release.
if (!value)
{
return;
}
if (!_jump && _characterController.isGrounded && _playerLocomotionData.GetJump() > 0)
{
_jump = true;
}
}
private void RunInput(bool value)
{
_isWalking = !value;
}
#endregion
#region ClientSim Events
private void SetMenuOpen(ClientSimMenuStateChangedEvent stateChangedEvent)
{
_menuIsOpen = stateChangedEvent.isMenuOpen;
}
private void MenuRespawnEvent(ClientSimMenuRespawnClickedEvent stateChangedEvent)
{
Respawn();
}
private void MouseReleasedEvent(ClientSimMouseReleasedEvent mouseReleasedEvent)
{
_mouseReleased = mouseReleasedEvent.isReleased;
}
private void CombatStatusEvent(ClientSimPlayerDeathStatusChangedEvent combatStatusEvent)
{
_isDead = combatStatusEvent.isDead;
}
private void OnTrackingScaleUpdated(ClientSimOnTrackingScaleUpdateEvent scaleUpdatedEvent)
{
NotifyPlayerMoved();
}
#endregion
public Vector3 GetPosition()
{
return transform.position;
}
public Quaternion GetRotation()
{
return transform.rotation;
}
public Vector3 GetVelocity()
{
return _characterController.velocity;
}
public void SetVelocity(Vector3 velocity)
{
_playerRetainedVelocity = velocity;
_velSet = true;
_jump = false;
}
public bool IsGrounded()
{
return _characterController.isGrounded;
}
public void Respawn()
{
Transform spawnPoint = _sceneManager.GetSpawnPoint(false);
Vector3 position = ClientSimPlayerSpawner.GetRandomPositionAroundSpawn(spawnPoint.position, _sceneManager.GetSpawnRadius());
Teleport(position, spawnPoint.rotation, false);
_eventDispatcher.SendEvent(new ClientSimOnPlayerRespawnEvent { player = _playerApi.Player });
}
public void Respawn(int index)
{
Transform spawnPoint = _sceneManager.GetSpawnPoint(index);
if (spawnPoint == null)
{
this.LogError($"Spawn {index} not found. Spawning at spawn 0");
spawnPoint = _sceneManager.GetSpawnPoint(0);
}
Vector3 position = ClientSimPlayerSpawner.GetRandomPositionAroundSpawn(spawnPoint.position, _sceneManager.GetSpawnRadius());
Teleport(position, spawnPoint.rotation, false);
_eventDispatcher.SendEvent(new ClientSimOnPlayerRespawnEvent { player = _playerApi.Player });
}
public void Teleport(Transform point, bool fromPlaySpace)
{
Teleport(point.position, Quaternion.Euler(0, point.rotation.eulerAngles.y, 0), fromPlaySpace);
}
public void Teleport(Vector3 position, Quaternion floorRotation, bool fromPlaySpace)
{
floorRotation = Quaternion.Euler(0, floorRotation.eulerAngles.y, 0);
if (fromPlaySpace)
{
VRCPlayerApi.TrackingData playspaceData = _trackingProvider.GetTrackingData(VRCPlayerApi.TrackingDataType.Origin);
Vector3 playspacePos = playspaceData.position - transform.position;
Quaternion playspaceRot = playspaceData.rotation * Quaternion.Inverse(transform.rotation);
floorRotation = floorRotation * Quaternion.Inverse(playspaceRot);
position += floorRotation * -playspacePos;
}
this.Log($"Moving player to {position.ToString("F3")} " +
$"and rotation {floorRotation.eulerAngles.ToString("F3")} " +
$"(fromPlaySpace={fromPlaySpace})");
transform.rotation = floorRotation;
transform.position = position;
NotifyPlayerMoved();
_eventDispatcher.SendEvent(new ClientSimOnPlayerTeleportedEvent { player = _playerApi.Player });
Physics.SyncTransforms();
}
#region Stations
public void EnterStation(IClientSimStation station)
{
if (!station.IsMobile())
{
_characterController.enabled = false;
Transform spawn = station.EnterLocation();
Teleport(spawn, false);
}
// VRChatBug: Note that in the else case, the player is teleported to a location that is twice the distance
// to the station, but since this appears to be a bug, it will not be implemented.
}
public void ExitStation(IClientSimStation station, bool skipTeleport)
{
_characterController.enabled = true;
if (!skipTeleport)
{
Transform spawn = station.ExitLocation();
Teleport(spawn, false);
}
_jump = false;
}
public void SitPosition(Transform seat)
{
transform.SetPositionAndRotation(seat.position, seat.rotation);
NotifyPlayerMoved();
}
#endregion
private void Update()
{
// Handle below respawn height.
if (transform.position.y < _sceneManager.GetRespawnHeight())
{
Respawn();
}
GetInput();
NotifyPlayerMoved();
}
private void FixedUpdate()
{
Physics.SyncTransforms();
Vector2 speed = GetSpeed();
Vector2 input = _prevInput;
if (!_stationManager.CanPlayerMove(input.magnitude))
{
return;
}
if (_menuIsOpen || _isDead)
{
input = Vector2.zero;
_jump = false;
}
// Immobile does not affect Jump
if (_playerLocomotionData.GetImmobilized())
{
input = Vector2.zero;
}
// Always move along the camera forward as it is the direction that it being aimed at
Vector3 desiredMove = input.y * speed.x * transform.forward + input.x * speed.y * transform.right;
desiredMove.y = 0;
float gravityContribution = _playerLocomotionData.GetGravityStrength() * Time.fixedDeltaTime * Physics.gravity.y;
if (!_velSet)
{
if (_characterController.isGrounded)
{
_playerRetainedVelocity = Vector3.zero;
_playerRetainedVelocity.y = -STICK_TO_GROUND_FORCE;
if (_jump)
{
if (!_playerLocomotionData.GetUseLegacyLocomotion())
{
_playerRetainedVelocity = desiredMove;
}
_playerRetainedVelocity.y = _playerLocomotionData.GetJump();
desiredMove = Vector3.zero;
_jump = false;
}
}
else
{
// Slowly add velocity from movement inputs
if (!_playerLocomotionData.GetUseLegacyLocomotion())
{
Vector3 localVelocity = transform.InverseTransformVector(_characterController.velocity);
localVelocity.x = Mathf.Clamp(localVelocity.x, -speed.y, speed.y);
localVelocity.z = Mathf.Clamp(localVelocity.z, -speed.x, speed.x);
Vector3 maxAc = new Vector3(speed.y - localVelocity.x, 0, speed.x - localVelocity.z);
Vector3 minAc = new Vector3(-speed.y - localVelocity.x, 0, -speed.x - localVelocity.z);
Vector3 inputAcceleration = Time.fixedDeltaTime * RATE_OF_AIR_ACCELERATION * new Vector3(input.x * speed.y, 0, input.y * speed.x);
inputAcceleration.x = Mathf.Clamp(inputAcceleration.x, minAc.x, maxAc.x);
inputAcceleration.z = Mathf.Clamp(inputAcceleration.z, minAc.z, maxAc.z);
inputAcceleration = transform.TransformVector(inputAcceleration);
_playerRetainedVelocity += inputAcceleration;
desiredMove = Vector3.zero;
}
// Legacy stutter stepping
else if (_directionChanged)
{
_playerRetainedVelocity = Vector3.zero;
}
_playerRetainedVelocity.y += gravityContribution;
}
}
else // Dumb behavior that hopefully needs to be removed
{
_characterController.Move(new Vector3(desiredMove.x * 0.05f, desiredMove.y * 0.05f + gravityContribution, desiredMove.z * 0.05f) * Time.fixedDeltaTime);
desiredMove = Vector3.zero;
}
desiredMove += _playerRetainedVelocity;
_characterController.Move(desiredMove * Time.fixedDeltaTime);
_velSet = false;
NotifyPlayerMoved();
}
#region Input
private void GetInput()
{
GetMovementInput();
if (_menuIsOpen && Mathf.Max(_prevInput.x, _prevInput.y) > 0)
{
_input.SendToggleMenuEvent(true, HandType.RIGHT);
}
// Only allow these input actions while the menu is closed
if (!_menuIsOpen)
{
RotateView();
}
}
private void GetMovementInput()
{
Vector2 input = _input.GetMovementAxes();
if (input.sqrMagnitude > 1)
{
input.Normalize();
}
_directionChanged = (input.sqrMagnitude < 1e-3 ^ _prevInput.sqrMagnitude < 1e-3);
_prevInput = input;
}
// TODO Move rotation of the player controller to be done in the tracking provider and have the player controller
// copy the head rotation. This would allow more generic handling of mouse released, VR snap turning, and locked in station.
private void RotateView()
{
// Allow player controller to look left and right when not in a locked station and for desktop users
// when the mouse is not released..
if (!_mouseReleased && !_stationManager.IsLockedInStation())
{
float yRot = _input.GetLookHorizontal();
transform.rotation *= Quaternion.Euler(0f, yRot, 0f);
}
}
// Used in tests to help look a specific direction
public void LookTowardsPoint(Vector3 point)
{
if (_stationManager.IsLockedInStation())
{
return;
}
point.y = transform.position.y;
transform.LookAt(point);
}
#endregion
// Notify all systems that are dependent on the player's position that the player has moved to a new location.
private void NotifyPlayerMoved()
{
_eventDispatcher.SendEvent(new ClientSimOnPlayerMovedEvent { player = _playerApi.Player });
if (_cameraProxyObject == null)
{
return;
}
var cameraTrackingData = _trackingProvider.GetTrackingData(VRCPlayerApi.TrackingDataType.Head);
_cameraProxyObject.SetPositionAndRotation(cameraTrackingData.position, cameraTrackingData.rotation);
_cameraProxyObject.localScale = _trackingProvider.GetTrackingScale() * Vector3.one;
}
private Vector2 GetSpeed()
{
// TODO check current bindings to see if non keyboard and only use runspeed.
Vector2 speed = new Vector2(
_isWalking? _playerLocomotionData.GetWalkSpeed() : _playerLocomotionData.GetRunSpeed(),
_playerLocomotionData.GetStrafeSpeed());
switch (_trackingProvider.GetPlayerStance())
{
case ClientSimPlayerStanceEnum.CROUCHING:
speed *= CROUCH_SPEED_MULTIPLIER;
break;
case ClientSimPlayerStanceEnum.PRONE:
speed *= PRONE_SPEED_MULTIPLIER;
break;
}
return speed;
}
}
}

View File

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

View File

@ -0,0 +1,553 @@
using System;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon.Common;
namespace VRC.SDK3.ClientSim
{
/// <summary>
/// This system is responsible for handling pickups.
/// </summary>
/// <remarks>
/// Sends Events:
/// - ClientSimOnPickupEvent
/// - ClientSimOnPickupDropEvent
/// - ClientSimOnPickupUseDownEvent
/// - ClientSimOnPickupUseUpEvent
/// Listens to Input Events:
/// - Grab
/// - Use
/// - Drop
/// </remarks>
[AddComponentMenu("")]
public class ClientSimPlayerHand : ClientSimBehaviour, IDisposable
{
// The duration after picking up an object to start sending pickup UseDown and UseUp events.
private const float INITIAL_PICKUP_DURATION = 0.5f;
// The distance at which a pickup will force snap to the hand.
private const float MAX_PICKUP_DISTANCE = 0.25f;
// How many units will the object rotate during manipulation.
private const float DESKTOP_ROTATION_MULTIPLIER = 2f;
// How many units will an object be moved forward or backwards during manipulation.
private const float DESKTOP_MANIPULATE_MULTIPLIER = 0.01f;
// How far can the object move away from the hand during manipulation.
private const float DESKTOP_MANIPULATION_MAX_DISTANCE = 0.64f;
private static readonly Quaternion _gripOffsetRotation = Quaternion.Euler(0, 35, 0);
private static readonly Quaternion _gunOffsetRotation = Quaternion.Euler(0, 305, 0);
private static readonly Quaternion _desktopManipulationRotation = Quaternion.Euler(180, 35, 90);
[SerializeField]
private HandType handType;
[SerializeField]
private Transform handTransform;
[SerializeField]
private ClientSimPlayerHand otherHand;
private IClientSimEventDispatcher _eventDispatcher;
private IClientSimInput _input;
private IClientSimTrackingProvider _trackingProvider;
private IClientSimPlayerApiProvider _player;
private IClientSimPlayerPickupData _pickupData;
private VRC_Pickup.PickupHand _pickupHandType;
// The object this hand hovering to know if it should pickup an object.
private IClientSimPickupable _hoverPickupable;
// The object currently held by this hand.
private IClientSimPickupable _heldPickupable;
private Rigidbody _heldPickupRigidbody;
private Transform _heldPickupTransform;
private GameObject _heldPickupGameObject;
private FixedJoint _heldPickupJoint;
// Used for determining pickup throw
private Vector3 _previousHandPosition;
private Vector3 _previousHandRotation;
// Check if the use input is down (true) or up (false)
private bool _useInputHeldDown;
// Has this pickup fired the UseDown event, to know if we need to fire the UseUp event.
private bool _isUseDown;
private bool _initialGrab;
private float _grabActionStartTime;
private float _dropActionStartTime;
public void Initialize(
IClientSimEventDispatcher eventDispatcher,
IClientSimInput input,
IClientSimTrackingProvider trackingProvider,
IClientSimPlayerApiProvider player,
IClientSimPlayerPickupData pickupData)
{
_eventDispatcher = eventDispatcher;
_input = input;
_trackingProvider = trackingProvider;
_player = player;
_pickupData = pickupData;
// Too many hand enums...
_pickupHandType = (handType == HandType.LEFT ? VRC_Pickup.PickupHand.Left : VRC_Pickup.PickupHand.Right);
enabled = false; // Only enabled while holding something to reduce Update checks.
// Subscribe to input events
// Input will be null with incorrect Unity input project settings.
_input?.SubscribeGrab(GrabInput);
_input?.SubscribeUse(UseInput);
_input?.SubscribeDrop(DropInput);
}
private void OnDestroy()
{
Dispose();
}
public void Dispose()
{
// Unsubscribe
_input?.UnsubscribeGrab(GrabInput);
_input?.UnsubscribeUse(UseInput);
_input?.UnsubscribeDrop(DropInput);
}
private void Update()
{
if (!IsHolding())
{
return;
}
if (_initialGrab && _useInputHeldDown)
{
HandleUseInput();
}
UpdateManipulation();
UpdatePosition();
_previousHandPosition = handTransform.position;
_previousHandRotation = handTransform.rotation.eulerAngles;
}
#region ClientSim Input
private void GrabInput(bool value, HandType hand)
{
if (hand != handType)
{
return;
}
if (value)
{
// Try to grab hover object
if (!IsHolding() && _hoverPickupable != null)
{
Pickup(_hoverPickupable);
}
}
else
{
// If releasing grab input and holding a pickup that is not auto hold, drop the pickup.
if (IsHolding() && !ShouldAutoHoldPickupable(_heldPickupable))
{
ForceDrop(_heldPickupable);
}
}
}
private void UseInput(bool value, HandType hand)
{
if (hand != handType)
{
return;
}
// Save input state to know when we can fire the first UseDown event after being picked up. See Update.
_useInputHeldDown = value;
if (!IsHolding())
{
return;
}
HandleUseInput();
}
private void DropInput(bool value, HandType hand)
{
if (hand != handType)
{
return;
}
// Do not try to drop anything if not holding a pickup.
if (!IsHolding())
{
return;
}
if (value)
{
// Button was just pressed. Start a timer to know how much "throw" charge should be added.
_dropActionStartTime = Time.time;
}
else
{
Drop(_heldPickupable, Time.time - _dropActionStartTime);
}
}
#endregion
public void SetHoverPickupable(IClientSimPickupable pickupable)
{
_hoverPickupable = pickupable;
}
public bool IsHolding()
{
return _heldPickupable != null;
}
private bool ShouldAutoHoldPickupable(IClientSimPickupable pickupable)
{
// Some VR controllers do not support auto hold.
return pickupable.AutoHold() && _trackingProvider.SupportsPickupAutoHold();
}
private void Pickup(IClientSimPickupable pickupable)
{
if (IsHolding())
{
LogErrorMessage("Cannot pickup a pickup while holding another.");
return;
}
if (pickupable.IsHeld())
{
// Allow yourself to grab a pickup from your other hand.
if (otherHand != null && otherHand._heldPickupable == pickupable)
{
otherHand.ForceDrop(pickupable);
}
else
{
LogErrorMessage("Cannot pickup a pickup someone else is holding.");
return;
}
}
handTransform.localPosition = Vector3.zero;
handTransform.localRotation = Quaternion.identity;
_heldPickupable = pickupable;
_heldPickupTransform = pickupable.GetTransform();
_heldPickupGameObject = pickupable.GetGameObject();
_heldPickupRigidbody = pickupable.GetRigidbody();
LogMessage($"Picking up object {_heldPickupGameObject.name}");
VRC_Pickup pickup = pickupable.GetPickup();
_pickupData.SetPickupInHand(_pickupHandType, pickup);
pickupable.Pickup(_player.Player, _pickupHandType, ForceDrop);
// Set the grab time to know if the player has held long enough to send Use events
_grabActionStartTime = Time.time;
_initialGrab = true;
// Set self enabled to allow for pickup manipulation
enabled = true;
VRC_Pickup.PickupOrientation pickupOrientation = pickupable.GetOrientation();
Transform pickupExactGrip = pickupable.GetGripLocation();
Transform pickupExactGun = pickupable.GetGunLocation();
// Calculate offset
Transform pickupHoldPoint = null;
Quaternion offsetRotation = Quaternion.identity;
if (pickupOrientation == VRC_Pickup.PickupOrientation.Grip && pickupExactGrip != null)
{
pickupHoldPoint = pickupExactGrip;
offsetRotation = _gripOffsetRotation;
}
else if (pickupOrientation == VRC_Pickup.PickupOrientation.Gun && pickupExactGun != null)
{
pickupHoldPoint = pickupExactGun;
offsetRotation = _gunOffsetRotation;
}
Vector3 positionOffset;
Quaternion rotationOffset;
// Grab as if no pickup point
if (pickupHoldPoint == null)
{
rotationOffset = Quaternion.Inverse(handTransform.rotation) * _heldPickupTransform.rotation;
positionOffset = handTransform.InverseTransformDirection(_heldPickupTransform.position - handTransform.position);
if (positionOffset.magnitude > MAX_PICKUP_DISTANCE && pickupOrientation == VRC_Pickup.PickupOrientation.Any)
{
positionOffset = positionOffset.normalized * MAX_PICKUP_DISTANCE;
}
}
else
{
rotationOffset = offsetRotation * Quaternion.Inverse(Quaternion.Inverse(_heldPickupTransform.rotation) * pickupHoldPoint.rotation);
positionOffset = rotationOffset * _heldPickupTransform.InverseTransformDirection(_heldPickupTransform.position - pickupHoldPoint.position);
}
Vector3 position = handTransform.position + handTransform.TransformDirection(positionOffset);
Quaternion rotation = handTransform.rotation * rotationOffset;
// Move hand and pickup to the same location
handTransform.position = _heldPickupTransform.position = position;
handTransform.rotation = _heldPickupTransform.rotation = rotation;
// Link with hand rigidbody
_heldPickupJoint = handTransform.gameObject.AddComponent<FixedJoint>();
_heldPickupJoint.connectedBody = _heldPickupRigidbody;
// Set the owner of this object to the player picking it up.
Networking.SetOwner(_player.Player, _heldPickupGameObject);
_eventDispatcher.SendEvent(new ClientSimOnPickupEvent
{
player = _player.Player,
handType = handType,
pickup = pickupable,
});
// Notify pickup handlers of object pickup.
foreach (var pickupHandler in _heldPickupGameObject.GetComponents<IClientSimPickupHandler>())
{
pickupHandler.OnPickup();
}
}
public void ForceDrop()
{
if (_heldPickupable != null)
{
ForceDrop(_heldPickupable);
}
}
private void ForceDrop(IClientSimPickupable pickupable)
{
Drop(pickupable, 0);
}
private void Drop(IClientSimPickupable pickupable, float throwHoldDuration)
{
if (_heldPickupable != pickupable || !pickupable.IsHeld() || pickupable.GetHoldingPlayer() != _player.Player)
{
LogErrorMessage("Cannot drop a pickup that you aren't holding.");
return;
}
// Ensure that UseUp is called before the drop event finishes.
OnPickupUseUp();
// Check to return early and ensure no errors if OnPickupUseUp calls Drop.
if (_heldPickupable != pickupable || !pickupable.IsHeld() || pickupable.GetHoldingPlayer() != _player.Player)
{
return;
}
LogMessage($"Dropping object {_heldPickupGameObject.name}");
// Unlink from arm rigidbody
if (_heldPickupJoint != null)
{
Destroy(_heldPickupJoint);
}
// When exiting playmode while holding an object, Drop will be called and Time.deltaTime will be 0.
// This check prevents setting the velocity to NaN due to divide by zero.
if (Time.deltaTime > 0)
{
_heldPickupRigidbody.velocity = (handTransform.position - _previousHandPosition) * (0.5f / Time.deltaTime);
_heldPickupRigidbody.angularVelocity = (handTransform.rotation.eulerAngles - _previousHandRotation);
}
// Calculate throw velocity
// TODO Verify how VR handles throwing pickups
if (!_heldPickupRigidbody.isKinematic)
{
float holdDuration = Mathf.Clamp(throwHoldDuration, 0, 3);
if (holdDuration > 0.2f)
{
float power = holdDuration * 500 * pickupable.GetThrowVelocityBoostScale();
Vector3 throwForce = power * transform.TransformDirection(_gripOffsetRotation * Vector3.forward);
_heldPickupRigidbody.AddForce(throwForce);
LogMessage($"Adding throw force: {throwForce}");
}
}
pickupable.Drop(_player.Player);
_pickupData.SetPickupInHand(_pickupHandType, null);
_eventDispatcher.SendEvent(new ClientSimOnPickupDropEvent
{
player = _player.Player,
handType = handType,
pickup = pickupable,
});
// Notify pickup handlers that the object has been dropped.
foreach (var pickupHandler in _heldPickupGameObject.GetComponents<IClientSimPickupHandler>())
{
pickupHandler.OnDrop();
}
_heldPickupable = null;
_heldPickupTransform = null;
_heldPickupGameObject = null;
_heldPickupRigidbody = null;
// Prevent throwing an exception when exiting playmode due to this object being destroyed.
if (this != null)
{
enabled = false;
}
handTransform.localPosition = Vector3.zero;
handTransform.localRotation = Quaternion.identity;
}
private bool HeldLongEnoughForUseEvents()
{
float grabDuration = Time.time - _grabActionStartTime;
return grabDuration >= INITIAL_PICKUP_DURATION;
}
private void HandleUseInput()
{
// Grab time has not been long enough to send use events
if (!HeldLongEnoughForUseEvents())
{
return;
}
// Only auto hold pickups can be used.
if (!_heldPickupable.AutoHold())
{
return;
}
if (_useInputHeldDown)
{
OnPickupUseDown();
}
else
{
OnPickupUseUp();
}
}
private void OnPickupUseDown()
{
LogMessage($"Pickup Use Down {_heldPickupGameObject.name}");
_initialGrab = false;
_isUseDown = true;
_eventDispatcher.SendEvent(new ClientSimOnPickupUseDownEvent
{
player = _player.Player,
handType = handType,
pickup = _heldPickupable,
});
// Notify pickup handlers that the object has Use Down.
foreach (var pickupHandler in _heldPickupGameObject.GetComponents<IClientSimPickupHandler>())
{
pickupHandler.OnPickupUseDown();
}
}
private void OnPickupUseUp()
{
// Prevent calling UseUp if UseDown was never called.
if (!_isUseDown)
{
return;
}
LogMessage($"Pickup Use Up {_heldPickupGameObject.name}");
_isUseDown = false;
_eventDispatcher.SendEvent(new ClientSimOnPickupUseUpEvent
{
player = _player.Player,
handType = handType,
pickup = _heldPickupable,
});
// Notify pickup handlers that the object has Use Up.
foreach (var pickupHandler in _heldPickupGameObject.GetComponents<IClientSimPickupHandler>())
{
pickupHandler.OnPickupUseUp();
}
}
// Apply desktop hand rotations
private void UpdateManipulation()
{
if (handType != HandType.RIGHT || !_heldPickupable.AllowManipulation())
{
return;
}
// Get the input for rotating the pickup.
Vector3 angles = new Vector3(
_input.GetPickupRotateUpDown(),
_input.GetPickupRotateLeftRight(),
_input.GetPickupRotateCwCcw());
// Only apply rotation if some input has been detected.
if (angles.sqrMagnitude > 0)
{
// Rotate the input angles to match rotation based on desktop view.
angles = transform.rotation * _desktopManipulationRotation * angles;
// Apply rotation to hand.
handTransform.Rotate(angles, DESKTOP_ROTATION_MULTIPLIER,Space.World);
}
// Move pickup forward and back.
float manipulateForwardBack = _input.GetPickupManipulateDistance();
if (!Mathf.Approximately(manipulateForwardBack, 0))
{
Vector3 forward = _gripOffsetRotation * Vector3.forward;
Vector3 offset = forward * Mathf.Sign(manipulateForwardBack) * DESKTOP_MANIPULATE_MULTIPLIER;
Vector3 handLocal = handTransform.localPosition + offset;
handLocal = Vector3.ClampMagnitude(handLocal, DESKTOP_MANIPULATION_MAX_DISTANCE);
handTransform.localPosition = handLocal;
}
}
public void UpdatePosition(bool force = false)
{
if ((_heldPickupRigidbody != null && _heldPickupRigidbody.isKinematic) || force)
{
_heldPickupTransform.SetPositionAndRotation(handTransform.position, handTransform.rotation);
}
}
private void LogMessage(string message)
{
this.Log($"[{handType}] {message}");
}
private void LogErrorMessage(string message)
{
this.LogError($"[{handType}] {message}");
}
}
}

View File

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

View File

@ -0,0 +1,276 @@

using System;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon.Common;
namespace VRC.SDK3.ClientSim
{
/// <summary>
/// This system is responsible for handling finding objects that can be interacted with or picked up.
/// </summary>
/// <remarks>
/// Sends Events:
/// - ClientSimRaycastHitResultsEvent
/// - ClientSimInteractEvent
/// Listens to Events:
/// - ClientSimOnPlayerMovedEvent
/// - ClientSimPlayerDeathStatusChangedEvent
/// Listens to Input Events:
/// - Use
/// </remarks>
[AddComponentMenu("")]
// Unity Event System Updates at -1000. Send raycast events before then to ensure UI interactions happen same frame.
[DefaultExecutionOrder(-2000)]
public class ClientSimPlayerRaycaster : ClientSimBehaviour, IDisposable
{
[SerializeField]
private ClientSimPlayerHand leftHand;
[SerializeField]
private ClientSimPlayerHand rightHand;
private IClientSimEventDispatcher _eventDispatcher;
private IClientSimInput _input;
private IClientSimPlayerApiProvider _playerApiProvider;
private IClientSimHighlightManager _highlightManager;
private IClientSimTooltipManager _tooltipManager;
private IClientSimInteractiveLayerProvider _interactiveLayerProvider;
private IClientSimInteractManager _interactManager;
private IClientSimTrackingProvider _trackingProvider;
private IClientSimPlayerStationManager _stationManager;
private ClientSimRaycaster _leftHandRaycaster;
private ClientSimRaycaster _rightHandRaycaster;
private ClientSimRaycastResults _hoverLeft;
private ClientSimRaycastResults _hoverRight;
public void Initialize(
IClientSimEventDispatcher eventDispatcher,
IClientSimInput input,
IClientSimPlayerApiProvider playerApiProvider,
IClientSimPlayerPickupData pickupData,
IClientSimHighlightManager highlightManager,
IClientSimTooltipManager tooltipManager,
IClientSimInteractiveLayerProvider interactiveLayerProvider,
IClientSimPlayerCameraProvider cameraProvider,
IClientSimMousePositionProvider mousePositionProvider,
IClientSimInteractManager interactManager,
IClientSimTrackingProvider trackingProvider,
IClientSimPlayerStationManager stationManager)
{
_eventDispatcher = eventDispatcher;
_input = input;
_playerApiProvider = playerApiProvider;
_highlightManager = highlightManager;
_tooltipManager = tooltipManager;
_interactiveLayerProvider = interactiveLayerProvider;
_interactManager = interactManager;
_trackingProvider = trackingProvider;
_stationManager = stationManager;
leftHand.Initialize(_eventDispatcher, _input, trackingProvider, _playerApiProvider, pickupData);
rightHand.Initialize(_eventDispatcher, _input, trackingProvider, _playerApiProvider, pickupData);
// Input will be null with incorrect Unity input project settings.
_input?.SubscribeUse(UseInput);
_eventDispatcher.Subscribe<ClientSimOnPlayerMovedEvent>(OnPlayerMoved);
_eventDispatcher.Subscribe<ClientSimPlayerDeathStatusChangedEvent>(CombatStatusEvent);
// Create raycasters
if (_trackingProvider.IsVR())
{
_leftHandRaycaster = new ClientSimRaycaster(
new ClientSimTransformRayProvider(_trackingProvider.GetHandRaycastTransform(HandType.LEFT)),
_interactiveLayerProvider,
_interactManager);
_rightHandRaycaster = new ClientSimRaycaster(
new ClientSimTransformRayProvider(_trackingProvider.GetHandRaycastTransform(HandType.RIGHT)),
_interactiveLayerProvider,
_interactManager);
}
else
{
// Left hand is always null for desktop users.
_leftHandRaycaster = null;
// Right hand is from the player's camera.
_rightHandRaycaster = new ClientSimRaycaster(
new ClientSimCameraRayProvider(cameraProvider, mousePositionProvider),
_interactiveLayerProvider,
_interactManager);
}
}
private void OnDestroy()
{
Dispose();
}
public void Dispose()
{
_input?.UnsubscribeUse(UseInput);
_eventDispatcher.Unsubscribe<ClientSimOnPlayerMovedEvent>(OnPlayerMoved);
_eventDispatcher.Unsubscribe<ClientSimPlayerDeathStatusChangedEvent>(CombatStatusEvent);
}
private void Update()
{
UpdateHandPositions();
SetHoverLeft(_leftHandRaycaster?.CheckForInteracts());
SetHoverRight(_rightHandRaycaster.CheckForInteracts());
}
private void SetHoverLeft(ClientSimRaycastResults raycastResults)
{
SetHover(ref _hoverLeft, HandType.LEFT, leftHand, raycastResults);
}
private void SetHoverRight(ClientSimRaycastResults raycastResults)
{
SetHover(ref _hoverRight, HandType.RIGHT, rightHand, raycastResults);
}
private void SetHover(
ref ClientSimRaycastResults handHover,
HandType handType,
ClientSimPlayerHand playerHand,
ClientSimRaycastResults raycastResults)
{
// TODO optimize this to check if previous was the same as new and not disable/re-enable.
if (handHover != null && handHover.interactable != null)
{
_highlightManager.DisableObjectHighlight(handHover.hitObject);
_tooltipManager.DisableTooltip(handHover.interactable);
}
raycastResults = FilterRaycastResults(raycastResults);
// If the player is not holding something, or the hit object is a UIShape, set the hover.
// This allows players to interact with UI while still holding objects,
// but holding objects will block pickups and interacts.
if (!playerHand.IsHolding() || (raycastResults != null && raycastResults.uiShape != null))
{
handHover = raycastResults;
}
else
{
handHover = null;
}
// Highlight the object if it has an interactable
if (handHover != null && handHover.interactable != null)
{
_highlightManager.EnableObjectHighlight(handHover.hitObject);
_tooltipManager.DisplayTooltip(handHover.interactable);
}
// If the hovered object has a pickupable that can be interacted with, set that as this hand's hovered pickup.
IClientSimPickupable pickupable = handHover?.GetPickupable();
if (pickupable != null && !_interactManager.CanInteract(pickupable, handHover.distance))
{
pickupable = null;
}
playerHand.SetHoverPickupable(pickupable);
_eventDispatcher.SendEvent(new ClientSimRaycastHitResultsEvent
{
handType = handType,
raycastResults = handHover
});
}
private ClientSimRaycastResults FilterRaycastResults(ClientSimRaycastResults results)
{
if (results == null || results.hitObject == null)
{
return results;
}
// If the user is in a station with "CanUseStationFromStation" set to false, disable interacts
// for any object that contains a station script.
if (_stationManager.InStation()
&& !_stationManager.GetCurrentStation().CanUseStationFromStation()
&& results.hitObject.GetComponent<IClientSimStation>() != null)
{
return null;
}
return results;
}
private void TryInteract(HandType handType, ClientSimRaycastResults hover)
{
// Nothing to interact with.
if (hover == null || hover.interactable == null)
{
return;
}
// Interact with the object and get the components that were interacted with.
var interacts = _interactManager.Interact(hover.hitObject, hover.distance);
// Notify ClientSim of interacted objects.
_eventDispatcher.SendEvent(new ClientSimInteractEvent
{
handType = handType,
interactObject = hover.hitObject,
interactDistance = hover.distance,
interacts = interacts,
});
}
private void UpdateHandPositions()
{
// TODO do not update hands if player controller is stuck in a collider.
// Note that these hands are only for pickups and that raycast logic uses the tracking provider hands.
var leftHandData = _trackingProvider.GetTrackingData(VRCPlayerApi.TrackingDataType.LeftHand);
leftHand.transform.SetPositionAndRotation(leftHandData.position, leftHandData.rotation);
leftHand.UpdatePosition();
var rightHandData = _trackingProvider.GetTrackingData(VRCPlayerApi.TrackingDataType.RightHand);
rightHand.transform.SetPositionAndRotation(rightHandData.position, rightHandData.rotation);
rightHand.UpdatePosition();
}
#region ClientSim Input
private void UseInput(bool value, HandType hand)
{
if (!value)
{
return;
}
if (hand == HandType.LEFT)
{
TryInteract(HandType.LEFT, _hoverLeft);
}
if (hand == HandType.RIGHT)
{
TryInteract(HandType.RIGHT, _hoverRight);
}
}
#endregion
#region ClientSim Events
private void OnPlayerMoved(ClientSimOnPlayerMovedEvent moveEvent)
{
UpdateHandPositions();
}
private void CombatStatusEvent(ClientSimPlayerDeathStatusChangedEvent combatStatusEvent)
{
leftHand.ForceDrop();
rightHand.ForceDrop();
}
#endregion
}
}

View File

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

View File

@ -0,0 +1,212 @@

using System;
using UnityEngine;
using VRC.SDKBase;
namespace VRC.SDK3.ClientSim
{
/// <summary>
/// This system is responsible for the player entering and exiting stations as well as repositioning the player to the station at the end of frame.
/// </summary>
/// <remarks>
/// Sends Events:
/// - ClientSimOnPlayerEnteredStationEvent
/// - ClientSimOnPlayerExitedStationEvent
/// Listens to Events:
/// - ClientSimOnPlayerTeleportedEvent
/// </remarks>
[AddComponentMenu("")]
// High execution order to ensure the player is positioned properly at the end of the frame.
[DefaultExecutionOrder(30000)]
public class ClientSimPlayerStationManager : ClientSimBehaviour, IClientSimPlayerStationManager, IDisposable
{
private IClientSimEventDispatcher _eventDispatcher;
private IClientSimPlayerApiProvider _playerApiProvider;
private ClientSimPlayerController _playerController;
private IClientSimStation _currentStation;
protected override void Awake()
{
base.Awake();
_playerController = GetComponent<ClientSimPlayerController>();
}
public void Initialize(
IClientSimEventDispatcher eventDispatcher,
IClientSimPlayerApiProvider playerApiProvider)
{
_eventDispatcher = eventDispatcher;
_playerApiProvider = playerApiProvider;
_eventDispatcher.Subscribe<ClientSimOnPlayerTeleportedEvent>(OnPlayerTeleported);
// Only enable this object while sitting in a station to prevent unneeded update checks.
enabled = false;
}
private void OnDestroy()
{
Dispose();
}
public void Dispose()
{
_eventDispatcher.Unsubscribe<ClientSimOnPlayerTeleportedEvent>(OnPlayerTeleported);
}
private void Update()
{
UpdateStationPosition();
}
private void LateUpdate()
{
// VRChatBug: VRChat seems to not handle the rotation in late update causing player's rotation to jitter
// while in a station that is updated in late update. This is not recreated here.
UpdateStationPosition();
}
private void FixedUpdate()
{
UpdateStationPosition();
}
public bool InStation()
{
return _currentStation != null;
}
public IClientSimStation GetCurrentStation()
{
return _currentStation;
}
public bool IsLockedInStation()
{
return InStation() && _currentStation.IsLockedInStation();
}
public bool CanPlayerMove(float moveValue)
{
return !InStation() || CanPlayerMoveWhileSeated(moveValue);
}
public void EnterStation(IClientSimStation station)
{
GameObject stationObj = station.GetStationGameObject();
if (_currentStation != null)
{
// VRChatBug: VRCStation.CanUseStationFromStation does not care about actually trying to enter stations.
// This will only block the interact on the object. Calling enter station while sitting in a station
// with this property set to false will still allow you to exit the current station and enter the new one.
ExitStation(_currentStation, true);
}
VRCPlayerApi player = _playerApiProvider.Player;
this.Log($"Entering Station {Tools.GetGameObjectPath(stationObj)}");
// Immobilize the player while sitting in the station.
// VRChatBug: Note that "mobile" stations require that seated be set to false and have mobility set to mobile.
if (!station.IsMobile())
{
player.Immobilize(true);
}
station.EnterStation(player);
_playerController.EnterStation(station);
// Set the station after notifying the player controller to prevent exit on Teleport.
_currentStation = station;
enabled = true;
// Notify all station handlers after notifying to ensure that player location is updated first.
var vrcStation = station.GetStation();
foreach (var stationHandler in stationObj.GetComponents<IClientSimStationHandler>())
{
stationHandler.OnStationEnter(vrcStation);
}
_eventDispatcher.SendEvent(new ClientSimOnPlayerEnteredStationEvent {player = player, station = station});
}
public void ExitStation(IClientSimStation station, bool forcedExit = false)
{
// Prevent Exception on exit playmode when this object is destroyed.
if (this == null)
{
return;
}
GameObject stationObj = station.GetStationGameObject();
if (_currentStation != station)
{
this.LogError($"Cannot exit station that the player is not in. {Tools.GetGameObjectPath(stationObj)}");
return;
}
_currentStation = null;
enabled = false;
this.Log($"Exiting Station {Tools.GetGameObjectPath(stationObj)}");
VRCPlayerApi player = _playerApiProvider.Player;
player.Immobilize(false);
station.ExitStation(player);
// Notify all station handlers first as player exit is handled after this event.
var vrcStation = station.GetStation();
foreach (var stationHandler in stationObj.GetComponents<IClientSimStationHandler>())
{
stationHandler.OnStationExit(vrcStation);
}
_playerController.ExitStation(station, forcedExit);
_eventDispatcher.SendEvent(new ClientSimOnPlayerExitedStationEvent {player = player, station = station});
}
private void UpdateStationPosition()
{
if (!InStation() || _currentStation.IsMobile())
{
return;
}
_playerController.SitPosition(_currentStation.EnterLocation());
}
// Returns if the player should move, and exit station if the player is in a non mobile station with exit enabled.
private bool CanPlayerMoveWhileSeated(float speed)
{
if (Mathf.Abs(speed) >= 0.1f && !_currentStation.DisableStationExit())
{
ExitStation(_currentStation);
return true;
}
if (_currentStation.IsMobile())
{
return true;
}
return false;
}
#region ClientSim Events
// Note that respawn is considered a teleport and will automatically be handled by this event.
private void OnPlayerTeleported(ClientSimOnPlayerTeleportedEvent teleportEvent)
{
if (InStation())
{
ExitStation(_currentStation, true);
}
}
#endregion
}
}

View File

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

View File

@ -0,0 +1,104 @@

using System;
using UnityEngine;
namespace VRC.SDK3.ClientSim
{
/// <summary>
/// This system is responsible for displaying the Reticle in the center of the screen
/// and for displaying the mouse UI pointer if there is a UI Shape under the mouse.
/// </summary>
/// <remarks>
/// Listens to Events:
/// - ClientSimMouseReleasedEvent
/// - ClientSimRaycastHitResultsEvent
/// </remarks>
[AddComponentMenu("")]
public class ClientSimReticle : ClientSimBehaviour, IDisposable
{
[SerializeField]
private Texture2D reticle;
[SerializeField]
private Texture2D uiShapeHoverIcon;
private IClientSimEventDispatcher _eventDispatcher;
private ClientSimSettings _settings;
private IClientSimMousePositionProvider _mousePositionProvider;
private bool _mouseReleased = false;
private int _lastUiShapeHoveredFrame = -1;
public void Initialize(
IClientSimEventDispatcher eventDispatcher,
ClientSimSettings settings,
IClientSimMousePositionProvider mousePositionProvider)
{
_settings = settings;
_eventDispatcher = eventDispatcher;
_mousePositionProvider = mousePositionProvider;
_eventDispatcher.Subscribe<ClientSimMouseReleasedEvent>(MouseReleasedEvent);
_eventDispatcher.Subscribe<ClientSimRaycastHitResultsEvent>(OnRaycastHit);
}
private void OnDestroy()
{
Dispose();
}
public void Dispose()
{
_eventDispatcher?.Unsubscribe<ClientSimMouseReleasedEvent>(MouseReleasedEvent);
_eventDispatcher?.Unsubscribe<ClientSimRaycastHitResultsEvent>(OnRaycastHit);
}
#region ClientSim Events
private void MouseReleasedEvent(ClientSimMouseReleasedEvent mouseReleasedEvent)
{
_mouseReleased = mouseReleasedEvent.isReleased;
}
private void OnRaycastHit(ClientSimRaycastHitResultsEvent hitEvent)
{
if (hitEvent.raycastResults?.uiShape != null)
{
_lastUiShapeHoveredFrame = Time.frameCount;
}
}
#endregion
private bool ShouldShowReticle()
{
return _settings.showDesktopReticle && !_mouseReleased;
}
private bool ShouldShowUiShapeHoverIcon()
{
return _lastUiShapeHoveredFrame == Time.frameCount;
}
private void OnGUI()
{
if (ShouldShowReticle())
{
Vector2 center = ClientSimBaseInput.GetScreenCenter();
Vector2 size = new Vector2(reticle.width, reticle.height);
Rect position = new Rect(center - size * 0.5f, size);
GUI.DrawTexture(position, reticle);
}
if (ShouldShowUiShapeHoverIcon())
{
Vector2 mousePos = _mousePositionProvider.GetMousePosition();
// GUI draws with inverted y
mousePos.y = Screen.height - mousePos.y;
Vector2 size = new Vector2(uiShapeHoverIcon.width, uiShapeHoverIcon.height);
Rect position = new Rect(mousePos - new Vector2(8, 8), size);
GUI.DrawTexture(position, uiShapeHoverIcon);
}
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,9 @@
using VRC.SDKBase;
namespace VRC.SDK3.ClientSim
{
public interface IClientSimPlayerApiProvider
{
VRCPlayerApi Player { get; }
}
}

View File

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

View File

@ -0,0 +1,17 @@
namespace VRC.SDK3.ClientSim
{
public interface IClientSimPlayerAudioData
{
void SetAvatarAudioVolumetricRadius(float value);
void SetAvatarAudioNearRadius(float value);
void SetAvatarAudioFarRadius(float value);
void SetAvatarAudioGain(float value);
void SetAvatarAudioForceSpatial(bool value);
void SetAvatarAudioCustomCurve(bool value);
void SetVoiceLowpass(bool value);
void SetVoiceVolumetricRadius(float value);
void SetVoiceDistanceFar(float value);
void SetVoiceDistanceNear(float value);
void SetVoiceGain(float value);
}
}

View File

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

View File

@ -0,0 +1,11 @@
using UnityEngine;
namespace VRC.SDK3.ClientSim
{
public interface IClientSimPlayerAvatarDataProvider
{
Transform GetBoneTransform(HumanBodyBones bone);
Quaternion GetBoneRotation(HumanBodyBones bone);
Vector3 GetBonePosition(HumanBodyBones bone);
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9daea73a61febf242b9a4aa963cc0dd7
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 IClientSimPlayerCameraProvider
{
Camera GetCamera();
Camera GetCameraForObject(GameObject obj);
}
}

View File

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

View File

@ -0,0 +1,20 @@
namespace VRC.SDK3.ClientSim
{
public interface IClientSimPlayerLocomotionData
{
float GetJump();
void SetJump(float jump);
float GetRunSpeed();
void SetRunSpeed(float runSpeed);
float GetWalkSpeed();
void SetWalkSpeed(float walkSpeed);
float GetStrafeSpeed();
void SetStrafeSpeed(float strafeSpeed);
float GetGravityStrength();
void SetGravityStrength(float gravity);
bool GetImmobilized();
void SetImmobilized(bool immobilize);
void SetUseLegacyLocomotion(bool useLegacyLocomotion);
bool GetUseLegacyLocomotion();
}
}

View File

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

View File

@ -0,0 +1,12 @@
using VRC.SDKBase;
namespace VRC.SDK3.ClientSim
{
public interface IClientSimPlayerPickupData
{
void SetPickupsEnabled(bool enabled);
bool GetPickupsEnabled();
VRC_Pickup GetPickupInHand(VRC_Pickup.PickupHand hand);
void SetPickupInHand(VRC_Pickup.PickupHand hand, VRC_Pickup pickup);
}
}

View File

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

View File

@ -0,0 +1,12 @@
namespace VRC.SDK3.ClientSim
{
public interface IClientSimPlayerStationManager
{
bool InStation();
IClientSimStation GetCurrentStation();
bool IsLockedInStation();
bool CanPlayerMove(float moveValue);
void EnterStation(IClientSimStation station);
void ExitStation(IClientSimStation station, bool forcedExit = false);
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1830e2049be64bfc95285fd733b6944b
timeCreated: 1641177044

View File

@ -0,0 +1,10 @@
namespace VRC.SDK3.ClientSim
{
public interface IClientSimPlayerTagsData
{
void ClearPlayerTags();
void SetPlayerTag(string tagName, string tagValue);
string GetPlayerTag(string tagName);
bool HasPlayerTag(string tagName, string tagValue);
}
}

View File

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

View File

@ -0,0 +1,18 @@
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon.Common;
namespace VRC.SDK3.ClientSim
{
public interface IClientSimTrackingProvider : IClientSimPlayerCameraProvider
{
VRCPlayerApi.TrackingData GetTrackingData(VRCPlayerApi.TrackingDataType trackingDataType);
Transform GetTrackingTransform(VRCPlayerApi.TrackingDataType trackingDataType);
float GetTrackingScale();
void SetTrackingScale(float scale);
ClientSimPlayerStanceEnum GetPlayerStance();
Transform GetHandRaycastTransform(HandType handType);
bool IsVR();
bool SupportsPickupAutoHold();
}
}

View File

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

View File

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

View File

@ -0,0 +1,70 @@
using System;
namespace VRC.SDK3.ClientSim
{
[Serializable]
public class ClientSimPlayerAudioData : IClientSimPlayerAudioData
{
public float voiceVolumetricRadius = 0;
public float voiceDistanceNear = 0;
public float voiceDistanceFar = 25;
public float voiceGain = 15;
public bool voiceLowpass = false;
public float avatarAudioVolumetricRadius = 40;
public float avatarAudioNearRadius = 40;
public float avatarAudioFarRadius = 40;
public float avatarAudioGain = 10;
public bool avatarAudioCustomCurve;
public bool avatarAudioForceSpatial;
#region Player Audio
public void SetVoiceGain(float value) => voiceGain = value;
public void SetVoiceDistanceNear(float value) => voiceDistanceNear = value;
public void SetVoiceDistanceFar(float value) => voiceDistanceFar = value;
public void SetVoiceVolumetricRadius(float value) => voiceVolumetricRadius = value;
public void SetVoiceLowpass(bool value) => voiceLowpass = value;
public float GetVoiceGain() => voiceGain;
public float GetVoiceDistanceNear() => voiceDistanceNear;
public float GetVoiceDistanceFar() => voiceDistanceFar;
public float GetVoiceVolumetricRadius() => voiceVolumetricRadius;
public bool GetVoiceLowpass() => voiceLowpass;
#endregion
#region Avatar Audio
public void SetAvatarAudioVolumetricRadius(float value)
{
avatarAudioVolumetricRadius = value;
}
public void SetAvatarAudioNearRadius(float value)
{
avatarAudioNearRadius = value;
}
public void SetAvatarAudioFarRadius(float value)
{
avatarAudioFarRadius = value;
}
public void SetAvatarAudioGain(float value)
{
avatarAudioGain = value;
}
public void SetAvatarAudioForceSpatial(bool value)
{
avatarAudioForceSpatial = value;
}
public void SetAvatarAudioCustomCurve(bool value)
{
avatarAudioCustomCurve = value;
}
#endregion
}
}

View File

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

View File

@ -0,0 +1,89 @@
using System;
namespace VRC.SDK3.ClientSim
{
[Serializable]
public class ClientSimPlayerLocomotionData : IClientSimPlayerLocomotionData
{
private const float DEFAULT_RUN_SPEED = 4;
private const float DEFAULT_WALK_SPEED = 2;
public float walkSpeed = DEFAULT_WALK_SPEED;
public float strafeSpeed = DEFAULT_WALK_SPEED;
public float runSpeed = DEFAULT_RUN_SPEED;
public float jumpSpeed;
public float gravityStrength = 1f;
public bool immobilized = false;
public bool useLegacyLocomotion = false;
public float GetJump()
{
return jumpSpeed;
}
public void SetJump(float value)
{
jumpSpeed = value;
}
public float GetRunSpeed()
{
return runSpeed;
}
public void SetRunSpeed(float value)
{
runSpeed = value;
}
public float GetWalkSpeed()
{
return walkSpeed;
}
public void SetWalkSpeed(float value)
{
walkSpeed = value;
}
public float GetStrafeSpeed()
{
return strafeSpeed;
}
public void SetStrafeSpeed(float value)
{
strafeSpeed = value;
}
public float GetGravityStrength()
{
return gravityStrength;
}
public void SetGravityStrength(float value)
{
gravityStrength = value;
}
public bool GetImmobilized()
{
return immobilized;
}
public void SetImmobilized(bool value)
{
immobilized = value;
}
public void SetUseLegacyLocomotion(bool value)
{
useLegacyLocomotion = value;
}
public bool GetUseLegacyLocomotion()
{
return useLegacyLocomotion;
}
}
}

View File

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

View File

@ -0,0 +1,49 @@
using System;
using VRC.SDKBase;
namespace VRC.SDK3.ClientSim
{
[Serializable]
public class ClientSimPlayerPickupData : IClientSimPlayerPickupData
{
public bool pickupsEnabled = true;
private VRC_Pickup _leftHandPickup;
private VRC_Pickup _rightHandPickup;
public void SetPickupsEnabled(bool enabled)
{
pickupsEnabled = enabled;
}
public bool GetPickupsEnabled()
{
return pickupsEnabled;
}
public VRC_Pickup GetPickupInHand(VRC_Pickup.PickupHand hand)
{
switch (hand)
{
case VRC_Pickup.PickupHand.Left:
return _leftHandPickup;
case VRC_Pickup.PickupHand.Right:
return _rightHandPickup;
default:
return null;
}
}
public void SetPickupInHand(VRC_Pickup.PickupHand hand, VRC_Pickup pickup)
{
switch (hand)
{
case VRC_Pickup.PickupHand.Left:
_leftHandPickup = pickup;
break;
case VRC_Pickup.PickupHand.Right:
_rightHandPickup = pickup;
break;
}
}
}
}

View File

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

View File

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
namespace VRC.SDK3.ClientSim
{
[Serializable]
public class ClientSimPlayerTagsData : IClientSimPlayerTagsData
{
private readonly Dictionary<string, string> _tags = new Dictionary<string, string>();
public void ClearPlayerTags()
{
_tags.Clear();
}
public void SetPlayerTag(string tagName, string tagValue)
{
_tags[tagName] = tagValue;
}
public string GetPlayerTag(string tagName)
{
if (_tags.TryGetValue(tagName, out string tagValue))
{
return tagValue;
}
return "";
}
public bool HasPlayerTag(string tagName, string tagValue)
{
return GetPlayerTag(tagName) == tagValue;
}
}
}

View File

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

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: dc69316ead1241668e91076f6d8027dd
timeCreated: 1708638333

View File

@ -0,0 +1,406 @@
#if VRC_ENABLE_PLAYER_PERSISTENCE
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEngine;
namespace VRC.SDK3.ClientSim.Persistence
{
public class ClientSimPlayerDataConverter : JsonConverter<ClientSimPlayerDataTypeUnion>
{
public override ClientSimPlayerDataTypeUnion ReadJson(JsonReader reader, Type objectType, ClientSimPlayerDataTypeUnion existingValue, bool hasExistingValue, JsonSerializer serializer)
{
JObject jsonObject = JObject.Load(reader);
if (jsonObject["type"] == null)
{
Debug.LogError("Failed to deserialize PlayerData: No type found");
return null;
}
if (jsonObject["value"] == null)
{
Debug.LogError("Failed to deserialize PlayerData: No value found");
return null;
}
try
{
ClientSimPlayerDataType type = GetPlayerDataType(jsonObject["type"].ToString());
object value = GetTypedValue(type, jsonObject["value"], reader, existingValue, serializer);
return new ClientSimPlayerDataTypeUnion { Type = type, Value = value };
}
catch (Exception e)
{
Debug.LogError($"Failed to parse PlayerData {objectType} : {e.Message}");
return default;
}
}
private ClientSimPlayerDataType GetPlayerDataType(string typeName)
{
return typeName switch
{
"Color" => ClientSimPlayerDataType.Color,
"Color32" => ClientSimPlayerDataType.Color32,
"Quaternion" => ClientSimPlayerDataType.Quaternion,
"Vector2" => ClientSimPlayerDataType.Vector2,
"Vector3" => ClientSimPlayerDataType.Vector3,
"Vector4" => ClientSimPlayerDataType.Vector4,
"Bool" => ClientSimPlayerDataType.WrappedBool,
"UByte" => ClientSimPlayerDataType.WrappedUByte,
"Byte" => ClientSimPlayerDataType.WrappedByte,
"Bytes" => ClientSimPlayerDataType.WrappedBytes,
"Float" => ClientSimPlayerDataType.WrappedFloat,
"Double" => ClientSimPlayerDataType.WrappedDouble,
"Long" => ClientSimPlayerDataType.WrappedLong,
"ULong" => ClientSimPlayerDataType.WrappedULong,
"Int" => ClientSimPlayerDataType.WrappedInt,
"UInt" => ClientSimPlayerDataType.WrappedUInt,
"Short" => ClientSimPlayerDataType.WrappedShort,
"UShort" => ClientSimPlayerDataType.WrappedUShort,
"String" => ClientSimPlayerDataType.WrappedString,
_ => ClientSimPlayerDataType.Color
};
}
public override void WriteJson(JsonWriter writer, ClientSimPlayerDataTypeUnion value, JsonSerializer serializer)
{
JsonConverter converter = null;
if (value.Type == ClientSimPlayerDataType.Color) converter = new ColorConverter();
else if (value.Type == ClientSimPlayerDataType.Color32) converter = new Color32Converter();
else if (value.Type == ClientSimPlayerDataType.Quaternion) converter = new QuaternionConverter();
else if (value.Type == ClientSimPlayerDataType.Vector2) converter = new Vector2Converter();
else if (value.Type == ClientSimPlayerDataType.Vector3) converter = new Vector3Converter();
else if (value.Type == ClientSimPlayerDataType.Vector4) converter = new Vector4Converter();
writer.WriteStartObject();
writer.WritePropertyName("type");
writer.WriteValue(value.Type.ToString().Replace("Wrapped", ""));
writer.WritePropertyName("value");
if (converter != null)
{
converter.WriteJson(writer, value.Value, serializer);
}
else
{
writer.WriteValue(value.Value);
}
writer.WriteEndObject();
}
private static object GetTypedValue(ClientSimPlayerDataType type, object deserializedValue, JsonReader reader, ClientSimPlayerDataTypeUnion existingValue, JsonSerializer serializer)
{
switch (type)
{
case ClientSimPlayerDataType.Color:
ColorUtility.TryParseHtmlString("#" + deserializedValue, out Color loadedColor);
return loadedColor;
case ClientSimPlayerDataType.Color32:
ColorUtility.TryParseHtmlString("#" + deserializedValue, out Color loadedColor32);
return new Color32(
(byte)(loadedColor32.r * 255),
(byte)(loadedColor32.g * 255),
(byte)(loadedColor32.b * 255),
(byte)(loadedColor32.a * 255));
case ClientSimPlayerDataType.Quaternion:
JObject quaternionObj = (JObject)deserializedValue;
return new Quaternion(
(float)quaternionObj["x"],
(float)quaternionObj["y"],
(float)quaternionObj["z"],
(float)quaternionObj["w"]);
case ClientSimPlayerDataType.Vector2:
JObject vector2Obj = (JObject)deserializedValue;
return new Vector2(
(float)vector2Obj["x"],
(float)vector2Obj["y"]);
case ClientSimPlayerDataType.Vector3:
JObject vector3Obj = (JObject)deserializedValue;
return new Vector3(
(float)vector3Obj["x"],
(float)vector3Obj["y"],
(float)vector3Obj["z"]);
case ClientSimPlayerDataType.Vector4:
JObject vector4Obj = (JObject)deserializedValue;
return new Vector4(
(float)vector4Obj["x"],
(float)vector4Obj["y"],
(float)vector4Obj["z"],
(float)vector4Obj["w"]);
case ClientSimPlayerDataType.WrappedBytes:
JValue byteArrayObj = (JValue)deserializedValue;
return Convert.FromBase64String((string)byteArrayObj.Value ?? string.Empty);
case ClientSimPlayerDataType.WrappedBool: return (bool)(JValue)deserializedValue;
case ClientSimPlayerDataType.WrappedUByte: return (byte)(JValue)deserializedValue;
case ClientSimPlayerDataType.WrappedByte: return (sbyte)(JValue)deserializedValue;
case ClientSimPlayerDataType.WrappedFloat: return (float)(JValue)deserializedValue;
case ClientSimPlayerDataType.WrappedDouble: return (double)(JValue)deserializedValue;
case ClientSimPlayerDataType.WrappedLong: return (long)(JValue)deserializedValue;
case ClientSimPlayerDataType.WrappedULong: return (ulong)(JValue)deserializedValue;
case ClientSimPlayerDataType.WrappedInt: return (int)(JValue)deserializedValue;
case ClientSimPlayerDataType.WrappedUInt: return (uint)(JValue)deserializedValue;
case ClientSimPlayerDataType.WrappedShort: return (short)(JValue)deserializedValue;
case ClientSimPlayerDataType.WrappedUShort: return (ushort)(JValue)deserializedValue;
case ClientSimPlayerDataType.WrappedString: return (string)(JValue)deserializedValue;
default: return deserializedValue;
}
}
}
internal class ColorConverter : JsonConverter<Color>
{
public override Color ReadJson(JsonReader reader, Type objectType, Color existingValue, bool hasExistingValue, JsonSerializer serializer)
{
try
{
ColorUtility.TryParseHtmlString("#" + reader.Value, out Color loadedColor);
return loadedColor;
}
catch (Exception ex)
{
Debug.LogError($"Failed to parse color {objectType} : {ex.Message}");
return default;
}
}
public override void WriteJson(JsonWriter writer, Color color, JsonSerializer serializer)
{
string val = ColorUtility.ToHtmlStringRGBA(color);
writer.WriteValue(val);
}
}
internal class Color32Converter : JsonConverter<Color32>
{
public override Color32 ReadJson(JsonReader reader, Type objectType, Color32 existingValue, bool hasExistingValue, JsonSerializer serializer)
{
try
{
ColorUtility.TryParseHtmlString("#" + reader.Value, out Color loadedColor);
return loadedColor;
}
catch (Exception ex)
{
Debug.LogError($"Failed to parse color {objectType} : {ex.Message}");
return default;
}
}
public override void WriteJson(JsonWriter writer, Color32 color, JsonSerializer serializer)
{
string val = ColorUtility.ToHtmlStringRGBA(color);
writer.WriteValue(val);
}
}
internal class QuaternionConverter : JsonConverter<Quaternion>
{
public override Quaternion ReadJson(JsonReader reader, Type objectType, Quaternion existingValue, bool hasExistingValue, JsonSerializer serializer)
{
try
{
Debug.Log($"Quaternion: type={objectType} ({existingValue.GetType()}), val={existingValue}");
float x = 0f, y = 0f, z = 0f, w = 0f;
while (reader.Read())
{
string propertyName = (string)reader.Value;
reader.Read();
switch (propertyName)
{
case "x":
x = Convert.ToSingle(reader.Value);
break;
case "y":
y = Convert.ToSingle(reader.Value);
break;
case "z":
z = Convert.ToSingle(reader.Value);
break;
case "w":
w = Convert.ToSingle(reader.Value);
break;
}
}
return new Quaternion(x, y, z, w);
}
catch (Exception ex)
{
Debug.LogError($"Failed to parse quaternion {objectType} : {ex.Message}");
return default;
}
}
public override void WriteJson(JsonWriter writer, Quaternion value, JsonSerializer serializer)
{
writer.WriteStartObject();
writer.WritePropertyName("x");
writer.WriteValue(value.x);
writer.WritePropertyName("y");
writer.WriteValue(value.y);
writer.WritePropertyName("z");
writer.WriteValue(value.z);
writer.WritePropertyName("w");
writer.WriteValue(value.w);
writer.WriteEndObject();
}
}
public class Vector2Converter : JsonConverter<Vector2>
{
public override Vector2 ReadJson(JsonReader reader, Type objectType, Vector2 existingValue, bool hasExistingValue, JsonSerializer serializer)
{
try
{
float x = 0f, y = 0f;
while (reader.Read())
{
string propertyName = (string)reader.Value;
reader.Read();
switch (propertyName)
{
case "x":
x = Convert.ToSingle(reader.Value);
break;
case "y":
y = Convert.ToSingle(reader.Value);
break;
}
}
return new Vector2(x, y);
}
catch (Exception ex)
{
Debug.LogError($"Failed to parse Vector2 {objectType} : {ex.Message}");
return default;
}
}
public override void WriteJson(JsonWriter writer, Vector2 value, JsonSerializer serializer)
{
writer.WriteStartObject();
writer.WritePropertyName("x");
writer.WriteValue(value.x);
writer.WritePropertyName("y");
writer.WriteValue(value.y);
writer.WriteEndObject();
}
}
public class Vector3Converter : JsonConverter<Vector3>
{
public override Vector3 ReadJson(JsonReader reader, Type objectType, Vector3 existingValue, bool hasExistingValue, JsonSerializer serializer)
{
try
{
float x = 0f, y = 0f, z = 0f;
while (reader.Read())
{
string propertyName = (string)reader.Value;
reader.Read();
switch (propertyName)
{
case "x":
x = Convert.ToSingle(reader.Value);
break;
case "y":
y = Convert.ToSingle(reader.Value);
break;
case "z":
z = Convert.ToSingle(reader.Value);
break;
}
}
return new Vector3(x, y, z);
}
catch (Exception ex)
{
Debug.LogError($"Failed to parse Vector3 {objectType} : {ex.Message}");
return default;
}
}
public override void WriteJson(JsonWriter writer, Vector3 value, JsonSerializer serializer)
{
writer.WriteStartObject();
writer.WritePropertyName("x");
writer.WriteValue(value.x);
writer.WritePropertyName("y");
writer.WriteValue(value.y);
writer.WritePropertyName("z");
writer.WriteValue(value.z);
writer.WriteEndObject();
}
}
public class Vector4Converter : JsonConverter<Vector4>
{
public override Vector4 ReadJson(JsonReader reader, Type objectType, Vector4 existingValue, bool hasExistingValue, JsonSerializer serializer)
{
try
{
float x = 0f, y = 0f, z = 0f, w = 0f;
while (reader.Read())
{
string propertyName = (string)reader.Value;
reader.Read();
switch (propertyName)
{
case "x":
x = Convert.ToSingle(reader.Value);
break;
case "y":
y = Convert.ToSingle(reader.Value);
break;
case "z":
z = Convert.ToSingle(reader.Value);
break;
case "w":
w = Convert.ToSingle(reader.Value);
break;
}
}
return new Vector4(x, y, z, w);
}
catch (Exception ex)
{
Debug.LogError($"Failed to parse Vector4 {objectType} : {ex.Message}");
return default;
}
}
public override void WriteJson(JsonWriter writer, Vector4 value, JsonSerializer serializer)
{
writer.WriteStartObject();
writer.WritePropertyName("x");
writer.WriteValue(value.x);
writer.WritePropertyName("y");
writer.WriteValue(value.y);
writer.WritePropertyName("z");
writer.WriteValue(value.z);
writer.WritePropertyName("w");
writer.WriteValue(value.w);
writer.WriteEndObject();
}
}
}
#endif

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d47ee12e2ac84367b7e3ced0dc76be28
timeCreated: 1708717453

View File

@ -0,0 +1,20 @@
using System;
#if VRC_ENABLE_PLAYER_PERSISTENCE
namespace VRC.SDK3.ClientSim.Persistence
{
public class ClientSimPlayerDataPair
{
public string Key { get; set; }
public ClientSimPlayerDataTypeUnion Value { get; set; }
public DateTime LastUpdated;
public ClientSimPlayerDataPair() {
Key = null;
Value = null;
LastUpdated = DateTime.Now;
}
}
}
#endif

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7da7fcefc4434d3e9b5e16edf46de990
timeCreated: 1708638443

View File

@ -0,0 +1,884 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
using UnityEngine;
using VRC.SDKBase;
#if VRC_ENABLE_PLAYER_PERSISTENCE
using UnityEngine.SceneManagement;
using VRC.SDK3.Data;
using VRC.SDK3.Persistence;
using VRC.Udon;
#endif
namespace VRC.SDK3.ClientSim.Persistence
{
[AddComponentMenu("")] // hides component in Add Component menu
public class ClientSimPlayerDataStorage : ClientSimBehaviour
{
#if VRC_ENABLE_PLAYER_PERSISTENCE
public static string PlayerDataFolder => Path.Combine("ClientSimStorage", "PlayerData");
internal static string PlayerDataFilePath(VRCPlayerApi player)
{
string root = Path.GetDirectoryName(Application.dataPath);
string path = Path.Combine(root, PlayerDataFolder);
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
return path + "/PlayerData_" + $"{player.playerId}" + $"_{SceneManager.GetActiveScene().name}" + ".json";
}
private VRCPlayerApi _player;
private IClientSimUdonEventSender _udonEventSender;
private IClientSimEventDispatcher _eventDispatcher;
private readonly Dictionary<string, ClientSimPlayerDataPair> leData = new();
private readonly Dictionary<string, PlayerData.Info> localInfoChanges = new();
private readonly List<PlayerData.Info> queuedPlayerDataUpdates = new();
private bool doDecode;
private bool isDoneDecoding;
private bool hasPostedPlayerDataDecoded;
private bool hasPostedPlayerRestored;
public int Size
=> System.Text.Encoding.UTF8.GetByteCount(JsonConvert.SerializeObject(leData, Formatting.Indented, new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
}));
public IEnumerable<string> GetKeys()
{
return leData.Keys;
}
public bool HasKey(string key) => leData.ContainsKey(key);
public Type GetType(string key)
{
if (!leData.TryGetValue(key, out ClientSimPlayerDataPair value))
return null;
switch (value.Value.Type)
{
default:
case ClientSimPlayerDataType.None:
return null;
case ClientSimPlayerDataType.Vector2:
return typeof(Vector2);
case ClientSimPlayerDataType.Vector3:
return typeof(Vector3);
case ClientSimPlayerDataType.Vector4:
return typeof(Vector4);
case ClientSimPlayerDataType.Quaternion:
return typeof(Quaternion);
case ClientSimPlayerDataType.Color:
return typeof(Color);
case ClientSimPlayerDataType.Color32:
return typeof(Color32);
case ClientSimPlayerDataType.WrappedString:
return typeof(string);
case ClientSimPlayerDataType.WrappedShort:
return typeof(short);
case ClientSimPlayerDataType.WrappedUShort:
return typeof(ushort);
case ClientSimPlayerDataType.WrappedInt:
return typeof(int);
case ClientSimPlayerDataType.WrappedUInt:
return typeof(uint);
case ClientSimPlayerDataType.WrappedLong:
return typeof(long);
case ClientSimPlayerDataType.WrappedULong:
return typeof(ulong);
case ClientSimPlayerDataType.WrappedFloat:
return typeof(float);
case ClientSimPlayerDataType.WrappedDouble:
return typeof(double);
case ClientSimPlayerDataType.WrappedBool:
return typeof(bool);
case ClientSimPlayerDataType.WrappedByte:
return typeof(sbyte);
case ClientSimPlayerDataType.WrappedUByte:
return typeof(byte);
case ClientSimPlayerDataType.WrappedBytes:
return typeof(byte[]);
}
}
#region Setters
private PlayerData.State _SetWrapper(string key, Func<ClientSimPlayerDataPair, bool> set, bool isRestore, bool flushChanges, DateTime lastUpdated)
{
if (string.IsNullOrWhiteSpace(key))
throw new ArgumentException("Key was invalid");
if (!isRestore && leData.TryGetValue(key, out ClientSimPlayerDataPair value))
{
if (set(value))
{
value.LastUpdated = DateTime.Now;
localInfoChanges[key] = new PlayerData.Info(key, PlayerData.State.Changed);
if (flushChanges)
{
FlushLocalInfoChanges();
}
return PlayerData.State.Changed;
}
return PlayerData.State.Unchanged;
}
value = new ClientSimPlayerDataPair { Key = key, LastUpdated = isRestore ? lastUpdated : DateTime.Now };
leData[key] = value;
set(value);
var state = isRestore ? PlayerData.State.Restored : PlayerData.State.Added;
localInfoChanges[key] = new PlayerData.Info(key, state);
if (flushChanges)
{
FlushLocalInfoChanges();
}
return state;
}
public PlayerData.State SetBool(string key, bool value, DateTime lastUpdated = new DateTime(), bool isRestore = false, bool flushChanges = true)
{
return _SetWrapper(key, Set, isRestore, flushChanges, lastUpdated);
bool Set(ClientSimPlayerDataPair pair)
{
if (pair.Value?.Type == ClientSimPlayerDataType.WrappedBool)
{
if (pair.Value.AsWrappedBool() == value)
return false;
pair.Value.Value = value;
}
else
pair.Value = new ClientSimPlayerDataTypeUnion()
{
Type = ClientSimPlayerDataType.WrappedBool,
Value = value
};
return true;
}
}
public PlayerData.State SetByte(string key, byte value, DateTime lastUpdated = new DateTime(), bool isRestore = false, bool flushChanges = true)
{
return _SetWrapper(key, Set, isRestore, flushChanges, lastUpdated);
bool Set(ClientSimPlayerDataPair pair)
{
if (pair.Value?.Type == ClientSimPlayerDataType.WrappedUByte)
{
if (pair.Value.AsWrappedUByte() == value)
return false;
pair.Value.Value = value;
}
else
pair.Value = new ClientSimPlayerDataTypeUnion()
{
Type = ClientSimPlayerDataType.WrappedUByte,
Value = value
};
return true;
}
}
public PlayerData.State SetSByte(string key, sbyte value, DateTime lastUpdated = new DateTime(), bool isRestore = false, bool flushChanges = true)
{
return _SetWrapper(key, Set, isRestore, flushChanges, lastUpdated);
bool Set(ClientSimPlayerDataPair pair)
{
if (pair.Value?.Type == ClientSimPlayerDataType.WrappedByte)
{
if (pair.Value.AsWrappedSByte() == value)
return false;
pair.Value.Value = value;
}
else
pair.Value = new ClientSimPlayerDataTypeUnion()
{
Type = ClientSimPlayerDataType.WrappedByte,
Value = value
};
return true;
}
}
public PlayerData.State SetBytes(string key, byte[] value, DateTime lastUpdated = new DateTime(), bool isRestore = false, bool flushChanges = true)
{
return _SetWrapper(key, Set, isRestore, flushChanges, lastUpdated);
bool Set(ClientSimPlayerDataPair pair)
{
if (pair.Value?.Type == ClientSimPlayerDataType.WrappedBytes)
{
if (pair.Value.AsWrappedBytes().SequenceEqual(value))
return false;
pair.Value.Value = value.ToArray();
}
else
pair.Value = new ClientSimPlayerDataTypeUnion()
{
Type = ClientSimPlayerDataType.WrappedBytes,
Value = value.ToArray()
};
return true;
}
}
public PlayerData.State SetString(string key, string value, DateTime lastUpdated = new DateTime(), bool isRestore = false, bool flushChanges = true)
{
return _SetWrapper(key, Set, isRestore, flushChanges, lastUpdated);
bool Set(ClientSimPlayerDataPair pair)
{
if (pair.Value?.Type == ClientSimPlayerDataType.WrappedString)
{
if (pair.Value.AsWrappedString() == value)
return false;
pair.Value.Value = value;
}
else
pair.Value = new ClientSimPlayerDataTypeUnion()
{
Type = ClientSimPlayerDataType.WrappedString,
Value = value
};
return true;
}
}
public PlayerData.State SetShort(string key, short value, DateTime lastUpdated = new DateTime(), bool isRestore = false, bool flushChanges = true)
{
return _SetWrapper(key, Set, isRestore, flushChanges, lastUpdated);
bool Set(ClientSimPlayerDataPair pair)
{
if (pair.Value?.Type == ClientSimPlayerDataType.WrappedShort)
{
if (pair.Value.AsWrappedShort() == value)
return false;
pair.Value.Value = value;
}
else
pair.Value = new ClientSimPlayerDataTypeUnion()
{
Type = ClientSimPlayerDataType.WrappedShort,
Value = value
};
return true;
}
}
public PlayerData.State SetUShort(string key, ushort value, DateTime lastUpdated = new DateTime(), bool isRestore = false, bool flushChanges = true)
{
return _SetWrapper(key, Set, isRestore, flushChanges, lastUpdated);
bool Set(ClientSimPlayerDataPair pair)
{
if (pair.Value?.Type == ClientSimPlayerDataType.WrappedUShort)
{
if (pair.Value.AsWrappedUShort() == value)
return false;
pair.Value.Value = value;
}
else
pair.Value = new ClientSimPlayerDataTypeUnion()
{
Type = ClientSimPlayerDataType.WrappedUShort,
Value = value
};
return true;
}
}
public PlayerData.State SetInt(string key, int value, DateTime lastUpdated = new DateTime(), bool isRestore = false, bool flushChanges = true)
{
return _SetWrapper(key, Set, isRestore, flushChanges, lastUpdated);
bool Set(ClientSimPlayerDataPair pair)
{
if (pair.Value?.Type == ClientSimPlayerDataType.WrappedInt)
{
if (pair.Value.AsWrappedInt() == value)
return false;
pair.Value.Value = value;
}
else
pair.Value = new ClientSimPlayerDataTypeUnion()
{
Type = ClientSimPlayerDataType.WrappedInt,
Value = value
};
return true;
}
}
public PlayerData.State SetUInt(string key, uint value, DateTime lastUpdated = new DateTime(), bool isRestore = false, bool flushChanges = true)
{
return _SetWrapper(key, Set, isRestore, flushChanges, lastUpdated);
bool Set(ClientSimPlayerDataPair pair)
{
if (pair.Value?.Type == ClientSimPlayerDataType.WrappedUInt)
{
if (pair.Value.AsWrappedUInt() == value)
return false;
pair.Value.Value = value;
}
else
pair.Value = new ClientSimPlayerDataTypeUnion()
{
Type = ClientSimPlayerDataType.WrappedUInt,
Value = value
};
return true;
}
}
public PlayerData.State SetLong(string key, long value, DateTime lastUpdated = new DateTime(), bool isRestore = false, bool flushChanges = true)
{
return _SetWrapper(key, Set, isRestore, flushChanges, lastUpdated);
bool Set(ClientSimPlayerDataPair pair)
{
if (pair.Value?.Type == ClientSimPlayerDataType.WrappedLong)
{
if (pair.Value.AsWrappedLong() == value)
return false;
pair.Value.Value = value;
}
else
pair.Value = new ClientSimPlayerDataTypeUnion()
{
Type = ClientSimPlayerDataType.WrappedLong,
Value = value
};
return true;
}
}
public PlayerData.State SetULong(string key, ulong value, DateTime lastUpdated = new DateTime(), bool isRestore = false, bool flushChanges = true)
{
return _SetWrapper(key, Set, isRestore, flushChanges, lastUpdated);
bool Set(ClientSimPlayerDataPair pair)
{
if (pair.Value?.Type == ClientSimPlayerDataType.WrappedULong)
{
if (pair.Value.AsWrappedULong() == value)
return false;
pair.Value.Value = value;
}
else
pair.Value = new ClientSimPlayerDataTypeUnion()
{
Type = ClientSimPlayerDataType.WrappedULong,
Value = value
};
return true;
}
}
public PlayerData.State SetFloat(string key, float value, DateTime lastUpdated = new DateTime(), bool isRestore = false, bool flushChanges = true)
{
return _SetWrapper(key, Set, isRestore, flushChanges, lastUpdated);
bool Set(ClientSimPlayerDataPair pair)
{
if (pair.Value?.Type == ClientSimPlayerDataType.WrappedFloat)
{
if (Math.Abs(pair.Value.AsWrappedFloat() - value) < 0.000001f)
return false;
pair.Value.Value = value;
}
else
pair.Value = new ClientSimPlayerDataTypeUnion()
{
Type = ClientSimPlayerDataType.WrappedFloat,
Value = value
};
return true;
}
}
public PlayerData.State SetDouble(string key, double value, DateTime lastUpdated = new DateTime(), bool isRestore = false, bool flushChanges = true)
{
return _SetWrapper(key, Set, isRestore, flushChanges, lastUpdated);
bool Set(ClientSimPlayerDataPair pair)
{
if (pair.Value?.Type == ClientSimPlayerDataType.WrappedDouble)
{
if (Math.Abs(pair.Value.AsWrappedDouble() - value) < 0.000001)
return false;
pair.Value.Value = value;
}
else
pair.Value = new ClientSimPlayerDataTypeUnion()
{
Type = ClientSimPlayerDataType.WrappedDouble,
Value = value
};
return true;
}
}
public PlayerData.State SetVector2(string key, Vector2 value, DateTime lastUpdated = new DateTime(), bool isRestore = false, bool flushChanges = true)
{
return _SetWrapper(key, Set, isRestore, flushChanges, lastUpdated);
bool Set(ClientSimPlayerDataPair pair)
{
if (pair.Value?.Type == ClientSimPlayerDataType.Vector2)
{
if (Math.Abs(pair.Value.AsVector2().x - value.x) < 0.000001f
&& Math.Abs(pair.Value.AsVector2().y - value.y) < 0.000001f)
return false;
pair.Value.Value = value;
}
else
pair.Value = new ClientSimPlayerDataTypeUnion()
{
Type = ClientSimPlayerDataType.Vector2,
Value = value
};
return true;
}
}
public PlayerData.State SetVector3(string key, Vector3 value, DateTime lastUpdated = new DateTime(), bool isRestore = false, bool flushChanges = true)
{
return _SetWrapper(key, Set, isRestore, flushChanges, lastUpdated);
bool Set(ClientSimPlayerDataPair pair)
{
if (pair.Value?.Type == ClientSimPlayerDataType.Vector3)
{
if (Math.Abs(pair.Value.AsVector3().x - value.x) < 0.000001f
&& Math.Abs(pair.Value.AsVector3().y - value.y) < 0.000001f
&& Math.Abs(pair.Value.AsVector3().z - value.z) < 0.000001f)
return false;
pair.Value.Value = value;
}
else
pair.Value = new ClientSimPlayerDataTypeUnion()
{
Type = ClientSimPlayerDataType.Vector3,
Value = value
};
return true;
}
}
public PlayerData.State SetVector4(string key, Vector4 value, DateTime lastUpdated = new DateTime(), bool isRestore = false, bool flushChanges = true)
{
return _SetWrapper(key, Set, isRestore, flushChanges, lastUpdated);
bool Set(ClientSimPlayerDataPair pair)
{
if (pair.Value?.Type == ClientSimPlayerDataType.Vector4)
{
if (Math.Abs(pair.Value.AsVector4().x - value.x) < 0.000001f
&& Math.Abs(pair.Value.AsVector4().y - value.y) < 0.000001f
&& Math.Abs(pair.Value.AsVector4().z - value.z) < 0.000001f
&& Math.Abs(pair.Value.AsVector4().w - value.w) < 0.000001f)
return false;
pair.Value.Value = value;
}
else
pair.Value = new ClientSimPlayerDataTypeUnion()
{
Type = ClientSimPlayerDataType.Vector4,
Value = value
};
return true;
}
}
public PlayerData.State SetQuaternion(string key, Quaternion value, DateTime lastUpdated = new DateTime(), bool isRestore = false, bool flushChanges = true)
{
return _SetWrapper(key, Set, isRestore, flushChanges, lastUpdated);
bool Set(ClientSimPlayerDataPair pair)
{
if (pair.Value?.Type == ClientSimPlayerDataType.Quaternion)
{
if (Math.Abs(pair.Value.AsQuaternion().x - value.x) < 0.000001f
&& Math.Abs(pair.Value.AsQuaternion().y - value.y) < 0.000001f
&& Math.Abs(pair.Value.AsQuaternion().z - value.z) < 0.000001f
&& Math.Abs(pair.Value.AsQuaternion().w - value.w) < 0.000001f)
return false;
pair.Value.Value = value;
}
else
pair.Value = new ClientSimPlayerDataTypeUnion()
{
Type = ClientSimPlayerDataType.Quaternion,
Value = value
};
return true;
}
}
public PlayerData.State SetColor(string key, Color value, DateTime lastUpdated = new DateTime(), bool isRestore = false, bool flushChanges = true)
{
return _SetWrapper(key, Set, isRestore, flushChanges, lastUpdated);
bool Set(ClientSimPlayerDataPair pair)
{
if (pair.Value?.Type == ClientSimPlayerDataType.Color)
{
var color = pair.Value.AsColor();
if (Math.Abs(color.r - value.r) < 0.01f
&& Math.Abs(color.g - value.g) < 0.01f
&& Math.Abs(color.b - value.b) < 0.01f
&& Math.Abs(color.a - value.a) < 0.01f)
return false;
pair.Value.Value = value;
}
else
pair.Value = new ClientSimPlayerDataTypeUnion()
{
Type = ClientSimPlayerDataType.Color,
Value = value
};
return true;
}
}
public PlayerData.State SetColor32(string key, Color32 value, DateTime lastUpdated = new DateTime(), bool isRestore = false, bool flushChanges = true)
{
return _SetWrapper(key, Set, isRestore, flushChanges, lastUpdated);
bool Set(ClientSimPlayerDataPair pair)
{
if (pair.Value?.Type == ClientSimPlayerDataType.Color32)
{
var color32 = pair.Value.AsColor32();
if (Math.Abs(color32.r - value.r) < 0.000001f
&& Math.Abs(color32.g - value.g) < 0.000001f
&& Math.Abs(color32.b - value.b) < 0.000001f
&& Math.Abs(color32.a - value.a) < 0.000001f)
return false;
pair.Value.Value = value;
}
else
pair.Value = new ClientSimPlayerDataTypeUnion()
{
Type = ClientSimPlayerDataType.Color32,
Value = value
};
return true;
}
}
#endregion
#region Getters
private ClientSimPlayerDataPair _GetChecked(string key, ClientSimPlayerDataType expectedType)
{
if (string.IsNullOrWhiteSpace(key))
throw new ArgumentException("Key was invalid");
if (!leData.TryGetValue(key, out ClientSimPlayerDataPair data) || data.Value == null)
{
return null;
}
if (data.Value.Type != expectedType)
{
this.LogError($"Data at {key} was not a {expectedType}, it was a {data.Value.Type}");
return null;
}
return data;
}
public bool GetBool(string key)
=> _GetChecked(key, ClientSimPlayerDataType.WrappedBool)?.Value.AsWrappedBool() ?? default;
public byte GetByte(string key)
=> _GetChecked(key, ClientSimPlayerDataType.WrappedUByte)?.Value.AsWrappedUByte() ?? default;
public sbyte GetSByte(string key)
=> _GetChecked(key, ClientSimPlayerDataType.WrappedByte)?.Value.AsWrappedSByte() ?? default;
public byte[] GetBytes(string key)
=> _GetChecked(key, ClientSimPlayerDataType.WrappedBytes)?.Value.AsWrappedBytes().ToArray();
public string GetString(string key)
=> _GetChecked(key, ClientSimPlayerDataType.WrappedString)?.Value.AsWrappedString();
public short GetShort(string key)
=> _GetChecked(key, ClientSimPlayerDataType.WrappedShort)?.Value.AsWrappedShort() ?? default;
public ushort GetUShort(string key)
=> _GetChecked(key, ClientSimPlayerDataType.WrappedUShort)?.Value.AsWrappedUShort() ?? default;
public int GetInt(string key)
=> _GetChecked(key, ClientSimPlayerDataType.WrappedInt)?.Value.AsWrappedInt() ?? default;
public uint GetUInt(string key)
=> _GetChecked(key, ClientSimPlayerDataType.WrappedUInt)?.Value.AsWrappedUInt() ?? default;
public long GetLong(string key)
=> _GetChecked(key, ClientSimPlayerDataType.WrappedLong)?.Value.AsWrappedLong() ?? default;
public ulong GetULong(string key)
=> _GetChecked(key, ClientSimPlayerDataType.WrappedULong)?.Value.AsWrappedULong() ?? default;
public float GetFloat(string key)
=> _GetChecked(key, ClientSimPlayerDataType.WrappedFloat)?.Value.AsWrappedFloat() ?? default;
public double GetDouble(string key)
=> _GetChecked(key, ClientSimPlayerDataType.WrappedDouble)?.Value.AsWrappedDouble() ?? default;
public Vector2 GetVector2(string key)
{
ClientSimPlayerDataPair data = _GetChecked(key, ClientSimPlayerDataType.Vector2);
if (data != null)
return new Vector2(data.Value.AsVector2().x, data.Value.AsVector2().y);
return default;
}
public Vector3 GetVector3(string key)
{
ClientSimPlayerDataPair data = _GetChecked(key, ClientSimPlayerDataType.Vector3);
if (data != null)
return new Vector3(data.Value.AsVector3().x, data.Value.AsVector3().y, data.Value.AsVector3().z);
return default;
}
public Vector4 GetVector4(string key)
{
ClientSimPlayerDataPair data = _GetChecked(key, ClientSimPlayerDataType.Vector4);
if (data != null)
return new Vector4(data.Value.AsVector4().x, data.Value.AsVector4().y, data.Value.AsVector4().z, data.Value.AsVector4().w);
return default;
}
public Quaternion GetQuaternion(string key)
{
ClientSimPlayerDataPair data = _GetChecked(key, ClientSimPlayerDataType.Quaternion);
if (data != null)
return new Quaternion(data.Value.AsQuaternion().x, data.Value.AsQuaternion().y, data.Value.AsQuaternion().z, data.Value.AsQuaternion().w);
return default;
}
public Color GetColor(string key)
{
ClientSimPlayerDataPair data = _GetChecked(key, ClientSimPlayerDataType.Color);
if (data != null)
return new Color(data.Value.AsColor().r, data.Value.AsColor().g, data.Value.AsColor().b, data.Value.AsColor().a);
return default;
}
public Color32 GetColor32(string key)
{
ClientSimPlayerDataPair data = _GetChecked(key, ClientSimPlayerDataType.Color32);
if (data != null)
return new Color32(data.Value.AsColor32().r, data.Value.AsColor32().g, data.Value.AsColor32().b, data.Value.AsColor32().a);
return default;
}
#endregion
#region DataPropagation
// can't decode json here because this is called before scene manager is ready
public void Init(VRCPlayerApi player, IClientSimUdonEventSender udonEventSender, IClientSimEventDispatcher eventDispatcher)
{
_player = player;
_udonEventSender = udonEventSender;
_eventDispatcher = eventDispatcher;
_eventDispatcher.Subscribe<ClientSimOnPlayerJoinedEvent>(OnPlayerJoined);
_eventDispatcher.Subscribe<ClientSimOnPlayerDataClearedEvent>(OnPlayerDataCleared);
_eventDispatcher.Subscribe<ClientSimOnPlayerRestoredEvent>(OnPlayerRestored);
}
private void OnDestroy()
{
_eventDispatcher.Unsubscribe<ClientSimOnPlayerJoinedEvent>(OnPlayerJoined);
_eventDispatcher.Unsubscribe<ClientSimOnPlayerDataClearedEvent>(OnPlayerDataCleared);
_eventDispatcher.Unsubscribe<ClientSimOnPlayerRestoredEvent>(OnPlayerRestored);
}
private void OnPlayerJoined(ClientSimOnPlayerJoinedEvent payload)
{
// remote player test data is decoded via ClientSimPlayerDataWindow.OnPlayerJoined
if (payload.player.playerId == _player.playerId)
doDecode = true;
}
private void OnPlayerDataCleared(ClientSimOnPlayerDataClearedEvent payload)
{
leData.Clear();
}
private void OnPlayerRestored(ClientSimOnPlayerRestoredEvent payload)
{
if (payload.player.isLocal)
hasPostedPlayerRestored = true;
}
private void Encode()
{
// PlayerData updates before OnPlayerRestored are ignored
if (!hasPostedPlayerRestored)
return;
string json = JsonConvert.SerializeObject(leData, Formatting.Indented, new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
});
File.WriteAllText(PlayerDataFilePath(_player), json);
}
internal void Decode(bool isRestore)
{
string path = PlayerDataFilePath(_player);
if (!File.Exists(path))
{
File.WriteAllText(path, "{}");
}
else
{
try
{
string json = File.ReadAllText(path);
var data = JsonConvert.DeserializeObject<Dictionary<string, ClientSimPlayerDataPair>>(json);
foreach (KeyValuePair<string, ClientSimPlayerDataPair> kvp in data)
{
Set(kvp.Value);
}
FlushLocalInfoChanges(); // bulk flush changes
}
catch (Exception e)
{
this.LogError($"Error initializing PlayerData: {e.Message}");
}
}
isDoneDecoding = true;
PlayerData.State Set(ClientSimPlayerDataPair pair)
{
try
{
switch (pair.Value.Type)
{
case ClientSimPlayerDataType.Color: return SetColor(pair.Key, pair.Value.AsColor(), pair.LastUpdated, isRestore, false);
case ClientSimPlayerDataType.Color32: return SetColor32(pair.Key, pair.Value.AsColor32(), pair.LastUpdated, isRestore, false);
case ClientSimPlayerDataType.Quaternion: return SetQuaternion(pair.Key, pair.Value.AsQuaternion(), pair.LastUpdated, isRestore, false);
case ClientSimPlayerDataType.Vector2: return SetVector2(pair.Key, pair.Value.AsVector2(), pair.LastUpdated, isRestore, false);
case ClientSimPlayerDataType.Vector3: return SetVector3(pair.Key, pair.Value.AsVector3(), pair.LastUpdated, isRestore, false);
case ClientSimPlayerDataType.Vector4: return SetVector4(pair.Key, pair.Value.AsVector4(), pair.LastUpdated, isRestore, false);
case ClientSimPlayerDataType.WrappedBool: return SetBool(pair.Key, pair.Value.AsWrappedBool(), pair.LastUpdated, isRestore, false);
case ClientSimPlayerDataType.WrappedByte: return SetSByte(pair.Key, pair.Value.AsWrappedSByte(), pair.LastUpdated, isRestore, false);
case ClientSimPlayerDataType.WrappedUByte: return SetByte(pair.Key, pair.Value.AsWrappedByte(), pair.LastUpdated, isRestore, false);
case ClientSimPlayerDataType.WrappedBytes: return SetBytes(pair.Key, pair.Value.AsWrappedBytes(), pair.LastUpdated, isRestore, false);
case ClientSimPlayerDataType.WrappedFloat: return SetFloat(pair.Key, pair.Value.AsWrappedFloat(), pair.LastUpdated, isRestore, false);
case ClientSimPlayerDataType.WrappedDouble: return SetDouble(pair.Key, pair.Value.AsWrappedDouble(), pair.LastUpdated, isRestore, false);
case ClientSimPlayerDataType.WrappedLong: return SetLong(pair.Key, pair.Value.AsWrappedLong(), pair.LastUpdated, isRestore, false);
case ClientSimPlayerDataType.WrappedULong: return SetULong(pair.Key, pair.Value.AsWrappedULong(), pair.LastUpdated, isRestore, false);
case ClientSimPlayerDataType.WrappedInt: return SetInt(pair.Key, pair.Value.AsWrappedInt(), pair.LastUpdated, isRestore, false);
case ClientSimPlayerDataType.WrappedUInt: return SetUInt(pair.Key, pair.Value.AsWrappedUInt(), pair.LastUpdated, isRestore, false);
case ClientSimPlayerDataType.WrappedShort: return SetShort(pair.Key, pair.Value.AsWrappedShort(), pair.LastUpdated, isRestore, false);
case ClientSimPlayerDataType.WrappedUShort: return SetUShort(pair.Key, pair.Value.AsWrappedUShort(), pair.LastUpdated, isRestore, false);
case ClientSimPlayerDataType.WrappedString: return SetString(pair.Key, pair.Value.AsWrappedString(), pair.LastUpdated, isRestore, false);
default:
case ClientSimPlayerDataType.None: return PlayerData.State.Unchanged;
}
}
catch (Exception e)
{
this.LogError("Error reading PlayerData: " +
$"key={pair.Key}, " +
$"type={pair.Value.Type} ({pair.Value.Value.GetType()}), " +
$"value={pair.Value.Value}, " +
$"error={e.Message}");
return default;
}
}
}
private void FlushLocalInfoChanges()
{
var infos = leData
.Select(kvp =>
localInfoChanges.TryGetValue(kvp.Key, out var value)
? value
: new PlayerData.Info(kvp.Key, PlayerData.State.Unchanged))
.ToArray();
QueuePlayerDataUpdate(infos);
localInfoChanges.Clear();
}
internal void QueuePlayerDataUpdate(PlayerData.Info[] infos)
{
if (infos.Length == 0)
return;
queuedPlayerDataUpdates.AddRange(infos);
}
private void RaisePlayerDataUpdated(PlayerData.Info[] infos)
{
_udonEventSender.RunEvent(UdonManager.UDON_EVENT_ONPLAYERDATAUPDATED, ("player", _player), ("infos", infos));
_eventDispatcher.SendEvent(new ClientSimOnPlayerDataUpdatedEvent
{
player = _player,
playerData = leData
});
}
private float lastCheckTime = 0;
private void LateUpdate()
{
if (queuedPlayerDataUpdates.Count > 0)
{
Encode();
RaisePlayerDataUpdated(queuedPlayerDataUpdates.ToArray());
queuedPlayerDataUpdates.Clear();
}
if (doDecode)
{
doDecode = false;
Decode(true);
}
else if (isDoneDecoding && !hasPostedPlayerDataDecoded)
{
hasPostedPlayerDataDecoded = true;
_eventDispatcher.SendEvent(new ClientSimOnPlayerDataDecodedEvent { player = _player });
}
if (Time.realtimeSinceStartup - lastCheckTime > 30f)
{
lastCheckTime = Time.realtimeSinceStartup;
float dataLimit = ClientSimMain.TryGetInstance(out var instance) ? instance.GetPlayerDataUsageLimit() : 0;
float sz = Size;
if (sz >= dataLimit)
UdonManager.Instance.RunEvent(UdonManager.UDON_EVENT_ONPLAYERDATASTORAGEEXCEEDED, ("player", _player));
else if (sz >= dataLimit * 0.7f)
UdonManager.Instance.RunEvent(UdonManager.UDON_EVENT_ONPLAYERDATASTORAGEWARNING, ("player", _player));
}
}
#endregion
#endif
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0ef04bdeba52437cb909dc841044fd57
timeCreated: 1708619978

View File

@ -0,0 +1,28 @@
#if VRC_ENABLE_PLAYER_PERSISTENCE
namespace VRC.SDK3.ClientSim.Persistence
{
public enum ClientSimPlayerDataType : byte
{
None = 0,
Vector2 = 1,
Vector3 = 2,
Vector4 = 3,
Quaternion = 4,
Color = 5,
Color32 = 6,
WrappedString = 7,
WrappedShort = 8,
WrappedInt = 9,
WrappedFloat = 10,
WrappedBool = 11,
WrappedByte = 12,
WrappedBytes = 13,
WrappedUShort = 14,
WrappedUByte = 15,
WrappedUInt = 16,
WrappedULong = 17,
WrappedDouble = 18,
WrappedLong = 19,
}
}
#endif

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ee1e38bff02f4812896fb863095a29ad
timeCreated: 1708638423

View File

@ -0,0 +1,39 @@
#if VRC_ENABLE_PLAYER_PERSISTENCE
using Newtonsoft.Json;
using UnityEngine;
namespace VRC.SDK3.ClientSim.Persistence
{
[JsonConverter(typeof(ClientSimPlayerDataConverter))]
public class ClientSimPlayerDataTypeUnion
{
public object Value { get; set; }
public ClientSimPlayerDataType Type { get; set; }
public ClientSimPlayerDataTypeUnion() {
this.Type = ClientSimPlayerDataType.None;
this.Value = null;
}
public Vector2 AsVector2() => (Vector2)Value;
public Vector3 AsVector3() => (Vector3)Value;
public Vector4 AsVector4() => (Vector4)Value;
public Quaternion AsQuaternion() => (Quaternion)Value;
public Color AsColor() => (Color)Value;
public Color32 AsColor32() => (Color32)Value;
public string AsWrappedString() { return (string)Value; }
public short AsWrappedShort() { return (short)Value; }
public ushort AsWrappedUShort() { return (ushort)Value; }
public int AsWrappedInt() { return (int)Value; }
public uint AsWrappedUInt() { return (uint)Value; }
public long AsWrappedLong() { return (long)Value; }
public ulong AsWrappedULong() { return ( ulong)Value; }
public float AsWrappedFloat() { return ( float)Value; }
public double AsWrappedDouble() { return (double)Value; }
public bool AsWrappedBool() { return (bool)Value; }
public byte AsWrappedByte() { return (byte)Value ; }
public sbyte AsWrappedSByte() { return (sbyte)Value; }
public byte[] AsWrappedBytes() { return (byte[])Value; }
public byte AsWrappedUByte() { return (byte)Value; }
}
}
#endif

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 53d85c02f89642f49608b67f2788c77a
timeCreated: 1708638433

View File

@ -0,0 +1,472 @@
#if VRC_ENABLE_PLAYER_PERSISTENCE
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
using VRC.SDKBase;
namespace VRC.SDK3.ClientSim.Persistence
{
public static class ClientSimPlayerDataWrapper
{
public static void ConfigureSDK()
{
System.Func<VRCPlayerApi, IEnumerable<string>> _getKeys = GetKeys;
FieldInfo getKeysInfo = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_getKeys", BindingFlags.Static | BindingFlags.NonPublic);
getKeysInfo?.SetValue(null, _getKeys);
System.Func<VRCPlayerApi, string, bool> _hasKey = HasKey;
FieldInfo hasKeyInfo = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_hasKey", BindingFlags.Static | BindingFlags.NonPublic);
hasKeyInfo?.SetValue(null, _hasKey);
System.Func<VRCPlayerApi, string, Type> _getType = GetType;
FieldInfo getTypeInfo = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_getType", BindingFlags.Static | BindingFlags.NonPublic);
getTypeInfo?.SetValue(null, _getType);
System.Action<string, bool> _setBool = SetBool;
System.Func<VRCPlayerApi, string, bool> _getBool = GetBool;
FieldInfo setBoolInfo = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_setBool", BindingFlags.Static | BindingFlags.NonPublic);
setBoolInfo?.SetValue(null, _setBool);
FieldInfo getBoolInfo = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_getBool", BindingFlags.Static | BindingFlags.NonPublic);
getBoolInfo?.SetValue(null, _getBool);
System.Action<string, byte> _setByte = SetByte;
System.Func<VRCPlayerApi, string, byte> _getByte = GetByte;
FieldInfo setByteInfo = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_setByte", BindingFlags.Static | BindingFlags.NonPublic);
setByteInfo?.SetValue(null, _setByte);
FieldInfo getByteInfo = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_getByte", BindingFlags.Static | BindingFlags.NonPublic);
getByteInfo?.SetValue(null, _getByte);
System.Action<string, sbyte> _setUByte = SetSByte;
System.Func<VRCPlayerApi, string, sbyte> _getUByte = GetSByte;
FieldInfo setUByteInfo = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_setSByte", BindingFlags.Static | BindingFlags.NonPublic);
setUByteInfo?.SetValue(null, _setUByte);
FieldInfo getUByteInfo = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_getSByte", BindingFlags.Static | BindingFlags.NonPublic);
getUByteInfo?.SetValue(null, _getUByte);
System.Action<string, byte[]> _setBytes = SetBytes;
System.Func<VRCPlayerApi, string, byte[]> _getBytes = GetBytes;
FieldInfo setBytesInfo = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_setBytes", BindingFlags.Static | BindingFlags.NonPublic);
setBytesInfo?.SetValue(null, _setBytes);
FieldInfo getBytesInfo = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_getBytes", BindingFlags.Static | BindingFlags.NonPublic);
getBytesInfo?.SetValue(null, _getBytes);
System.Action<string, string> _setString = SetString;
System.Func<VRCPlayerApi, string, string> _getString = GetString;
FieldInfo setStringInfo = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_setString", BindingFlags.Static | BindingFlags.NonPublic);
setStringInfo?.SetValue(null, _setString);
FieldInfo getStringInfo = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_getString", BindingFlags.Static | BindingFlags.NonPublic);
getStringInfo?.SetValue(null, _getString);
System.Action<string, short> _setShort = SetShort;
System.Func<VRCPlayerApi, string, short> _getShort = GetShort;
FieldInfo setShortInfo = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_setShort", BindingFlags.Static | BindingFlags.NonPublic);
setShortInfo?.SetValue(null, _setShort);
FieldInfo getShortInfo = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_getShort", BindingFlags.Static | BindingFlags.NonPublic);
getShortInfo?.SetValue(null, _getShort);
System.Action<string, ushort> _setUShort = SetUShort;
System.Func<VRCPlayerApi, string, ushort> _getUShort = GetUShort;
FieldInfo setUShortInfo = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_setUShort", BindingFlags.Static | BindingFlags.NonPublic);
setUShortInfo?.SetValue(null, _setUShort);
FieldInfo getUShortInfo = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_getUShort", BindingFlags.Static | BindingFlags.NonPublic);
getUShortInfo?.SetValue(null, _getUShort);
System.Action<string, int> _setInt = SetInt;
System.Func<VRCPlayerApi, string, int> _getInt = GetInt;
FieldInfo setIntInfo = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_setInt", BindingFlags.Static | BindingFlags.NonPublic);
setIntInfo?.SetValue(null, _setInt);
FieldInfo getIntInfo = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_getInt", BindingFlags.Static | BindingFlags.NonPublic);
getIntInfo?.SetValue(null, _getInt);
System.Action<string, uint> _setUInt = SetUInt;
System.Func<VRCPlayerApi, string, uint> _getUInt = GetUInt;
FieldInfo setUIntInfo = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_setUInt", BindingFlags.Static | BindingFlags.NonPublic);
setUIntInfo?.SetValue(null, _setUInt);
FieldInfo getUIntInfo = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_getUInt", BindingFlags.Static | BindingFlags.NonPublic);
getUIntInfo?.SetValue(null, _getUInt);
System.Action<string, long> _setLong = SetLong;
System.Func<VRCPlayerApi, string, long> _getLong = GetLong;
FieldInfo setLongInfo = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_setLong", BindingFlags.Static | BindingFlags.NonPublic);
setLongInfo?.SetValue(null, _setLong);
FieldInfo getLongInfo = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_getLong", BindingFlags.Static | BindingFlags.NonPublic);
getLongInfo?.SetValue(null, _getLong);
System.Action<string, ulong> _setULong = SetULong;
System.Func<VRCPlayerApi, string, ulong> _getULong = GetULong;
FieldInfo setULongInfo = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_setULong", BindingFlags.Static | BindingFlags.NonPublic);
setULongInfo?.SetValue(null, _setULong);
FieldInfo getULongInfo = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_getULong", BindingFlags.Static | BindingFlags.NonPublic);
getULongInfo?.SetValue(null, _getULong);
System.Action<string, double> _setDouble = SetDouble;
System.Func<VRCPlayerApi, string, double> _getDouble = GetDouble;
FieldInfo setDoubleInfo = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_setDouble", BindingFlags.Static | BindingFlags.NonPublic);
setDoubleInfo?.SetValue(null, _setDouble);
FieldInfo getDoubleInfo = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_getDouble", BindingFlags.Static | BindingFlags.NonPublic);
getDoubleInfo?.SetValue(null, _getDouble);
System.Action<string, float> _setFloat = SetFloat;
System.Func<VRCPlayerApi, string, float> _getFloat = GetFloat;
FieldInfo setFloatInfo = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_setFloat", BindingFlags.Static | BindingFlags.NonPublic);
setFloatInfo?.SetValue(null, _setFloat);
FieldInfo getFloatInfo = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_getFloat", BindingFlags.Static | BindingFlags.NonPublic);
getFloatInfo?.SetValue(null, _getFloat);
System.Action<string, Vector2> _setVector2 = SetVector2;
System.Func<VRCPlayerApi, string, Vector2> _getVector2 = GetVector2;
FieldInfo setVector2Info = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_setVector2", BindingFlags.Static | BindingFlags.NonPublic);
setVector2Info?.SetValue(null, _setVector2);
FieldInfo getVector2Info = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_getVector2", BindingFlags.Static | BindingFlags.NonPublic);
getVector2Info?.SetValue(null, _getVector2);
System.Action<string, Vector3> _setVector3 = SetVector3;
System.Func<VRCPlayerApi, string, Vector3> _getVector3 = GetVector3;
FieldInfo setVector3Info = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_setVector3", BindingFlags.Static | BindingFlags.NonPublic);
setVector3Info?.SetValue(null, _setVector3);
FieldInfo getVector3Info = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_getVector3", BindingFlags.Static | BindingFlags.NonPublic);
getVector3Info?.SetValue(null, _getVector3);
System.Action<string, Vector4> _setVector4 = SetVector4;
System.Func<VRCPlayerApi, string, Vector4> _getVector4 = GetVector4;
FieldInfo setVector4Info = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_setVector4", BindingFlags.Static | BindingFlags.NonPublic);
setVector4Info?.SetValue(null, _setVector4);
FieldInfo getVector4Info = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_getVector4", BindingFlags.Static | BindingFlags.NonPublic);
getVector4Info?.SetValue(null, _getVector4);
System.Action<string, Quaternion> _setQuaternion = SetQuaternion;
System.Func<VRCPlayerApi, string, Quaternion> _getQuaternion = GetQuaternion;
FieldInfo setQuaternionInfo = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_setQuaternion", BindingFlags.Static | BindingFlags.NonPublic);
setQuaternionInfo?.SetValue(null, _setQuaternion);
FieldInfo getQuaternionInfo = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_getQuaternion", BindingFlags.Static | BindingFlags.NonPublic);
getQuaternionInfo?.SetValue(null, _getQuaternion);
System.Action<string, UnityEngine.Color> _setColor = SetColor;
System.Func<VRCPlayerApi, string, UnityEngine.Color> _getColor = GetColor;
FieldInfo setColorInfo = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_setColor", BindingFlags.Static | BindingFlags.NonPublic);
setColorInfo?.SetValue(null, _setColor);
FieldInfo getColorInfo = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_getColor", BindingFlags.Static | BindingFlags.NonPublic);
getColorInfo?.SetValue(null, _getColor);
System.Action<string, UnityEngine.Color32> _setColor32 = SetColor32;
System.Func<VRCPlayerApi, string, UnityEngine.Color32> _getColor32 = GetColor32;
FieldInfo setColor32Info = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_setColor32", BindingFlags.Static | BindingFlags.NonPublic);
setColor32Info?.SetValue(null, _setColor32);
FieldInfo getColor32Info = typeof(VRC.SDK3.Persistence.PlayerData).GetField("_getColor32", BindingFlags.Static | BindingFlags.NonPublic);
getColor32Info?.SetValue(null, _getColor32);
}
public static int GetUsage(VRCPlayerApi player)
=> FindStorage(player, out ClientSimPlayerDataStorage storage) ? storage.Size : 0;
private static bool FindStorage(VRCPlayerApi playerApi, out ClientSimPlayerDataStorage storage)
{
ClientSimPlayer player = playerApi.GetClientSimPlayer();
if (!player)
{
UnityEngine.Debug.LogError("Could not locate player with id " + playerApi.playerId + " for PlayerData storage.");
storage = null;
return false;
}
storage = player.PlayerDataObject;
if (!storage)
{
UnityEngine.Debug.LogError("Could not locate PlayerData storage for player " + playerApi.playerId);
return false;
}
return true;
}
public static IEnumerable<string> GetKeys(VRCPlayerApi playerApi)
{
if (!FindStorage(playerApi, out ClientSimPlayerDataStorage storage)) return Enumerable.Empty<string>();
return storage.GetKeys();
}
public static bool HasKey(VRCPlayerApi playerApi, string key)
{
if (!FindStorage(playerApi, out ClientSimPlayerDataStorage storage)) return false;
return storage.HasKey(key);
}
public static Type GetType(VRCPlayerApi playerApi, string key)
{
if (!FindStorage(playerApi, out ClientSimPlayerDataStorage storage)) return null;
return storage.GetType(key);
}
public static void SetBool(string key, bool data)
{
if (!FindStorage(Networking.LocalPlayer, out ClientSimPlayerDataStorage storage)) return;
storage.SetBool(key, data);
}
public static bool GetBool(VRCPlayerApi playerApi, string key)
{
if (!FindStorage(playerApi, out ClientSimPlayerDataStorage storage)) return default;
return storage.GetBool(key);
}
public static void SetByte(string key, byte data)
{
if (!FindStorage(Networking.LocalPlayer, out ClientSimPlayerDataStorage storage)) return;
storage.SetByte(key, data);
}
public static byte GetByte(VRCPlayerApi playerApi, string key)
{
if (!FindStorage(playerApi, out ClientSimPlayerDataStorage storage)) return default;
return storage.GetByte(key);
}
public static void SetSByte(string key, sbyte data)
{
if (!FindStorage(Networking.LocalPlayer, out ClientSimPlayerDataStorage storage)) return;
storage.SetSByte(key, data);
}
public static sbyte GetSByte(VRCPlayerApi playerApi, string key)
{
if (!FindStorage(playerApi, out ClientSimPlayerDataStorage storage)) return default;
return storage.GetSByte(key);
}
public static void SetBytes(string key, byte[] data)
{
if (!FindStorage(Networking.LocalPlayer, out ClientSimPlayerDataStorage storage)) return;
storage.SetBytes(key, data);
}
public static byte[] GetBytes(VRCPlayerApi playerApi, string key)
{
if (!FindStorage(playerApi, out ClientSimPlayerDataStorage storage)) return default;
return storage.GetBytes(key);
}
public static void SetString(string key, string data)
{
if (!FindStorage(Networking.LocalPlayer, out ClientSimPlayerDataStorage storage)) return;
storage.SetString(key, data);
}
public static string GetString(VRCPlayerApi playerApi, string key)
{
if (!FindStorage(playerApi, out ClientSimPlayerDataStorage storage)) return default;
return storage.GetString(key);
}
public static void SetShort(string key, short data)
{
if (!FindStorage(Networking.LocalPlayer, out ClientSimPlayerDataStorage storage)) return;
storage.SetShort(key, data);
}
public static void SetUShort(string key, ushort data)
{
if (!FindStorage(Networking.LocalPlayer, out ClientSimPlayerDataStorage storage)) return;
storage.SetUShort(key, data);
}
public static short GetShort(VRCPlayerApi playerApi, string key)
{
if (!FindStorage(playerApi, out ClientSimPlayerDataStorage storage)) return default;
return storage.GetShort(key);
}
public static ushort GetUShort(VRCPlayerApi playerApi, string key)
{
if (!FindStorage(playerApi, out ClientSimPlayerDataStorage storage)) return default;
return storage.GetUShort(key);
}
public static void SetInt(string key, int data)
{
if (!FindStorage(Networking.LocalPlayer, out ClientSimPlayerDataStorage storage)) return;
storage.SetInt(key, data);
}
public static void SetUInt(string key, uint data)
{
if (!FindStorage(Networking.LocalPlayer, out ClientSimPlayerDataStorage storage)) return;
storage.SetUInt(key, data);
}
public static int GetInt(VRCPlayerApi playerApi, string key)
{
if (!FindStorage(playerApi, out ClientSimPlayerDataStorage storage)) return default;
return storage.GetInt(key);
}
public static uint GetUInt(VRCPlayerApi playerApi, string key)
{
if (!FindStorage(playerApi, out ClientSimPlayerDataStorage storage)) return default;
return storage.GetUInt(key);
}
public static void SetLong(string key, long data)
{
if (!FindStorage(Networking.LocalPlayer, out ClientSimPlayerDataStorage storage)) return;
storage.SetLong(key, data);
}
public static long GetLong(VRCPlayerApi playerApi, string key)
{
if (!FindStorage(playerApi, out ClientSimPlayerDataStorage storage)) return default;
return storage.GetLong(key);
}
public static void SetULong(string key, ulong data)
{
if (!FindStorage(Networking.LocalPlayer, out ClientSimPlayerDataStorage storage)) return;
storage.SetULong(key, data);
}
public static ulong GetULong(VRCPlayerApi playerApi, string key)
{
if (!FindStorage(playerApi, out ClientSimPlayerDataStorage storage)) return default;
return storage.GetULong(key);
}
public static void SetFloat(string key, float data)
{
if (!FindStorage(Networking.LocalPlayer, out ClientSimPlayerDataStorage storage)) return;
storage.SetFloat(key, data);
}
public static float GetFloat(VRCPlayerApi playerApi, string key)
{
if (!FindStorage(playerApi, out ClientSimPlayerDataStorage storage)) return default;
return storage.GetFloat(key);
}
public static void SetDouble(string key, double data)
{
if (!FindStorage(Networking.LocalPlayer, out ClientSimPlayerDataStorage storage)) return;
storage.SetDouble(key, data);
}
public static double GetDouble(VRCPlayerApi playerApi, string key)
{
if (!FindStorage(playerApi, out ClientSimPlayerDataStorage storage)) return default;
return storage.GetDouble(key);
}
public static void SetVector2(string key, Vector2 data)
{
if (!FindStorage(Networking.LocalPlayer, out ClientSimPlayerDataStorage storage)) return;
storage.SetVector2(key, data);
}
public static Vector2 GetVector2(VRCPlayerApi playerApi, string key)
{
if (!FindStorage(playerApi, out ClientSimPlayerDataStorage storage)) return default;
return storage.GetVector2(key);
}
public static void SetVector3(string key, Vector3 data)
{
if (!FindStorage(Networking.LocalPlayer, out ClientSimPlayerDataStorage storage)) return;
storage.SetVector3(key, data);
}
public static Vector3 GetVector3(VRCPlayerApi playerApi, string key)
{
if (!FindStorage(playerApi, out ClientSimPlayerDataStorage storage)) return default;
return storage.GetVector3(key);
}
public static void SetVector4(string key, Vector4 data)
{
if (!FindStorage(Networking.LocalPlayer, out ClientSimPlayerDataStorage storage)) return;
storage.SetVector4(key, data);
}
public static Vector4 GetVector4(VRCPlayerApi playerApi, string key)
{
if (!FindStorage(playerApi, out ClientSimPlayerDataStorage storage)) return default;
return storage.GetVector4(key);
}
public static void SetQuaternion(string key, Quaternion data)
{
if (!FindStorage(Networking.LocalPlayer, out ClientSimPlayerDataStorage storage)) return;
storage.SetQuaternion(key, data);
}
public static Quaternion GetQuaternion(VRCPlayerApi playerApi, string key)
{
if (!FindStorage(playerApi, out ClientSimPlayerDataStorage storage)) return default;
return storage.GetQuaternion(key);
}
public static void SetColor(string key, UnityEngine.Color data)
{
if (!FindStorage(Networking.LocalPlayer, out ClientSimPlayerDataStorage storage)) return;
storage.SetColor(key, data);
}
public static UnityEngine.Color GetColor(VRCPlayerApi playerApi, string key)
{
if (!FindStorage(playerApi, out ClientSimPlayerDataStorage storage)) return default;
return storage.GetColor(key);
}
public static void SetColor32(string key, UnityEngine.Color32 data)
{
if (!FindStorage(Networking.LocalPlayer, out ClientSimPlayerDataStorage storage)) return;
storage.SetColor32(key, data);
}
public static UnityEngine.Color32 GetColor32(VRCPlayerApi playerApi, string key)
{
if (!FindStorage(playerApi, out ClientSimPlayerDataStorage storage)) return default;
return storage.GetColor32(key);
}
}
}
#endif

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: cf5156727fe54ec1b396e496f91f03d4
timeCreated: 1708639223

View File

@ -0,0 +1,198 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using Cysharp.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEngine;
using UnityEngine.SceneManagement;
using VRC.SDK3.ClientSim.Interfaces;
using VRC.SDK3.Data;
using VRC.SDKBase;
using VRC.Udon;
namespace VRC.SDK3.ClientSim.Persistence
{
[AddComponentMenu("")] // hides component in Add Component menu
public class ClientSimPlayerObjectStorage : ClientSimBehaviour
{
#if VRC_ENABLE_PLAYER_PERSISTENCE
public static string PlayerObjectsFolder => Path.Combine("ClientSimStorage", "PlayerObjects");
internal static string ActiveSceneName;
internal static string PlayerDataFilePath(VRCPlayerApi player)
{
string root = Path.GetDirectoryName(Application.dataPath);
string path = Path.Combine(root, PlayerObjectsFolder);
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
return path + "/PlayerObject_" + $"{player.playerId}" + $"_{ActiveSceneName}" + ".json";
}
private VRCPlayerApi _player;
private IClientSimUdonEventSender _udonEventSender;
private IClientSimEventDispatcher _eventDispatcher;
private Dictionary<int,IClientSimNetworkView> _persistentObjects = new Dictionary<int, IClientSimNetworkView>();
private DataDictionary _persistentObjectData = new DataDictionary();
private bool _isInitialized = false;
private bool _HasJoined = false;
private Coroutine _ContinuousUpdate;
private const float _updateInterval = 1 / 4f;
private bool hadUpdate = false;
public int Size =>
VRCJson.TrySerializeToJson(_persistentObjectData, JsonExportType.Beautify, out DataToken json)
? System.Text.Encoding.UTF8.GetByteCount(json.String)
: 0;
public void Init(VRCPlayerApi player, IClientSimUdonEventSender udonEventSender, IClientSimEventDispatcher eventDispatcher)
{
_player = player;
_udonEventSender = udonEventSender;
_eventDispatcher = eventDispatcher;
ActiveSceneName = SceneManager.GetActiveScene().name;
_eventDispatcher.Subscribe<ClientSimOnPlayerJoinedEvent>(OnPlayerJoined);
UdonBehaviour.RequestSerializationHook += RequestSerializationHook;
_isInitialized = true;
_ContinuousUpdate = StartCoroutine(UpdateContinuous());
}
private void OnDestroy()
{
if(_eventDispatcher != null)
_eventDispatcher.Unsubscribe<ClientSimOnPlayerJoinedEvent>(OnPlayerJoined);
if(_ContinuousUpdate != null)
StopCoroutine(_ContinuousUpdate);
}
public IEnumerator UpdateContinuous()
{
while (true)
{
if (_HasJoined && _isInitialized)
{
Encode();
}
yield return new WaitForSeconds(_updateInterval);
}
}
public void RequestSerializationHook(UdonBehaviour udonBehaviour)
{
ClientSimPersistenceEventSending.Instance.QueueRequest(udonBehaviour, this);
}
private void OnPlayerJoined(ClientSimOnPlayerJoinedEvent payload)
{
if (payload.player.playerId == _player.playerId)
{
_HasJoined = true;
Decode();
}
}
public void Encode(GameObject gameObject = null)
{
if (!_isInitialized || !_HasJoined) return;
if(_persistentObjectData == null)
_persistentObjectData = new DataDictionary();
foreach (var keyValuePersistentObject in _persistentObjects)
{
DataToken key = keyValuePersistentObject.Key.ToString();
if (!_persistentObjectData.ContainsKey(key))
_persistentObjectData.Add(key, new DataList());
_persistentObjectData[key] = keyValuePersistentObject.Value.Encode(gameObject);
}
hadUpdate = true;
}
private async UniTask SaveToFile(string data)
{
await UniTask.SwitchToTaskPool();
try{
await File.WriteAllTextAsync(PlayerDataFilePath(_player), data);
}
catch (Exception e)
{
this.LogError($"Error saving PlayerObjects: {e.Message}");
}
}
private void Decode()
{
string path = PlayerDataFilePath(_player);
if (!File.Exists(path))
{
File.WriteAllText(path, "{}");
}
string json = File.ReadAllText(path);
if (!VRCJson.TryDeserializeFromJson(json, out DataToken token))
{
this.LogError($"Error initializing PlayerObjects: {token.Error}");
return;
}
_persistentObjectData = token.DataDictionary;
ClientSimPlayer player = _player.GetClientSimPlayer();
foreach (GameObject persistantObject in player.PlayerPersistenceObjects)
{
IClientSimNetworkId networkId = persistantObject.GetComponent<IClientSimNetworkId>();
if (networkId == null) continue;
int id = networkId.GetNetworkId();
IClientSimNetworkView serializer = persistantObject.GetComponent<IClientSimNetworkView>();
_persistentObjects.TryAdd(id, serializer);
if (_persistentObjectData.TryGetValue(id.ToString(), out var data))
{
_persistentObjects[id].Decode(data.DataList);
}
}
_eventDispatcher.SendEvent(new ClientSimOnPlayerObjectsDecodedEvent { player = _player });
}
private float lastCheckTime = 0;
public void LateUpdate()
{
if (hadUpdate)
{
hadUpdate = false;
_eventDispatcher.SendEvent(new ClientSimOnPlayerObjectUpdateEndedEvent());
VRCJson.TrySerializeToJson(_persistentObjectData, JsonExportType.Beautify, out DataToken json);
SaveToFile(json.String).Forget();
}
if (Time.realtimeSinceStartup - lastCheckTime > 30f)
{
lastCheckTime = Time.realtimeSinceStartup;
float objLimit = ClientSimMain.TryGetInstance(out var instance) ? instance.GetPlayerObjectsUsageLimit() : 0;
float sz = Size;
if (sz >= objLimit)
UdonManager.Instance.RunEvent(UdonManager.UDON_EVENT_ONPLAYEROBJECTSTORAGEEXCEEDED, ("player", _player));
else if (sz >= objLimit * 0.7f)
UdonManager.Instance.RunEvent(UdonManager.UDON_EVENT_ONPLAYERDATASTORAGEWARNING, ("player", _player));
}
}
#endif
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 06c904c1b8134763a58e319723ec67ad
timeCreated: 1709650412

View File

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

View File

@ -0,0 +1,218 @@

using UnityEngine;
using VRC.SDK3.ClientSim.PlayerTracking;
using VRC.Udon.Common;
namespace VRC.SDK3.ClientSim
{
// Listens to Events:
// - ClientSimMouseReleasedEvent
// - ClientSimOnPlayerEnteredStationEvent
// - ClientSimOnPlayerExitedStationEvent
// Listens to Input Events:
// - ToggleCrouch
// - ToggleProne
[AddComponentMenu("")]
public class ClientSimDesktopTrackingProvider : ClientSimTrackingProviderBase
{
[SerializeField]
private Transform playerXRotationBase;
[SerializeField]
private Transform playerYRotationBase;
private bool _mouseReleased = false;
private ClientSimDesktopTrackingRotator _desktopRotator;
private IClientSimStation _currentStation;
public override void Initialize(
IClientSimEventDispatcher eventDispatcher,
IClientSimInput input,
ClientSimSettings settings,
IClientSimPlayerHeightManager heightManager)
{
base.Initialize(eventDispatcher, input, settings, heightManager);
SetTrackingItemPositions();
_desktopRotator = new ClientSimDesktopTrackingRotator(playerXRotationBase, playerYRotationBase);
}
private void SetTrackingItemPositions()
{
head.localPosition = new Vector3(0, STANDING_HEIGHT, .1f);
head.localRotation = Quaternion.identity;
rightHand.localPosition = new Vector3(0.15f, -0.13f, 0.4f);
rightHand.localRotation = Quaternion.Euler(-35, 0, -90);
leftHand.localPosition = new Vector3(-0.15f, -0.13f, 0.4f);
leftHand.localRotation = Quaternion.Euler(-35, 0, -90);
}
#region IClientSimInputEventSubscribable
public override void SubscribeInputEvents()
{
base.SubscribeInputEvents();
input.SubscribeToggleCrouch(ToggleCrouchInput);
input.SubscribeToggleProne(ToggleProneInput);
}
public override void UnsubscribeInputEvents()
{
base.UnsubscribeInputEvents();
input.UnsubscribeToggleCrouch(ToggleCrouchInput);
input.UnsubscribeToggleProne(ToggleProneInput);
}
#endregion
#region IClientSimInputEventSubscribable
public override void SubscribeEvents()
{
base.SubscribeEvents();
eventDispatcher.Subscribe<ClientSimMouseReleasedEvent>(MouseReleasedEvent);
eventDispatcher.Subscribe<ClientSimOnPlayerEnteredStationEvent>(PlayerEnteredStation);
eventDispatcher.Subscribe<ClientSimOnPlayerExitedStationEvent>(PlayerExitedStation);
}
public override void UnsubscribeEvents()
{
base.UnsubscribeEvents();
eventDispatcher.Unsubscribe<ClientSimMouseReleasedEvent>(MouseReleasedEvent);
eventDispatcher.Unsubscribe<ClientSimOnPlayerEnteredStationEvent>(PlayerEnteredStation);
eventDispatcher.Unsubscribe<ClientSimOnPlayerExitedStationEvent>(PlayerExitedStation);
}
#endregion
#region ClientSim Events
private void MouseReleasedEvent(ClientSimMouseReleasedEvent mouseReleasedEvent)
{
_mouseReleased = mouseReleasedEvent.isReleased;
}
private void PlayerEnteredStation(ClientSimOnPlayerEnteredStationEvent stationEvent)
{
_currentStation = stationEvent.station;
_desktopRotator.SetStation(_currentStation);
if (_currentStation.IsSeated())
{
SetStance(ClientSimPlayerStanceEnum.SITTING);
}
}
private void PlayerExitedStation(ClientSimOnPlayerExitedStationEvent stationEvent)
{
_currentStation = null;
_desktopRotator.SetStation(null);
SetStance(ClientSimPlayerStanceEnum.STANDING);
}
#endregion
#region ClientSim Input
private void ToggleCrouchInput(bool value)
{
// Only handle on down, and not on release.
if (!value)
{
return;
}
if (GetPlayerStance() == ClientSimPlayerStanceEnum.CROUCHING)
{
SetStance(ClientSimPlayerStanceEnum.STANDING);
}
else
{
SetStance(ClientSimPlayerStanceEnum.CROUCHING);
}
}
private void ToggleProneInput(bool value)
{
// Only handle on down, and not on release.
if (!value)
{
return;
}
if (GetPlayerStance() == ClientSimPlayerStanceEnum.PRONE)
{
SetStance(ClientSimPlayerStanceEnum.STANDING);
}
else
{
SetStance(ClientSimPlayerStanceEnum.PRONE);
}
}
#endregion
private void Update()
{
// If mouse is released, do not update rotation.
if (!_mouseReleased)
{
_desktopRotator.HandleRotation(input.GetLookHorizontal(), input.GetLookVertical());
}
}
private void SetStance(ClientSimPlayerStanceEnum stance)
{
// If in a station, ignore all non sitting stances.
if (_currentStation != null && _currentStation.IsLockedInStation() && stance != ClientSimPlayerStanceEnum.SITTING)
{
return;
}
Vector3 cameraPosition = head.localPosition;
switch (stance)
{
case ClientSimPlayerStanceEnum.PRONE:
cameraPosition.y = PRONE_HEIGHT;
break;
case ClientSimPlayerStanceEnum.CROUCHING:
cameraPosition.y = CROUCHING_HEIGHT;
break;
case ClientSimPlayerStanceEnum.SITTING:
cameraPosition.y = SITTING_HEIGHT;
break;
case ClientSimPlayerStanceEnum.STANDING:
cameraPosition.y = STANDING_HEIGHT;
break;
}
head.localPosition = cameraPosition;
}
public override Transform GetHandRaycastTransform(HandType handType)
{
throw new ClientSimException("Desktop tracking does not support arm based raycasting");
}
public override bool IsVR()
{
return false;
}
public override bool SupportsPickupAutoHold()
{
return true;
}
public override void LookTowardsPoint(Vector3 point)
{
_desktopRotator.LookAtPoint(point);
}
}
}

View File

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

View File

@ -0,0 +1,83 @@
using UnityEngine;
namespace VRC.SDK3.ClientSim.PlayerTracking
{
public class ClientSimDesktopTrackingRotator
{
private const float MINIMUM_X_ANGLE = -80f;
private const float MAXIMUM_X_ANGLE = 70f;
private const float MINIMUM_Y_ANGLE = -90F;
private const float MAXIMUM_Y_ANGLE = 90F;
private Quaternion _targetBaseYRot;
private Quaternion _targetHeadXRot;
private readonly Transform _headXTransform;
private readonly Transform _headYTransform;
private IClientSimStation _currentStation;
public ClientSimDesktopTrackingRotator(Transform headXTransform, Transform headYTransform)
{
_headXTransform = headXTransform;
_headYTransform = headYTransform;
_targetBaseYRot = _headYTransform.localRotation;
_targetHeadXRot = _headXTransform.localRotation;
}
public void SetStation(IClientSimStation station)
{
_currentStation = station;
_targetBaseYRot = Quaternion.identity;
}
public void HandleRotation(float xDelta, float yDelta)
{
_targetHeadXRot *= Quaternion.Euler(-yDelta, 0f, 0f);
_targetHeadXRot = ClampRotationAroundAxis(_targetHeadXRot, 0, MINIMUM_X_ANGLE, MAXIMUM_X_ANGLE);
// Player in a station, allow limited rotation on Y axis.
if (_currentStation != null && _currentStation.IsLockedInStation())
{
_targetBaseYRot *= Quaternion.Euler(0f, xDelta, 0f);
_targetBaseYRot = ClampRotationAroundAxis(_targetBaseYRot, 1, MINIMUM_Y_ANGLE, MAXIMUM_Y_ANGLE);
}
_headXTransform.localRotation = _targetHeadXRot;
_headYTransform.localRotation = _targetBaseYRot;
}
// Used in tests to force rotate the player to look at an object.
public void LookAtPoint(Vector3 point)
{
// Get the amount the player needs to rotate on Y
Vector3 localizedYPoint = _headYTransform.InverseTransformPoint(point);
localizedYPoint.y = 0;
float yAngle = Vector3.SignedAngle(Vector3.forward, localizedYPoint, Vector3.up);
// Get the amount the player needs to rotate on X
Vector3 localizedXPoint = _headXTransform.InverseTransformPoint(point);
localizedXPoint.x = 0;
float xAngle = Vector3.SignedAngle(Vector3.forward, localizedXPoint, Vector3.left);
HandleRotation(yAngle, xAngle);
}
private static Quaternion ClampRotationAroundAxis(Quaternion q, int index, float minAngle, float maxAngle)
{
q.x /= q.w;
q.y /= q.w;
q.z /= q.w;
q.w = 1.0f;
float angle = 2.0f * Mathf.Rad2Deg * Mathf.Atan(q[index]);
angle = Mathf.Clamp(angle, minAngle, maxAngle);
q[index] = Mathf.Tan(0.5f * Mathf.Deg2Rad * angle);
return q;
}
}
}

View File

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

View File

@ -0,0 +1,10 @@
namespace VRC.SDK3.ClientSim
{
public enum ClientSimPlayerStanceEnum
{
STANDING,
CROUCHING,
PRONE,
SITTING
}
}

View File

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

View File

@ -0,0 +1,270 @@

using System;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon.Common;
namespace VRC.SDK3.ClientSim
{
/// <summary>
/// System responsible for providing tracking data to the rest of ClientSim.
/// </summary>
/// <remarks>
/// Currently only used for DesktopTracking, but can be extended for VR and even fake VR implementations.
/// Sends Events:
/// - ClientSimOnPlayerHeightUpdateEvent
/// - ClientSimOnTrackingScaleUpdateEvent
/// Listens to Events:
/// - ClientSimOnPlayerHeightUpdateEvent
/// </remarks>
[DefaultExecutionOrder(-3000)] // Update before player raycasting
public abstract class ClientSimTrackingProviderBase : ClientSimBehaviour, IClientSimTrackingProvider, IDisposable
{
private const float MINIMUM_TRACKING_SCALE = 0.1f;
private const float MAXIMUM_TRACKING_SCALE = 50f;
// TODO calculate this value based on the avatar instead of hard coding it.
public const float AVATAR_HEIGHT = 1.9f;
protected const float STANDING_HEIGHT = 1.75f;
protected const float CROUCHING_HEIGHT = 1.0f;
protected const float PRONE_HEIGHT = 0.5f;
protected const float SITTING_HEIGHT = 1.2f;
[SerializeField]
protected Transform head;
[SerializeField]
protected Transform leftHand;
[SerializeField]
protected Transform rightHand;
[SerializeField]
protected Transform playspace;
[SerializeField]
protected Transform playerAudioListener;
[SerializeField]
protected Camera playerCamera;
protected IClientSimEventDispatcher eventDispatcher;
protected IClientSimInput input;
protected ClientSimSettings settings;
protected IClientSimPlayerHeightManager heightManager;
private float _trackingScale = 1;
public static float CalculateTrackingScaleFromPlayerHeight(float playerHeight)
{
return playerHeight / AVATAR_HEIGHT;
}
public static float CalculatePlayerHeightFromTrackingScale(float trackingScale)
{
return trackingScale * AVATAR_HEIGHT;
}
public virtual void Initialize(
IClientSimEventDispatcher eventDispatcher,
IClientSimInput input,
ClientSimSettings settings,
IClientSimPlayerHeightManager heightManager)
{
this.eventDispatcher = eventDispatcher;
this.input = input;
this.settings = settings;
this.heightManager = heightManager;
SubscribeEvents();
// Input will be null with incorrect Unity input project settings.
if (input != null)
{
SubscribeInputEvents();
}
}
protected virtual void Start()
{
// Send event for this to ensure everything that uses the player height is properly updated.
eventDispatcher.SendEvent(new ClientSimOnPlayerHeightUpdateEvent
{
playerHeight = heightManager.GetAvatarEyeHeightAsMeters()
});
// Only disable audio listeners and cameras if the player is spawned.
if (settings.spawnPlayer)
{
// Destroy other audio listeners
foreach (var listener in FindObjectsByType<AudioListener>(FindObjectsSortMode.None))
{
if (listener.transform == playerAudioListener)
{
continue;
}
DestroyImmediate(listener);
}
// Disable all cameras that do not render to a render texture.
foreach (var worldCamera in FindObjectsByType<Camera>(FindObjectsSortMode.None))
{
if (worldCamera == playerCamera)
{
continue;
}
if (worldCamera.targetTexture != null)
{
continue;
}
worldCamera.enabled = false;
}
}
}
protected virtual void OnDestroy()
{
Dispose();
}
public void Dispose()
{
UnsubscribeEvents();
// Input will be null with incorrect Unity input project settings.
if (input != null)
{
UnsubscribeInputEvents();
}
}
#region IClientSimTrackingProvider
public virtual VRCPlayerApi.TrackingData GetTrackingData(VRCPlayerApi.TrackingDataType trackingDataType)
{
VRCPlayerApi.TrackingData data = new VRCPlayerApi.TrackingData();
switch (trackingDataType)
{
case VRCPlayerApi.TrackingDataType.Head:
data.position = head.position;
data.rotation = head.rotation;
break;
case VRCPlayerApi.TrackingDataType.LeftHand:
data.position = leftHand.position;
data.rotation = leftHand.rotation;
break;
case VRCPlayerApi.TrackingDataType.RightHand:
data.position = rightHand.position;
data.rotation = rightHand.rotation;
break;
case VRCPlayerApi.TrackingDataType.Origin:
data.position = playspace.position;
data.rotation = playspace.rotation;
break;
}
return data;
}
public virtual Transform GetTrackingTransform(VRCPlayerApi.TrackingDataType trackingDataType)
{
switch (trackingDataType)
{
case VRCPlayerApi.TrackingDataType.Head:
return head;
case VRCPlayerApi.TrackingDataType.LeftHand:
return leftHand;
case VRCPlayerApi.TrackingDataType.RightHand:
return rightHand;
case VRCPlayerApi.TrackingDataType.Origin:
return playspace;
}
return null;
}
public float GetTrackingScale()
{
return _trackingScale;
}
public void SetTrackingScale(float scale)
{
scale = Mathf.Clamp(scale, MINIMUM_TRACKING_SCALE, MAXIMUM_TRACKING_SCALE);
_trackingScale = scale;
playspace.localScale = scale * Vector3.one;
// Audio listener must always be scale 1 to ensure ONSP sounds correctly.
playerAudioListener.localScale = 1.0f / scale * Vector3.one;
eventDispatcher.SendEvent(new ClientSimOnTrackingScaleUpdateEvent { trackingScale = scale });
}
public ClientSimPlayerStanceEnum GetPlayerStance()
{
// Check heights starting from shortest
float headHeight = head.localPosition.y;
if (headHeight <= PRONE_HEIGHT)
{
return ClientSimPlayerStanceEnum.PRONE;
}
if (headHeight <= CROUCHING_HEIGHT)
{
return ClientSimPlayerStanceEnum.CROUCHING;
}
return ClientSimPlayerStanceEnum.STANDING;
}
public abstract Transform GetHandRaycastTransform(HandType handType);
public abstract bool IsVR();
public abstract bool SupportsPickupAutoHold();
public abstract void LookTowardsPoint(Vector3 point);
#endregion
#region IClientSimPlayerCameraProvider
public Camera GetCamera()
{
return playerCamera;
}
public Camera GetCameraForObject(GameObject obj)
{
// TODO: Make this interact with camera stacking
return playerCamera;
}
#endregion
#region ClientSim Events
private void OnPlayerHeightUpdate(ClientSimOnPlayerHeightUpdateEvent heightEvent)
{
// Convert player height to tracking scale and set the new scale.
SetTrackingScale(CalculateTrackingScaleFromPlayerHeight(heightEvent.playerHeight));
}
#endregion
public virtual void SubscribeInputEvents() { }
public virtual void UnsubscribeInputEvents() { }
public virtual void SubscribeEvents()
{
eventDispatcher.Subscribe<ClientSimOnPlayerHeightUpdateEvent>(OnPlayerHeightUpdate);
}
public virtual void UnsubscribeEvents()
{
eventDispatcher.Unsubscribe<ClientSimOnPlayerHeightUpdateEvent>(OnPlayerHeightUpdate);
}
}
}

View File

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