using System; using System.Globalization; using System.Runtime.CompilerServices; using KinematicCharacterController; using RoR2.ConVar; using Unity; using UnityEngine; using UnityEngine.Networking; namespace RoR2; [RequireComponent(typeof(CharacterBody))] public class CharacterMotor : BaseCharacterController, IPhysMotor, ILifeBehavior, IDisplacementReceiver, ICharacterGravityParameterProvider, ICharacterFlightParameterProvider { [Serializable] public struct HitGroundInfo { public Vector3 velocity; public Vector3 position; public bool isValidForEffect; public override string ToString() { return $"velocity={velocity} position={position}"; } } public delegate void HitGroundDelegate(ref HitGroundInfo hitGroundInfo); public struct MovementHitInfo { public Vector3 velocity; public Collider hitCollider; } public delegate void MovementHitDelegate(ref MovementHitInfo movementHitInfo); [HideInInspector] public float walkSpeedPenaltyCoefficient = 1f; [Tooltip("The character direction component to supply a move vector to.")] public CharacterDirection characterDirection; [Tooltip("Whether or not a move vector supplied to this component can cause movement. Use this when the object is driven by root motion.")] public bool muteWalkMotion; [Tooltip("The mass of this character.")] public float mass = 1f; [Tooltip("The air control value of this character as a fraction of ground control.")] public float airControl = 0.25f; [Tooltip("Disables Air Control for things like jumppads")] public bool disableAirControlUntilCollision; [Tooltip("Auto-assigns parameters skin width, slope angle, and step offset as a function of the Character Motor's radius and height")] public bool generateParametersOnAwake = true; [Tooltip("Ignores JumpVolumes")] public bool doNotTriggerJumpVolumes; private NetworkIdentity networkIdentity; private CharacterBody body; private CapsuleCollider capsuleCollider; private static readonly bool enableMotorWithoutAuthority; private bool alive = true; private const float restDuration = 1f; private const float restVelocityThreshold = 0.025f; private const float restVelocityThresholdSqr = 0.00062500004f; public const float slipStartAngle = 70f; public const float slipEndAngle = 55f; private float restStopwatch; private Vector3 previousPosition; private bool isAirControlForced; [NonSerialized] public int jumpCount; [NonSerialized] public bool netIsGrounded; [NonSerialized] public Vector3 netGroundNormal; [NonSerialized] public Vector3 velocity; private Vector3 lastVelocity; [NonSerialized] public Vector3 rootMotion; private Vector3 _moveDirection; private static readonly FloatConVar cvCMotorSafeCollisionStepThreshold; private int _safeCollisionEnableCount; [SerializeField] [Tooltip("Determins how gravity affects this character.")] private CharacterGravityParameters _gravityParameters; [Tooltip("Determines whether this character has three-dimensional or two-dimensional movement capabilities.")] [SerializeField] private CharacterFlightParameters _flightParameters; private static int kRpcRpcApplyForceImpulse; private static int kRpcRpcOnHitGroundSFX; private static int kCmdCmdReportHitGround; public float walkSpeed => body.moveSpeed * walkSpeedPenaltyCoefficient; public float acceleration => body.acceleration; public bool atRest => restStopwatch > 1f; public bool hasEffectiveAuthority { get; private set; } public Vector3 estimatedGroundNormal { get { if (!hasEffectiveAuthority) { return netGroundNormal; } return base.Motor.GroundingStatus.GroundNormal; } } private bool canWalk { get { if (!muteWalkMotion) { return alive; } return false; } } public bool isGrounded { get { if (!hasEffectiveAuthority) { return netIsGrounded; } return base.Motor.GroundingStatus.IsStableOnGround; } } float IPhysMotor.mass => mass; Vector3 IPhysMotor.velocity => velocity; Vector3 IPhysMotor.velocityAuthority { get { return velocity; } set { velocity = value; } } public Vector3 moveDirection { get { return _moveDirection; } set { _moveDirection = value; } } private float slopeLimit { get { return base.Motor.MaxStableSlopeAngle; } set { base.Motor.MaxStableSlopeAngle = value; } } public float stepOffset { get { return base.Motor.MaxStepHeight; } set { base.Motor.MaxStepHeight = value; } } public float capsuleHeight => capsuleCollider.height; public float capsuleRadius => capsuleCollider.radius; public StepHandlingMethod stepHandlingMethod { get { return base.Motor.StepHandling = StepHandlingMethod.None; } set { base.Motor.StepHandling = value; } } public bool ledgeHandling { get { return base.Motor.LedgeAndDenivelationHandling; } set { base.Motor.LedgeAndDenivelationHandling = value; } } public bool interactiveRigidbodyHandling { get { return base.Motor.InteractiveRigidbodyHandling; } set { base.Motor.InteractiveRigidbodyHandling = value; } } public Run.FixedTimeStamp lastGroundedTime { get; private set; } = Run.FixedTimeStamp.negativeInfinity; public CharacterGravityParameters gravityParameters { get { return _gravityParameters; } set { if (!_gravityParameters.Equals(value)) { _gravityParameters = value; useGravity = _gravityParameters.CheckShouldUseGravity(); } } } public bool useGravity { get; private set; } public CharacterFlightParameters flightParameters { get { return _flightParameters; } set { if (!_flightParameters.Equals(value)) { _flightParameters = value; isFlying = _flightParameters.CheckShouldUseFlight(); } } } public bool isFlying { get; private set; } [Obsolete("Use '.onHitGroundAuthority' instead, which this is just a backwards-compatibility wrapper for. Or, use '.onHitGroundAuthority' if that is more appropriate to your use case.", false)] public event HitGroundDelegate onHitGround { add { onHitGroundAuthority += value; } remove { onHitGroundAuthority -= value; } } public event HitGroundDelegate onHitGroundAuthority; public event Action onMotorStart; public event MovementHitDelegate onMovementHit; private void UpdateInnerMotorEnabled() { base.Motor.enabled = enableMotorWithoutAuthority || hasEffectiveAuthority; } private void UpdateAuthority() { hasEffectiveAuthority = Util.HasEffectiveAuthority(base.gameObject); UpdateInnerMotorEnabled(); } private void Awake() { networkIdentity = GetComponent(); body = GetComponent(); capsuleCollider = GetComponent(); } private void Start() { previousPosition = base.transform.position; if (base.Motor == null) { SetupCharacterMotor(GetComponent()); } if (base.Motor.AttachedRigidbody != null) { base.Motor.AttachedRigidbody.mass = mass; } base.Motor.MaxStableSlopeAngle = 70f; base.Motor.MaxStableDenivelationAngle = 55f; base.Motor.RebuildCollidableLayers(); if (generateParametersOnAwake) { GenerateParameters(); } useGravity = gravityParameters.CheckShouldUseGravity(); isFlying = flightParameters.CheckShouldUseFlight(); UpdateAuthority(); this.onMotorStart?.Invoke(body); } public override void OnStartAuthority() { UpdateAuthority(); } public override void OnStopAuthority() { UpdateAuthority(); } private void OnEnable() { UpdateInnerMotorEnabled(); } private void OnDisable() { base.Motor.enabled = false; } private void PreMove(float deltaTime) { if (!hasEffectiveAuthority) { return; } float num = acceleration; if (isAirControlForced || !isGrounded) { num *= (disableAirControlUntilCollision ? 0f : airControl); } Vector3 vector = moveDirection; if (!isFlying) { vector.y = 0f; } if (body.isSprinting) { float magnitude = vector.magnitude; if (magnitude < 1f && magnitude > 0f) { float num2 = 1f / vector.magnitude; vector *= num2; } } Vector3 target = vector * walkSpeed; if (!isFlying) { target.y = velocity.y; } velocity = Vector3.MoveTowards(velocity, target, num * deltaTime); if (useGravity) { ref float y = ref velocity.y; y += Physics.gravity.y * deltaTime; if (isGrounded) { y = Mathf.Max(y, 0f); } } } public void OnDeathStart() { alive = false; } private void FixedUpdate() { float fixedDeltaTime = Time.fixedDeltaTime; if (fixedDeltaTime != 0f) { Vector3 position = base.transform.position; if ((previousPosition - position).sqrMagnitude < 0.00062500004f * fixedDeltaTime) { restStopwatch += fixedDeltaTime; } else { restStopwatch = 0f; } previousPosition = position; if (netIsGrounded) { lastGroundedTime = Run.FixedTimeStamp.now; } } } private void GenerateParameters() { slopeLimit = 70f; stepOffset = Mathf.Min(capsuleHeight * 0.1f, 0.2f); stepHandlingMethod = StepHandlingMethod.None; ledgeHandling = false; interactiveRigidbodyHandling = true; } public void ApplyForce(Vector3 force, bool alwaysApply = false, bool disableAirControlUntilCollision = false) { PhysForceInfo forceInfo = PhysForceInfo.Create(); forceInfo.force = force; forceInfo.ignoreGroundStick = alwaysApply; forceInfo.disableAirControlUntilCollision = disableAirControlUntilCollision; forceInfo.massIsOne = false; ApplyForceImpulse(in forceInfo); } public void ApplyForceImpulse(in PhysForceInfo forceInfo) { if (NetworkServer.active && !hasEffectiveAuthority) { CallRpcApplyForceImpulse(forceInfo); return; } Vector3 force = forceInfo.force; if (!forceInfo.massIsOne) { _ = force.magnitude; force *= 1f / mass; } if (mass != 0f) { if (force.y < 6f && isGrounded && !forceInfo.ignoreGroundStick) { force.y = 0f; } if (force.y > 0f) { base.Motor.ForceUnground(); } velocity += force; if (forceInfo.disableAirControlUntilCollision) { disableAirControlUntilCollision = true; } } } [ClientRpc] private void RpcApplyForceImpulse(PhysForceInfo physForceInfo) { if (!NetworkServer.active) { ApplyForceImpulse(in physForceInfo); } } public override void UpdateRotation(ref Quaternion currentRotation, float deltaTime) { currentRotation = Quaternion.identity; } public override void UpdateVelocity(ref Vector3 currentVelocity, float deltaTime) { currentVelocity = velocity; } public override void BeforeCharacterUpdate(float deltaTime) { _ = cvCMotorSafeCollisionStepThreshold.value; _ = cvCMotorSafeCollisionStepThreshold.value; if (rootMotion != Vector3.zero) { Vector3 vector = rootMotion; rootMotion = Vector3.zero; base.Motor.MoveCharacter(base.transform.position + vector); } PreMove(deltaTime); _ = (velocity * deltaTime).sqrMagnitude; } public override void PostGroundingUpdate(float deltaTime) { if (base.Motor.GroundingStatus.IsStableOnGround != base.Motor.LastGroundingStatus.IsStableOnGround) { netIsGrounded = base.Motor.GroundingStatus.IsStableOnGround; if (base.Motor.GroundingStatus.IsStableOnGround) { OnLanded(); } else { OnLeaveStableGround(); } } } private void OnLanded() { jumpCount = 0; HitGroundInfo hitGroundInfo = default(HitGroundInfo); hitGroundInfo.velocity = lastVelocity; hitGroundInfo.position = base.Motor.GroundingStatus.GroundPoint; hitGroundInfo.isValidForEffect = Run.FixedTimeStamp.now - lastGroundedTime > 0.2f; HitGroundInfo hitGroundInfo2 = hitGroundInfo; if (hasEffectiveAuthority) { try { this.onHitGroundAuthority?.Invoke(ref hitGroundInfo2); } catch (Exception message) { Debug.LogError(message); } if (NetworkServer.active) { GlobalEventManager.instance.OnCharacterHitGroundServer(body, hitGroundInfo2); } else { CallCmdReportHitGround(hitGroundInfo2); } } } [ClientRpc] public void RpcOnHitGroundSFX(HitGroundInfo hitGroundInfo, bool tookFallDamage) { if (!NetworkServer.active) { GlobalEventManager.instance.OnCharacterHitGroundSFX(body, hitGroundInfo, tookFallDamage); } } [Command] private void CmdReportHitGround(HitGroundInfo hitGroundInfo) { GlobalEventManager.instance.OnCharacterHitGroundServer(body, hitGroundInfo); } private void OnLeaveStableGround() { if (jumpCount < 1) { jumpCount = 1; } body.SetInLava(b: false); } public override void AfterCharacterUpdate(float deltaTime) { lastVelocity = velocity; velocity = base.Motor.BaseVelocity; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public override bool IsColliderValidForCollisions(Collider coll) { if (!coll.isTrigger) { return coll != base.Motor.Capsule; } return false; } public override void OnGroundHit(Collider hitCollider, Vector3 hitNormal, Vector3 hitPoint, ref HitStabilityReport hitStabilityReport) { isAirControlForced = false; SurfaceDef objectSurfaceDef = SurfaceDefProvider.GetObjectSurfaceDef(hitCollider, hitPoint); if ((bool)objectSurfaceDef) { isAirControlForced = objectSurfaceDef.isSlippery; body.SetInLava(objectSurfaceDef.isLava); } } public override void OnMovementHit(Collider hitCollider, Vector3 hitNormal, Vector3 hitPoint, ref HitStabilityReport hitStabilityReport) { disableAirControlUntilCollision = false; if (this.onMovementHit != null) { MovementHitInfo movementHitInfo = default(MovementHitInfo); movementHitInfo.velocity = velocity; movementHitInfo.hitCollider = hitCollider; MovementHitInfo movementHitInfo2 = movementHitInfo; this.onMovementHit(ref movementHitInfo2); SurfaceDef objectSurfaceDef = SurfaceDefProvider.GetObjectSurfaceDef(hitCollider, hitPoint); if ((bool)objectSurfaceDef) { body.SetInLava(objectSurfaceDef.isLava); } } } public override void ProcessHitStabilityReport(Collider hitCollider, Vector3 hitNormal, Vector3 hitPoint, Vector3 atCharacterPosition, Quaternion atCharacterRotation, ref HitStabilityReport hitStabilityReport) { } public void Jump(float horizontalMultiplier, float verticalMultiplier, bool vault = false) { Vector3 vector = moveDirection; if (vault) { velocity = vector; } else { vector.y = 0f; float magnitude = vector.magnitude; if (magnitude > 0f) { vector /= magnitude; } Vector3 vector2 = vector * body.moveSpeed * horizontalMultiplier; vector2.y = body.jumpPower * verticalMultiplier; velocity = vector2; } base.Motor.ForceUnground(); } public void AddDisplacement(Vector3 displacement) { rootMotion += displacement; } static CharacterMotor() { enableMotorWithoutAuthority = false; cvCMotorSafeCollisionStepThreshold = new FloatConVar("cmotor_safe_collision_step_threshold", ConVarFlags.Cheat, 1.0833334f.ToString(CultureInfo.InvariantCulture), "How large of a movement in meters/fixedTimeStep is needed to trigger more expensive \"safe\" collisions to prevent tunneling."); kCmdCmdReportHitGround = 1796547162; NetworkBehaviour.RegisterCommandDelegate(typeof(CharacterMotor), kCmdCmdReportHitGround, InvokeCmdCmdReportHitGround); kRpcRpcApplyForceImpulse = 1042934326; NetworkBehaviour.RegisterRpcDelegate(typeof(CharacterMotor), kRpcRpcApplyForceImpulse, InvokeRpcRpcApplyForceImpulse); kRpcRpcOnHitGroundSFX = -1661136052; NetworkBehaviour.RegisterRpcDelegate(typeof(CharacterMotor), kRpcRpcOnHitGroundSFX, InvokeRpcRpcOnHitGroundSFX); NetworkCRC.RegisterBehaviour("CharacterMotor", 0); } void IPhysMotor.ApplyForceImpulse(in PhysForceInfo physForceInfo) { ApplyForceImpulse(in physForceInfo); } private void UNetVersion() { } protected static void InvokeCmdCmdReportHitGround(NetworkBehaviour obj, NetworkReader reader) { if (!NetworkServer.active) { Debug.LogError("Command CmdReportHitGround called on client."); } else { ((CharacterMotor)obj).CmdReportHitGround(GeneratedNetworkCode._ReadHitGroundInfo_CharacterMotor(reader)); } } public void CallCmdReportHitGround(HitGroundInfo hitGroundInfo) { if (!NetworkClient.active) { Debug.LogError("Command function CmdReportHitGround called on server."); return; } if (base.isServer) { CmdReportHitGround(hitGroundInfo); return; } NetworkWriter networkWriter = new NetworkWriter(); networkWriter.Write((short)0); networkWriter.Write((short)5); networkWriter.WritePackedUInt32((uint)kCmdCmdReportHitGround); networkWriter.Write(GetComponent().netId); GeneratedNetworkCode._WriteHitGroundInfo_CharacterMotor(networkWriter, hitGroundInfo); SendCommandInternal(networkWriter, 0, "CmdReportHitGround"); } protected static void InvokeRpcRpcApplyForceImpulse(NetworkBehaviour obj, NetworkReader reader) { if (!NetworkClient.active) { Debug.LogError("RPC RpcApplyForceImpulse called on server."); } else { ((CharacterMotor)obj).RpcApplyForceImpulse(GeneratedNetworkCode._ReadPhysForceInfo_None(reader)); } } protected static void InvokeRpcRpcOnHitGroundSFX(NetworkBehaviour obj, NetworkReader reader) { if (!NetworkClient.active) { Debug.LogError("RPC RpcOnHitGroundSFX called on server."); } else { ((CharacterMotor)obj).RpcOnHitGroundSFX(GeneratedNetworkCode._ReadHitGroundInfo_CharacterMotor(reader), reader.ReadBoolean()); } } public void CallRpcApplyForceImpulse(PhysForceInfo physForceInfo) { if (!NetworkServer.active) { Debug.LogError("RPC Function RpcApplyForceImpulse called on client."); return; } NetworkWriter networkWriter = new NetworkWriter(); networkWriter.Write((short)0); networkWriter.Write((short)2); networkWriter.WritePackedUInt32((uint)kRpcRpcApplyForceImpulse); networkWriter.Write(GetComponent().netId); GeneratedNetworkCode._WritePhysForceInfo_None(networkWriter, physForceInfo); SendRPCInternal(networkWriter, 0, "RpcApplyForceImpulse"); } public void CallRpcOnHitGroundSFX(HitGroundInfo hitGroundInfo, bool tookFallDamage) { if (!NetworkServer.active) { Debug.LogError("RPC Function RpcOnHitGroundSFX called on client."); return; } NetworkWriter networkWriter = new NetworkWriter(); networkWriter.Write((short)0); networkWriter.Write((short)2); networkWriter.WritePackedUInt32((uint)kRpcRpcOnHitGroundSFX); networkWriter.Write(GetComponent().netId); GeneratedNetworkCode._WriteHitGroundInfo_CharacterMotor(networkWriter, hitGroundInfo); networkWriter.Write(tookFallDamage); SendRPCInternal(networkWriter, 0, "RpcOnHitGroundSFX"); } public override bool OnSerialize(NetworkWriter writer, bool forceAll) { bool flag = base.OnSerialize(writer, forceAll); bool flag2 = default(bool); return flag2 || flag; } public override void OnDeserialize(NetworkReader reader, bool initialState) { base.OnDeserialize(reader, initialState); } public override void PreStartClient() { base.PreStartClient(); } }