first commit

This commit is contained in:
2025-07-06 00:23:46 +02:00
commit 38f50c8819
1788 changed files with 112878 additions and 0 deletions

View File

@@ -0,0 +1,378 @@
using NitroxClient.GameLogic;
using NitroxClient.MonoBehaviours.Cyclops;
using UnityEngine;
using UnityEngine.XR;
namespace NitroxClient.MonoBehaviours;
/// <summary>
/// A replacement for <see cref="GroundMotor"/> while Local Player is in a Cyclops.
/// </summary>
public partial class CyclopsMotor : GroundMotor
{
public GroundMotor ActualMotor { get; private set; }
public CyclopsPawn Pawn;
private Transform body;
private NitroxCyclops cyclops;
private SubRoot sub;
private Transform realAxis;
private Transform virtualAxis;
private WorldForces worldForces;
public Vector3 Up => virtualAxis.up;
public float DeltaTime => Time.fixedDeltaTime;
private Vector3 verticalVelocity;
private Vector3 latestVelocity;
public new void Awake()
{
controller = GetComponent<CharacterController>();
controller.enabled = false;
controller.stepOffset = controllerSetup.stepOffset;
controller.slopeLimit = controllerSetup.slopeLimit;
rb = GetComponent<Rigidbody>();
playerController = GetComponent<PlayerController>();
worldForces = GetComponent<WorldForces>();
body = Player.mainObject.transform.Find("body");
}
public override void SetEnabled(bool enabled)
{
base.SetEnabled(enabled);
Setup(enabled);
}
public void Initialize(GroundMotor reference)
{
ActualMotor = reference;
movement = reference.movement;
jumping = reference.jumping;
movingPlatform = reference.movingPlatform;
sliding = reference.sliding;
controllerSetup = reference.controllerSetup;
floatingModeSetup = reference.floatingModeSetup;
allowMidAirJumping = reference.allowMidAirJumping;
minWindSpeedToAffectMovement = reference.minWindSpeedToAffectMovement;
percentWindDampeningOnGround = reference.percentWindDampeningOnGround;
percentWindDampeningInAir = reference.percentWindDampeningInAir;
floatingModeEnabled = reference.floatingModeEnabled;
forwardMaxSpeed = reference.forwardMaxSpeed;
backwardMaxSpeed = reference.backwardMaxSpeed;
strafeMaxSpeed = reference.strafeMaxSpeed;
verticalMaxSpeed = reference.verticalMaxSpeed;
climbSpeed = reference.climbSpeed;
gravity = reference.gravity;
forwardSprintModifier = reference.forwardSprintModifier;
strafeSprintModifier = reference.strafeSprintModifier;
groundAcceleration = reference.groundAcceleration;
airAcceleration = reference.airAcceleration;
jumpHeight = reference.jumpHeight;
SetEnabled(false);
RecalculateConstants();
}
public void SetCyclops(NitroxCyclops cyclops, SubRoot subRoot, CyclopsPawn pawn)
{
this.cyclops = cyclops;
sub = subRoot;
realAxis = sub.subAxis;
virtualAxis = cyclops.Virtual.axis;
Pawn = pawn;
}
public void Setup(bool enabled)
{
verticalVelocity = Vector3.zero;
latestVelocity = Vector3.zero;
if (enabled)
{
rb.isKinematic = false;
Player.wantInterpolate = false;
rb.detectCollisions = false;
worldForces.lockInterpolation = true;
worldForces.enabled = false;
controller.detectCollisions = false;
Player.mainCollider.isTrigger = true;
UWE.Utils.EnterPhysicsSyncSection();
}
else
{
rb.isKinematic = true;
Player.wantInterpolate = true;
worldForces.lockInterpolation = false;
rb.detectCollisions = true;
worldForces.enabled = true;
controller.detectCollisions = true;
Player.mainCollider.isTrigger = false;
UWE.Utils.ExitPhysicsSyncSection();
}
Pawn?.SetReference();
}
public override Vector3 UpdateMove()
{
if (!canControl)
{
return Vector3.zero;
}
// Compute movements velocities based on inputs and previous movement
Position = Pawn.Position;
Center = cyclops.Virtual.transform.TransformVector(Pawn.Controller.center);
Pawn.Handle.transform.localRotation = body.localRotation;
sprinting = false;
verticalVelocity += CalculateVerticalVelocity();
Vector3 horizontalVelocity = CalculateInputVelocity();
// movement.velocity gives velocity info for the animations and footsteps
movement.velocity = Move(horizontalVelocity);
return movement.velocity;
}
/// <summary>
/// Simulates player movement on its pawn and update the grounded state
/// </summary>
/// <remarks>
/// Adapted from <see cref="GroundMotor.UpdateFunction"/>
/// </remarks>
/// <returns>Pawn's local velocity</returns>
public Vector3 Move(Vector3 horizontalVelocity)
{
Vector3 beforePosition = Pawn.Position;
Vector3 velocity = new(horizontalVelocity.x, verticalVelocity.y, horizontalVelocity.z);
Vector3 movementThisFrame = velocity * DeltaTime;
float step = Mathf.Max(Pawn.Controller.stepOffset, Mathf.Sqrt(movementThisFrame.x * movementThisFrame.x + movementThisFrame.z * movementThisFrame.z));
if (grounded)
{
movementThisFrame -= step * Up;
}
Collision = Pawn.Controller.Move(movementThisFrame);
float verticalDot = Vector3.Dot(verticalVelocity, Up);
bool previouslyGrounded = grounded;
CheckGrounded(Collision, verticalDot <= 0f);
Vector3 velocityXZ = velocity._X0Z();
Vector3 instantVelocity = (Pawn.Position - beforePosition) / DeltaTime;
if (instantVelocity.sqrMagnitude <= 0.2f)
{
instantVelocity = velocity;
}
if (instantVelocity.y > 0f || Collision == CollisionFlags.None)
{
instantVelocity.y = velocity.y;
}
latestVelocity = instantVelocity;
Vector3 instantVelocityXZ = instantVelocity._X0Z();
if (velocityXZ == Vector3.zero)
{
latestVelocity = latestVelocity._0Y0();
}
else
{
float deviation = Vector3.Dot(instantVelocityXZ, velocityXZ) / velocityXZ.sqrMagnitude;
latestVelocity = velocityXZ * Mathf.Clamp01(deviation) + latestVelocity.y * Up;
}
if (latestVelocity.y < velocity.y - 0.001)
{
if (latestVelocity.y < 0f)
{
latestVelocity.y = velocity.y;
}
else
{
jumping.holdingJumpButton = false;
}
}
if (grounded)
{
verticalVelocity = Vector3.zero;
if (!previouslyGrounded)
{
jumping.jumping = false;
// Prefilled data is made to not hurt the player at any time when colliding with cyclops, but only to play the noise
SendMessage(nameof(Player.OnLand), new MovementCollisionData
{
impactVelocity = Vector3.one,
surfaceType = VFXSurfaceTypes.metal
}, SendMessageOptions.DontRequireReceiver);
}
}
// If player is no longer grounded after move
else if (previouslyGrounded)
{
SendMessage("OnFall", SendMessageOptions.DontRequireReceiver);
Pawn.Handle.transform.localPosition += step * Up;
}
return cyclops.transform.rotation * latestVelocity;
}
/// <summary>
/// Calculates vertical velocity variation based on the grounded state.
/// Code adapted from <see cref="GroundMotor.ApplyGravityAndJumping"/>.
/// </summary>
public Vector3 CalculateVerticalVelocity()
{
if (!jumpPressed)
{
jumping.holdingJumpButton = false;
jumping.lastButtonDownTime = -100f;
}
if (jumpPressed && (jumping.lastButtonDownTime < 0f || flyCheatEnabled))
{
jumping.lastButtonDownTime = Time.time;
}
Vector3 verticalMove = Vector3.zero;
if (!grounded)
{
verticalMove = -gravity * Up * DeltaTime;
verticalMove.y = Mathf.Max(verticalMove.y, -movement.maxFallSpeed);
}
if (grounded || allowMidAirJumping || flyCheatEnabled)
{
if (Time.time - jumping.lastButtonDownTime < 0.2)
{
grounded = false;
jumping.jumping = true;
jumping.lastStartTime = Time.time;
jumping.lastButtonDownTime = -100f;
jumping.holdingJumpButton = true;
Vector3 jumpDirection = Vector3.Slerp(Up, groundNormal, TooSteep() ? jumping.steepPerpAmount : jumping.perpAmount);
verticalMove = jumpDirection * CalculateJumpVerticalSpeed(jumping.baseHeight);
SendMessage("OnJump", SendMessageOptions.DontRequireReceiver);
}
else
{
jumping.holdingJumpButton = false;
}
}
return verticalMove;
}
/// <summary>
/// Calculates instantaneous horizontal velocity from input for the <see cref="Pawn"/> object.
/// Code adapted from <see cref="GroundMotor.ApplyInputVelocityChange"/>.
/// </summary>
public Vector3 CalculateInputVelocity()
{
// Project the movement input to the right rotation
float moveMinMagnitude = Mathf.Min(1f, movementInputDirection.magnitude);
// We rotate the input in the right basis
Vector3 input = movementInputDirection._X0Z();
Transform forwardRef = Pawn.Handle.transform;
Vector3 projectedForward = Vector3.ProjectOnPlane(forwardRef.forward, Up).normalized;
Vector3 projectedRight = Vector3.ProjectOnPlane(forwardRef.right, Up).normalized;
Vector3 moveDirection = (projectedForward * input.z + projectedRight * input.x).normalized;
Vector3 velocity;
// Manage sliding on slopes
if (grounded && TooSteep())
{
velocity = GetSlidingDirection();
Vector3 moveProjectedOnSlope = Vector3.Project(movementInputDirection, velocity);
velocity += moveProjectedOnSlope * sliding.speedControl + (movementInputDirection - moveProjectedOnSlope) * sliding.sidewaysControl;
velocity *= sliding.slidingSpeed;
}
else
{
// Apply speed modifiers
float modifier = 1f;
if (sprintPressed && grounded)
{
float z = movementInputDirection.z;
if (z > 0f)
{
modifier *= forwardSprintModifier;
}
else if (z == 0f)
{
modifier *= strafeSprintModifier;
}
sprinting = true;
}
velocity = moveDirection * forwardMaxSpeed * modifier * moveMinMagnitude;
}
if (XRSettings.enabled)
{
velocity *= VROptions.groundMoveScale;
}
if (grounded)
{
velocity = AdjustGroundVelocityToNormal(velocity, groundNormal);
}
else
{
latestVelocity.y = 0f;
}
float maxSpeed = GetMaxAcceleration(grounded) * DeltaTime;
Vector3 difference = velocity - latestVelocity;
if (difference.sqrMagnitude > maxSpeed * maxSpeed)
{
difference = difference.normalized * maxSpeed;
}
latestVelocity += difference;
if (grounded)
{
latestVelocity.y = Mathf.Min(latestVelocity.y, 0f);
}
return latestVelocity;
}
private new Vector3 GetSlidingDirection()
{
return Vector3.ProjectOnPlane(groundNormal, Up).normalized;
}
private new bool TooSteep()
{
float dotUp = Vector3.Dot(groundNormal, Up);
return dotUp <= Mathf.Cos(controller.slopeLimit * Mathf.Deg2Rad);
}
public void ToggleCyclopsMotor(bool toggled)
{
GroundMotor groundMotor = toggled ? this : ActualMotor;
Player.main.playerController.SetEnabled(false);
Player.main.groundMotor = groundMotor;
Player.main.footStepSounds.groundMoveable = groundMotor;
Player.main.groundMotor = groundMotor;
Player.main.playerController.groundController = groundMotor;
if (Player.main.playerController.activeController is GroundMotor)
{
Player.main.playerController.activeController = groundMotor;
}
// SetMotorMode sets some important variables in the motor abstract class PlayerMotor
Player.main.playerController.SetMotorMode(Player.MotorMode.Walk);
Player.main.playerController.SetEnabled(true);
}
}

