using NitroxModel.DataStructures; using NitroxModel.Packets; using NitroxModel_Subnautica.DataStructures; using UnityEngine; namespace NitroxClient.MonoBehaviours.Vehicles; public class WatchedEntry { /// /// In unity position units. Refer to for use infos. /// private const float MINIMAL_MOVEMENT_TRESHOLD = 0.05f; /// /// In degrees (°). Refer to for use infos. /// private const float MINIMAL_ROTATION_TRESHOLD = 0.05f; /// /// In seconds. Refer to for use infos. /// private const float MAX_TIME_WITHOUT_BROADCAST = 5f; /// private const float SAFETY_BROADCAST_WINDOW = 0.2f; private readonly NitroxId Id; private readonly Transform transform; private readonly Vehicle vehicle; private readonly SubControl subControl; private float latestBroadcastTime; private Vector3 latestLocalPositionSent; private Quaternion latestLocalRotationSent; public WatchedEntry(NitroxId Id, Transform transform) { this.Id = Id; this.transform = transform; vehicle = transform.GetComponent(); subControl = transform.GetComponent(); } private bool IsDrivenVehicle() { return vehicle && Player.main.currentMountedVehicle == vehicle; } private bool IsDrivenCyclops() { return subControl && Player.main.currentSub == subControl.sub && Player.main.mode == Player.Mode.Piloting; } public MovementData GetMovementData(NitroxId id) { // Packets should be filled with more data if the vehicle is being driven by the local player if (IsDrivenVehicle()) { // Those two values are set between -1 and 1 so we can easily scale them up while still in range for sbyte sbyte steeringWheelYaw = (sbyte)(Mathf.Clamp(vehicle.steeringWheelYaw, -1, 1) * 70f); sbyte steeringWheelPitch = (sbyte)(Mathf.Clamp(vehicle.steeringWheelPitch, -1, 1) * 45f); bool throttleApplied = false; Vector3 input = AvatarInputHandler.main.IsEnabled() ? GameInput.GetMoveDirection() : Vector3.zero; // See SeaMoth.UpdateSounds if (vehicle is SeaMoth) { throttleApplied = input.magnitude > 0f; } // See Exosuit.Update else if (vehicle is Exosuit) { throttleApplied = input.y > 0f; } return new DrivenVehicleMovementData(id, transform.position.ToDto(), transform.rotation.ToDto(), steeringWheelYaw, steeringWheelPitch, throttleApplied); } if (IsDrivenCyclops()) { // Cyclop steering wheel's yaw and pitch are between -90 and 90 so they're already in range for sbyte sbyte steeringWheelYaw = (sbyte)Mathf.Clamp(subControl.steeringWheelYaw, -90, 90); sbyte steeringWheelPitch = (sbyte)Mathf.Clamp(subControl.steeringWheelPitch, -90, 90); // See SubControl.Update bool throttleApplied = subControl.throttle.magnitude > 0.0001f; return new DrivenVehicleMovementData(id, transform.position.ToDto(), transform.rotation.ToDto(), steeringWheelYaw, steeringWheelPitch, throttleApplied); } // Normal case in which the vehicule isn't driven by the local player return new SimpleMovementData(id, transform.position.ToDto(), transform.rotation.ToDto()); } public void OnBroadcastPosition() { latestLocalPositionSent = transform.localPosition; latestLocalRotationSent = transform.localRotation; } private bool HasVehicleMoved() { return Vector3.Distance(latestLocalPositionSent, transform.localPosition) > MINIMAL_MOVEMENT_TRESHOLD || Quaternion.Angle(latestLocalRotationSent, transform.localRotation) > MINIMAL_ROTATION_TRESHOLD; } /// /// Rate limiter which prevents all non-moving vehicles from sending too many packets following some rules: /// - the driven vehicle is not rate limited /// - position changes less than are ignored /// - rotation changes less than are ignored /// - every period of , there's a /// during which movements packets are sent to avoid any packet drop's bad effect, regardless of /// /// /// is not updated during the so we can recognize this window /// public bool ShouldBroadcastMovement() { // Watched entry validity check (e.g. for vehicle death) if (!transform) { MovementBroadcaster.UnregisterWatched(Id); return false; } float deltaTimeSinceBroadcast = DayNightCycle.main.timePassedAsFloat - latestBroadcastTime; if (IsDrivenCyclops() || IsDrivenVehicle() || deltaTimeSinceBroadcast < 0 || HasVehicleMoved()) { // As long as the vehicle has moved, we can reset the broadcast timer latestBroadcastTime = DayNightCycle.main.timePassedAsFloat; return true; } if (deltaTimeSinceBroadcast > MAX_TIME_WITHOUT_BROADCAST) { if (deltaTimeSinceBroadcast > MAX_TIME_WITHOUT_BROADCAST + SAFETY_BROADCAST_WINDOW) { // only reset the broadcast timer after the safety window has elapsed latestBroadcastTime = DayNightCycle.main.timePassedAsFloat; } return true; } return false; } }