using System; using UnityEngine; using VRC.SDKBase; namespace VRC.SDK3.ClientSim { /// /// 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. /// /// /// Sends Events: /// - ClientSimOnPlayerEnteredStationEvent /// - ClientSimOnPlayerExitedStationEvent /// Listens to Events: /// - ClientSimOnPlayerTeleportedEvent /// [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(); } public void Initialize( IClientSimEventDispatcher eventDispatcher, IClientSimPlayerApiProvider playerApiProvider) { _eventDispatcher = eventDispatcher; _playerApiProvider = playerApiProvider; _eventDispatcher.Subscribe(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(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()) { 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()) { 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 } }