View File

@@ -0,0 +1,116 @@
using NitroxClient.GameLogic;
using UnityEngine;
namespace NitroxClient.MonoBehaviours;
/// <remarks>
/// Ground detection adapted from <see href="https://github.com/Unity-Technologies/Standard-Assets-Characters/blob/master/Assets/_Standard%20Assets/Characters/Scripts/Physics/OpenCharacterController.cs"/>
/// </remarks>
public partial class CyclopsMotor
{
private const float CAST_DISTANCE = 0.001f;
private const float CAST_EXTRA_DISTANCE = 0.001f;
public const QueryTriggerInteraction QuerySetting = QueryTriggerInteraction.Ignore;
public static readonly int LayerMaskExceptPlayer = ~CyclopsPawn.PLAYER_LAYER;
/// <summary>
/// Latest snapshot of the Pawn's global position. It is updated every frame before being used.
/// </summary>
public Vector3 Position;
/// <summary>
/// Latest snapshot of the globally transformed center offset. It is updated every frame before being used.
/// </summary>
public Vector3 Center;
/// <summary>
/// <see cref="CharacterController.height"/> scaled by the transform's y global scale
/// </summary>
public float Height;
/// <summary>
/// <see cref="CharacterController.radius"/> scaled by the transform's maximum global scale parameter
/// </summary>
public float Radius;
/// <summary>
/// Unscaled <see cref="CharacterController.skinWidth"/>
/// </summary>
public float SkinWidth;
/// <summary>
/// Snapshot of the latest <see cref="CollisionFlags"/> obtained when simulating movement on the pawn.
/// </summary>
private CollisionFlags Collision { get; set; }
/// <summary>
/// Checks if Pawn is grounded by up to 2 sphere casts. Updates the registered ground normal accordingly.
/// </summary>
public void CheckGrounded(CollisionFlags flags, bool cast)
{
if (cast)
{
Vector3 lowerPoint = GetLowerPoint();
grounded = false;
if (SphereCast(-Up, SkinWidth + CAST_DISTANCE, out RaycastHit hitInfo, lowerPoint, false))
{
grounded = true;
hitInfo.distance = Mathf.Max(0f, hitInfo.distance - SkinWidth);
}
if (!grounded && SphereCast(-Up, CAST_DISTANCE + CAST_EXTRA_DISTANCE, out hitInfo, lowerPoint + Up * CAST_EXTRA_DISTANCE, true))
{
grounded = true;
hitInfo.distance = Mathf.Max(0f, hitInfo.distance - SkinWidth);
}
groundNormal = hitInfo.normal;
return;
}
// Exceptional case in which movement was made on the ground but the casts failed
if (flags == CollisionFlags.Below)
{
grounded = true;
groundNormal = Up;
return;
}
grounded = false;
groundNormal = Vector3.zero;
}
public bool SphereCast(Vector3 direction, float distance, out RaycastHit hitInfo, Vector3 spherePosition, bool big)
{
float radius = big ? Radius + SkinWidth : Radius;
if (Physics.SphereCast(spherePosition, radius, direction, out hitInfo, distance + radius, LayerMaskExceptPlayer, QuerySetting))
{
return hitInfo.distance <= distance;
}
return false;
}
public Vector3 GetLowerPoint()
{
return Position + Center - Up * (Height * 0.5f - Radius);
}
public override void SetControllerHeight(float height, float cameraOffset)
{
base.SetControllerHeight(height, cameraOffset);
RecalculateConstants();
}
public override void SetControllerRadius(float radius)
{
base.SetControllerRadius(radius);
RecalculateConstants();
}
private void RecalculateConstants()
{
Vector3 scale = transform.lossyScale;
Height = controller.height * scale.y;
Radius = controller.radius * Mathf.Max(Mathf.Max(scale.x, scale.y), scale.z);
SkinWidth = controller.skinWidth;
}
}

View File

@@ -0,0 +1,177 @@
using System.Collections.Generic;
using NitroxClient.GameLogic;
using NitroxClient.GameLogic.PlayerLogic;
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract;
using UnityEngine;
namespace NitroxClient.MonoBehaviours.Cyclops;
/// <summary>
/// Script responsible for managing all player movement-related interactions.
/// </summary>
public class NitroxCyclops : MonoBehaviour
{
public VirtualCyclops Virtual;
private CyclopsMotor cyclopsMotor;
private SubRoot subRoot;
private SubControl subControl;
private Rigidbody rigidbody;
private WorldForces worldForces;
private Stabilizer stabilizer;
private CharacterController controller;
private CyclopsNoiseManager cyclopsNoiseManager;
public readonly Dictionary<INitroxPlayer, CyclopsPawn> Pawns = [];
public static readonly Dictionary<NitroxCyclops, float> ScaledNoiseByCyclops = [];
public void Start()
{
cyclopsMotor = Player.mainObject.GetComponent<CyclopsMotor>();
subRoot = GetComponent<SubRoot>();
subControl = GetComponent<SubControl>();
rigidbody = GetComponent<Rigidbody>();
worldForces = GetComponent<WorldForces>();
stabilizer = GetComponent<Stabilizer>();
controller = cyclopsMotor.controller;
cyclopsNoiseManager = GetComponent<CyclopsNoiseManager>();
UWE.Utils.SetIsKinematicAndUpdateInterpolation(rigidbody, false, true);
WorkaroundColliders();
ScaledNoiseByCyclops.Add(this, 0f);
}
public void Update()
{
MaintainPawns();
// Calculation from AttackCyclops.UpdateAggression
ScaledNoiseByCyclops[this] = Mathf.Lerp(0f, 150f, cyclopsNoiseManager.GetNoisePercent());
}
public void OnDestroy()
{
ScaledNoiseByCyclops.Remove(this);
}
/// <summary>
/// Triggers required on-remove callbacks on children player objects, including the local player.
/// </summary>
public void RemoveAllPlayers()
{
// This will call OnLocalPlayerExit
if (Player.main.currentSub == subRoot)
{
Player.main.SetCurrentSub(null);
}
foreach (RemotePlayerIdentifier remotePlayerIdentifier in GetComponentsInChildren<RemotePlayerIdentifier>(true))
{
remotePlayerIdentifier.RemotePlayer.ResetStates();
OnPlayerExit(remotePlayerIdentifier.RemotePlayer);
}
}
/// <summary>
/// Parents local player to the cyclops and registers it in the current cyclops.
/// </summary>
public void OnLocalPlayerEnter()
{
Virtual = VirtualCyclops.Instance;
Virtual.SetCurrentCyclops(this);
Player.mainObject.transform.parent = subRoot.transform;
CyclopsPawn pawn = AddPawnForPlayer(this.Resolve<ILocalNitroxPlayer>());
cyclopsMotor.SetCyclops(this, subRoot, pawn);
cyclopsMotor.ToggleCyclopsMotor(true);
}
/// <summary>
/// Unregisters the local player from the current cyclops. Ensures the player is not weirdly rotated when it leaves the cyclops.
/// </summary>
public void OnLocalPlayerExit()
{
RemovePawnForPlayer(this.Resolve<ILocalNitroxPlayer>());
Player.main.transform.parent = null;
Player.main.transform.rotation = Quaternion.identity;
cyclopsMotor.ToggleCyclopsMotor(false);
cyclopsMotor.Pawn = null;
if (Virtual)
{
Virtual.SetCurrentCyclops(null);
}
}
/// <summary>
/// Registers a remote player for it to get a pawn in the current cyclops.
/// </summary>
public void OnPlayerEnter(RemotePlayer remotePlayer)
{
remotePlayer.Pawn = AddPawnForPlayer(remotePlayer);
}
/// <summary>
/// Unregisters a remote player from the current cyclops.
/// </summary>
public void OnPlayerExit(RemotePlayer remotePlayer)
{
RemovePawnForPlayer(remotePlayer);
remotePlayer.Pawn = null;
}
public void MaintainPawns()
{
foreach (CyclopsPawn pawn in Pawns.Values)
{
if (pawn.MaintainPredicate())
{
pawn.MaintainPosition();
}
}
}
public CyclopsPawn AddPawnForPlayer(INitroxPlayer player)
{
if (!Pawns.TryGetValue(player, out CyclopsPawn pawn))
{
pawn = new(player, this);
Pawns.Add(player, pawn);
}
return pawn;
}
public void RemovePawnForPlayer(INitroxPlayer player)
{
if (Pawns.TryGetValue(player, out CyclopsPawn pawn))
{
pawn.Terminate();
}
Pawns.Remove(player);
}
public void SetBroadcasting()
{
worldForces.enabled = true;
stabilizer.stabilizerEnabled = true;
}
public void SetReceiving()
{
worldForces.enabled = false;
stabilizer.stabilizerEnabled = false;
}
private void WorkaroundColliders()
{
CyclopsSubNameScreen cyclopsSubNameScreen = transform.GetComponentInChildren<CyclopsSubNameScreen>(true);
TriggerWorkaround subNameTriggerWorkaround = cyclopsSubNameScreen.gameObject.AddComponent<TriggerWorkaround>();
subNameTriggerWorkaround.Initialize(this,cyclopsSubNameScreen.animator, cyclopsSubNameScreen.ContentOn, nameof(CyclopsSubNameScreen.ContentOff), cyclopsSubNameScreen);
CyclopsLightingPanel cyclopsLightingPanel = transform.GetComponentInChildren<CyclopsLightingPanel>(true);
TriggerWorkaround lightingTriggerWorkaround = cyclopsLightingPanel.gameObject.AddComponent<TriggerWorkaround>();
lightingTriggerWorkaround.Initialize(this, cyclopsLightingPanel.uiPanel, cyclopsLightingPanel.ButtonsOn, nameof(CyclopsLightingPanel.ButtonsOff), cyclopsLightingPanel);
}
}

View File

@@ -0,0 +1,59 @@
using System;
using UnityEngine;
namespace NitroxClient.MonoBehaviours.Cyclops;
/// <summary>
/// With the changes to the Player's colliders, the cyclops doesn't detect the player entering or leaving to triggers
/// The easiest workaround is to replace proximity detection by distance checks.
/// </summary>
/// <remarks>
/// Works for <see cref="CyclopsLightingPanel"/> and <see cref="CyclopsSubNameScreen"/>.
/// </remarks>
public class TriggerWorkaround : MonoBehaviour
{
private const float DETECTION_RANGE = 5f;
private const string ANIMATOR_PARAM = "PanelActive";
private bool playerIn;
private NitroxCyclops cyclops;
private Animator animator;
private Action onEnterCallback;
private string onExitInvokeCallback;
private MonoBehaviour targetBehaviour;
public void Initialize(NitroxCyclops cyclops, Animator animator, Action onEnterCallback, string onExitInvokeCallback, MonoBehaviour targetBehaviour)
{
this.cyclops = cyclops;
this.animator = animator;
this.onEnterCallback = onEnterCallback;
this.onExitInvokeCallback = onExitInvokeCallback;
this.targetBehaviour = targetBehaviour;
}
/// <summary>
/// Code adapted from <see cref="CyclopsSubNameScreen.OnTriggerEnter"/> and <see cref="CyclopsSubNameScreen.OnTriggerExit"/>
/// </summary>
public void Update()
{
// Virtual is not null only when the local player is aboard
if (cyclops.Virtual && Vector3.Distance(Player.main.transform.position, transform.position) < DETECTION_RANGE)
{
if (!playerIn)
{
playerIn = true;
animator.SetBool(ANIMATOR_PARAM, true);
onEnterCallback();
targetBehaviour.CancelInvoke(onExitInvokeCallback);
}
return;
}
if (playerIn)
{
playerIn = false;
animator.SetBool(ANIMATOR_PARAM, false);
targetBehaviour.Invoke(onExitInvokeCallback, 0.5f);
}
}
}

View File

@@ -0,0 +1,290 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using NitroxClient.Communication;
using NitroxClient.GameLogic.Spawning.WorldEntities;
using NitroxModel.Packets;
using UnityEngine;
namespace NitroxClient.MonoBehaviours.Cyclops;
/// <summary>
/// Script responsible for creating a virtual counterpart of every cyclops, which will always be horizontal and motionless so that simulated movement is always clear.
/// Contains a pawn for each player entering the regular cyclops.
/// </summary>
public class VirtualCyclops : MonoBehaviour
{
public static VirtualCyclops Instance;
public const string NAME = "VirtualCyclops";
private static readonly Dictionary<TechType, GameObject> cacheColliderCopy = [];
private readonly Dictionary<string, Openable> virtualOpenableByName = [];
private readonly Dictionary<string, Openable> realOpenableByName = [];
private readonly Dictionary<GameObject, GameObject> virtualConstructableByRealGameObject = [];
public NitroxCyclops Cyclops;
public Transform axis;
private Rigidbody rigidbody;
private Vector3 InitialPosition;
private Quaternion InitialRotation;
public static void Initialize()
{
CreateVirtualCyclops();
Multiplayer.OnAfterMultiplayerEnd += Dispose;
}
public static void Dispose()
{
Destroy(Instance.gameObject);
Instance = null;
Multiplayer.OnAfterMultiplayerEnd -= Dispose;
}
/// <summary>
/// Initializes the <see cref="Prefab"/> object with reduced utility to ensure the virtual cyclops won't be eating too much performance.
/// </summary>
public static void CreateVirtualCyclops()
{
if (Instance)
{
return;
}
LightmappedPrefabs.main.RequestScenePrefab("cyclops", (cyclopsPrefab) =>
{
SubConsoleCommand.main.OnSubPrefabLoaded(cyclopsPrefab);
GameObject model = SubConsoleCommand.main.GetLastCreatedSub();
model.name = NAME;
Vector3 position = Vector3.up * 500;
Quaternion rotation = Quaternion.identity;
model.transform.position = position;
model.transform.rotation = rotation;
Instance = model.AddComponent<VirtualCyclops>();
Instance.axis = model.GetComponent<SubRoot>().subAxis;
GameObject.Destroy(model.GetComponent<EcoTarget>());
GameObject.Destroy(model.GetComponent<PingInstance>());
GameObject.Destroy(model.GetComponent<CyclopsDestructionEvent>());
GameObject.Destroy(model.GetComponent<VFXConstructing>());
Instance.InitialPosition = position;
Instance.InitialRotation = rotation;
Instance.rigidbody = Instance.GetComponent<Rigidbody>();
Instance.rigidbody.constraints = RigidbodyConstraints.FreezeAll;
model.GetComponent<WorldForces>().enabled = false;
model.GetComponent<WorldForces>().lockInterpolation = false;
model.GetComponent<Stabilizer>().stabilizerEnabled = false;
model.GetComponent<Rigidbody>().isKinematic = true;
model.GetComponent<LiveMixin>().invincible = true;
Instance.RegisterVirtualOpenables();
Instance.ToggleRenderers(false);
Instance.DisableBadComponents();
model.SetActive(true);
});
}
public static IEnumerator InitializeConstructablesCache()
{
List<TechType> constructableTechTypes = [];
CraftData.GetBuilderGroupTech(TechGroup.InteriorModules, constructableTechTypes, true);
CraftData.GetBuilderGroupTech(TechGroup.Miscellaneous, constructableTechTypes, true);
TaskResult<GameObject> result = new();
foreach (TechType techType in constructableTechTypes)
{
yield return DefaultWorldEntitySpawner.RequestPrefab(techType, result);
if (result.value && result.value.GetComponent<Constructable>())
{
// We immediately destroy the copy because we only want to cache it for now
Destroy(CreateColliderCopy(result.value, techType));
}
}
}
public void Populate()
{
foreach (Constructable constructable in Cyclops.GetComponentsInChildren<Constructable>(true))
{
ReplicateConstructable(constructable);
}
foreach (Openable openable in Cyclops.GetComponentsInChildren<Openable>(true))
{
openable.blocked = false;
ReplicateOpening(openable, openable.isOpen);
realOpenableByName.Add(openable.name, openable);
}
}
public void Depopulate()
{
foreach (GameObject virtualObject in virtualConstructableByRealGameObject.Values)
{
Destroy(virtualObject);
}
virtualConstructableByRealGameObject.Clear();
foreach (Openable openable in realOpenableByName.Values)
{
openable.blocked = false;
}
realOpenableByName.Clear();
}
public void SetCurrentCyclops(NitroxCyclops nitroxCyclops)
{
if (Cyclops)
{
Cyclops.Virtual = null;
Depopulate();
Cyclops = null;
}
Cyclops = nitroxCyclops;
if (Cyclops)
{
Populate();
}
}
public void Update()
{
transform.position = InitialPosition;
transform.rotation = InitialRotation;
}
public void ToggleRenderers(bool toggled)
{
foreach (Renderer renderer in transform.GetComponentsInChildren<Renderer>(true))
{
renderer.enabled = toggled;
}
}
private void RegisterVirtualOpenables()
{
foreach (Openable openable in transform.GetComponentsInChildren<Openable>(true))
{
virtualOpenableByName.Add(openable.name, openable);
}
}
private void DisableBadComponents()
{
CyclopsLightingPanel cyclopsLightingPanel = GetComponentInChildren<CyclopsLightingPanel>(true);
cyclopsLightingPanel.floodlightsOn = false;
cyclopsLightingPanel.lightingOn = false;
cyclopsLightingPanel.SetExternalLighting(false);
cyclopsLightingPanel.cyclopsRoot.ForceLightingState(false);
cyclopsLightingPanel.enabled = false;
Destroy(cyclopsLightingPanel);
// Disable a source of useless logs
foreach (FMOD_CustomEmitter customEmitter in GetComponentsInChildren<FMOD_CustomEmitter>(true))
{
customEmitter.enabled = false;
}
foreach (PlayerCinematicController cinematicController in GetComponentsInChildren<PlayerCinematicController>(true))
{
cinematicController.enabled = false;
}
}
public void ReplicateOpening(Openable openable, bool openState)
{
if (virtualOpenableByName.TryGetValue(openable.name, out Openable virtualOpenable))
{
using (PacketSuppressor<OpenableStateChanged>.Suppress())
{
virtualOpenable.PlayOpenAnimation(openState, virtualOpenable.animTime);
}
}
}
public void ReplicateBlock(Openable openable, bool blockState)
{
if (realOpenableByName.TryGetValue(openable.name, out Openable realOpenable))
{
realOpenable.blocked = blockState;
}
}
public void ReplicateConstructable(Constructable constructable)
{
if (virtualConstructableByRealGameObject.ContainsKey(constructable.gameObject))
{
return;
}
GameObject colliderCopy = CreateColliderCopy(constructable.gameObject, constructable.techType);
colliderCopy.transform.parent = transform;
colliderCopy.transform.CopyLocals(constructable.transform);
virtualConstructableByRealGameObject.Add(constructable.gameObject, colliderCopy);
}
/// <summary>
/// Creates an empty shell simulating the presence of modules by copying its children containing a collider.
/// </summary>
public static GameObject CreateColliderCopy(GameObject realObject, TechType techType)
{
if (cacheColliderCopy.TryGetValue(techType, out GameObject colliderCopy))
{
return GameObject.Instantiate(colliderCopy);
}
colliderCopy = new GameObject($"{realObject.name}-collidercopy");
// This will act as a prefab but will stay in the material world so we put it out of hands in the meantime
colliderCopy.transform.position = Vector3.up * 1000 + Vector3.right * 10 * cacheColliderCopy.Count;
Transform transform = realObject.transform;
Dictionary<Transform, Transform> copiedTransformByRealTransform = [];
copiedTransformByRealTransform[transform] = colliderCopy.transform;
IEnumerable<GameObject> uniqueColliderObjects = realObject.GetComponentsInChildren<Collider>(true).Select(c => c.gameObject).Distinct();
foreach (GameObject colliderObject in uniqueColliderObjects)
{
GameObject copiedColliderObject = new(colliderObject.name);
copiedColliderObject.transform.CopyLocals(colliderObject.transform);
foreach (Collider collider in colliderObject.GetComponents<Collider>())
{
collider.CopyComponent(copiedColliderObject);
}
// "child" is always a copied transform looking for its copied parent
Transform child = copiedColliderObject.transform;
// "parent" is always the real parent of the real transform corresponding to "child"
Transform parent = colliderObject.transform.parent;
while (!copiedTransformByRealTransform.ContainsKey(parent))
{
Transform copiedParent = copiedTransformByRealTransform[parent] = Instantiate(parent);
child.SetParent(copiedParent, false);
child = copiedParent;
parent = parent.parent;
}
// At the top of the tree we can simply stick the latest child to the collider
child.SetParent(colliderCopy.transform, false);
}
cacheColliderCopy.Add(techType, colliderCopy);
return GameObject.Instantiate(colliderCopy);
}
public void UnregisterConstructable(GameObject realObject)
{
if (virtualConstructableByRealGameObject.TryGetValue(realObject, out GameObject virtualConstructable))
{
Destroy(virtualConstructable);
virtualConstructableByRealGameObject.Remove(realObject);
}
}
}