first commit
This commit is contained in:
72
NitroxClient/MonoBehaviours/AnimationController.cs
Normal file
72
NitroxClient/MonoBehaviours/AnimationController.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours;
|
||||
|
||||
// Shouldn't this class be named after the armscontroller?
|
||||
public class AnimationController : MonoBehaviour
|
||||
{
|
||||
private const float SMOOTHING_SPEED = 4f;
|
||||
private Animator animator;
|
||||
|
||||
public bool UpdatePlayerAnimations { get; set; } = true;
|
||||
public Quaternion AimingRotation { get; set; }
|
||||
public Vector3 Velocity { get; set; }
|
||||
public Quaternion BodyRotation { get; set; }
|
||||
|
||||
private Vector3 smoothedVelocity = Vector3.zero;
|
||||
private float smoothViewPitch;
|
||||
|
||||
public void Awake()
|
||||
{
|
||||
animator = GetComponent<Animator>();
|
||||
|
||||
this["is_underwater"] = true;
|
||||
}
|
||||
|
||||
public void FixedUpdate()
|
||||
{
|
||||
if (UpdatePlayerAnimations)
|
||||
{
|
||||
Vector3 rotationCorrectedVelocity = transform.rotation.GetInverse() * Velocity;
|
||||
|
||||
smoothedVelocity = UWE.Utils.SlerpVector(smoothedVelocity, rotationCorrectedVelocity, Vector3.Normalize(rotationCorrectedVelocity - smoothedVelocity) * (SMOOTHING_SPEED * Time.fixedDeltaTime));
|
||||
|
||||
animator.SetFloat("move_speed", smoothedVelocity.magnitude);
|
||||
animator.SetFloat("move_speed_x", smoothedVelocity.x);
|
||||
animator.SetFloat("move_speed_y", smoothedVelocity.y);
|
||||
animator.SetFloat("move_speed_z", smoothedVelocity.z);
|
||||
|
||||
float viewPitch = AimingRotation.eulerAngles.x;
|
||||
if (viewPitch > 180f)
|
||||
{
|
||||
viewPitch -= 360f;
|
||||
}
|
||||
|
||||
viewPitch = -viewPitch;
|
||||
smoothViewPitch = Mathf.Lerp(smoothViewPitch, viewPitch, 4f * Time.fixedDeltaTime);
|
||||
animator.SetFloat("view_pitch", smoothViewPitch);
|
||||
}
|
||||
}
|
||||
|
||||
public bool this[string name]
|
||||
{
|
||||
get => animator.GetBool(name);
|
||||
set => animator.SetBool(name, value);
|
||||
}
|
||||
|
||||
internal void SetFloat(string name, float value)
|
||||
{
|
||||
animator.SetFloat(name, value);
|
||||
}
|
||||
|
||||
internal void SetFloat(int id, float value)
|
||||
{
|
||||
animator.SetFloat(id, value);
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
animator.Rebind();
|
||||
animator.Update(0f);
|
||||
}
|
||||
}
|
42
NitroxClient/MonoBehaviours/AnimationSender.cs
Normal file
42
NitroxClient/MonoBehaviours/AnimationSender.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using NitroxClient.GameLogic;
|
||||
using NitroxModel.Core;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours
|
||||
{
|
||||
public class AnimationSender : MonoBehaviour
|
||||
{
|
||||
private LocalPlayer localPlayer;
|
||||
|
||||
AnimChangeState lastUnderwaterState = AnimChangeState.UNSET;
|
||||
|
||||
public void Awake()
|
||||
{
|
||||
localPlayer = NitroxServiceLocator.LocateService<LocalPlayer>();
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
AnimChangeState underwaterState = (AnimChangeState)(Player.main.IsUnderwater() ? 1 : 0);
|
||||
if (lastUnderwaterState != underwaterState)
|
||||
{
|
||||
localPlayer.AnimationChange(AnimChangeType.UNDERWATER, underwaterState);
|
||||
lastUnderwaterState = underwaterState;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum AnimChangeState
|
||||
{
|
||||
OFF,
|
||||
ON,
|
||||
UNSET
|
||||
}
|
||||
|
||||
public enum AnimChangeType
|
||||
{
|
||||
UNDERWATER,
|
||||
BENCH,
|
||||
INFECTION_REVEAL
|
||||
}
|
||||
}
|
121
NitroxClient/MonoBehaviours/BaseLeakManager.cs
Normal file
121
NitroxClient/MonoBehaviours/BaseLeakManager.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using System.Collections.Generic;
|
||||
using NitroxClient.Communication;
|
||||
using NitroxClient.GameLogic;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.Packets;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours;
|
||||
|
||||
public class BaseLeakManager : MonoBehaviour
|
||||
{
|
||||
private Dictionary<Int3, NitroxId> idByRelativeCell;
|
||||
private Base @base;
|
||||
private BaseHullStrength baseHullStrength;
|
||||
private NitroxId baseId;
|
||||
|
||||
public void Awake()
|
||||
{
|
||||
if (!TryGetComponent(out baseHullStrength))
|
||||
{
|
||||
Log.Error($"Tried adding a {nameof(BaseLeakManager)} to a GameObject that isn't a base, deleting it.");
|
||||
Destroy(this);
|
||||
return;
|
||||
}
|
||||
@base = baseHullStrength.baseComp;
|
||||
@base.TryGetNitroxId(out baseId);
|
||||
idByRelativeCell = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Either creates or updates existing leaks by modifying the base cell's health.
|
||||
/// Also registers the leak's id for further use.
|
||||
/// </summary>
|
||||
public void EnsureLeak(Int3 relativeCell, NitroxId cellId, float health)
|
||||
{
|
||||
Int3 absoluteCell = Absolute(relativeCell);
|
||||
Transform cellObject = @base.GetCellObject(absoluteCell);
|
||||
if (!cellObject)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
idByRelativeCell[relativeCell] = cellId;
|
||||
|
||||
if (cellObject.TryGetComponent(out LiveMixin cellLiveMixin))
|
||||
{
|
||||
// Health goes from 0 to 100
|
||||
float deltaHealth = health - cellLiveMixin.health;
|
||||
if (Mathf.Abs(deltaHealth) > 1)
|
||||
{
|
||||
// Useful part of BaseHullStrength.CrushDamageUpdate
|
||||
this.Resolve<LiveMixinManager>().SyncRemoteHealth(cellLiveMixin, health, cellObject.position, DamageType.Pressure);
|
||||
|
||||
// Only play noise if the leak lost health
|
||||
if (deltaHealth >= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Spawning multiple leaks would result in a big sounds when loading the game
|
||||
if (Multiplayer.Main && Multiplayer.Main.InitialSyncCompleted)
|
||||
{
|
||||
// Code from BaseHullStrength.CrushDamageUpdate
|
||||
int num = 0;
|
||||
if (baseHullStrength.totalStrength <= -3f)
|
||||
{
|
||||
num = 2;
|
||||
}
|
||||
else if (baseHullStrength.totalStrength <= -2f)
|
||||
{
|
||||
num = 1;
|
||||
}
|
||||
if (baseHullStrength.crushSounds[num] != null)
|
||||
{
|
||||
using (PacketSuppressor<FMODAssetPacket>.Suppress())
|
||||
{
|
||||
Utils.PlayFMODAsset(baseHullStrength.crushSounds[num], cellObject, 20f);
|
||||
}
|
||||
}
|
||||
ErrorMessage.AddMessage(Language.main.GetFormat("BaseHullStrDamageDetected", baseHullStrength.totalStrength));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void HealLeakToMax(Int3 relativeCell)
|
||||
{
|
||||
Transform cellObject = @base.GetCellObject(Absolute(relativeCell));
|
||||
if (cellObject && cellObject.TryGetComponent(out LiveMixin liveMixin))
|
||||
{
|
||||
this.Resolve<LiveMixinManager>().SyncRemoteHealth(liveMixin, liveMixin.maxHealth);
|
||||
idByRelativeCell.Remove(relativeCell);
|
||||
}
|
||||
}
|
||||
|
||||
public Int3 Absolute(Int3 relativeCell)
|
||||
{
|
||||
return relativeCell + @base.anchor;
|
||||
}
|
||||
|
||||
public Int3 Relative(Int3 absoluteCell)
|
||||
{
|
||||
return absoluteCell - @base.anchor;
|
||||
}
|
||||
|
||||
public LeakRepaired RemoveLeakByAbsoluteCell(Int3 absoluteCell)
|
||||
{
|
||||
Int3 relativeCell = Relative(absoluteCell);
|
||||
if (idByRelativeCell.TryGetValue(relativeCell, out NitroxId cellId))
|
||||
{
|
||||
idByRelativeCell.Remove(relativeCell);
|
||||
return new(baseId, cellId, relativeCell.ToDto());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public bool TryGetAbsoluteCellId(Int3 absoluteCell, out NitroxId cellId)
|
||||
{
|
||||
return idByRelativeCell.TryGetValue(Relative(absoluteCell), out cellId);
|
||||
}
|
||||
}
|
@@ -0,0 +1,156 @@
|
||||
using System.Collections.Generic;
|
||||
using NitroxClient.GameLogic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.CinematicController;
|
||||
|
||||
public class MultiplayerCinematicController : MonoBehaviour
|
||||
{
|
||||
private readonly Dictionary<ushort, RemotePlayerCinematicController> controllerByPlayerId = new();
|
||||
|
||||
/// <summary>
|
||||
/// MCCs with the same Animator to reset state if needed.
|
||||
/// </summary>
|
||||
private readonly List<MultiplayerCinematicController> multiplayerControllerSameAnimator = new();
|
||||
|
||||
private CinematicControllerPrefab controllerPrefab;
|
||||
private PlayerCinematicController playerController;
|
||||
|
||||
public void CallStartCinematicMode(RemotePlayer player)
|
||||
{
|
||||
if (!playerController.cinematicModeActive) // Check if local player is occupying the animator.
|
||||
{
|
||||
GetController(player).StartCinematicMode(player);
|
||||
}
|
||||
}
|
||||
|
||||
public void CallCinematicModeEnd(RemotePlayer player)
|
||||
{
|
||||
if (!playerController.cinematicModeActive) // Check if local player is occupying the animator.
|
||||
{
|
||||
GetController(player).OnPlayerCinematicModeEnd();
|
||||
}
|
||||
}
|
||||
|
||||
public void CallAllCinematicModeEnd()
|
||||
{
|
||||
foreach (RemotePlayerCinematicController remoteController in controllerByPlayerId.Values)
|
||||
{
|
||||
remoteController.EndCinematicMode(true);
|
||||
}
|
||||
|
||||
foreach (MultiplayerCinematicController controller in multiplayerControllerSameAnimator)
|
||||
{
|
||||
foreach (RemotePlayerCinematicController remoteController in controller.controllerByPlayerId.Values)
|
||||
{
|
||||
remoteController.EndCinematicMode(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private RemotePlayerCinematicController GetController(RemotePlayer player)
|
||||
{
|
||||
if (controllerByPlayerId.TryGetValue(player.PlayerId, out RemotePlayerCinematicController controller))
|
||||
{
|
||||
return controller;
|
||||
}
|
||||
|
||||
player.PlayerDisconnectEvent.AddHandler(gameObject, OnPlayerDisconnect);
|
||||
|
||||
controller = CreateNewControllerForPlayer();
|
||||
controllerByPlayerId.Add(player.PlayerId, controller);
|
||||
return controller;
|
||||
}
|
||||
|
||||
public void OnPlayerDisconnect(RemotePlayer player)
|
||||
{
|
||||
if (controllerByPlayerId.TryGetValue(player.PlayerId, out RemotePlayerCinematicController controller))
|
||||
{
|
||||
Destroy(controller);
|
||||
controllerByPlayerId.Remove(player.PlayerId);
|
||||
}
|
||||
}
|
||||
|
||||
private RemotePlayerCinematicController CreateNewControllerForPlayer()
|
||||
{
|
||||
RemotePlayerCinematicController controller = gameObject.AddComponent<RemotePlayerCinematicController>();
|
||||
controllerPrefab.PopulateRemoteController(controller);
|
||||
|
||||
return controller;
|
||||
}
|
||||
|
||||
public void AddOtherControllers(IEnumerable<MultiplayerCinematicController> otherControllers)
|
||||
{
|
||||
foreach (MultiplayerCinematicController controller in otherControllers)
|
||||
{
|
||||
if (controller.playerController.animator == playerController.animator)
|
||||
{
|
||||
multiplayerControllerSameAnimator.Add(controller);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static MultiplayerCinematicController Initialize(PlayerCinematicController playerController)
|
||||
{
|
||||
MultiplayerCinematicController mcp = playerController.gameObject.AddComponent<MultiplayerCinematicController>();
|
||||
mcp.controllerPrefab = new CinematicControllerPrefab(playerController);
|
||||
mcp.playerController = playerController;
|
||||
return mcp;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct CinematicControllerPrefab
|
||||
{
|
||||
private readonly Transform animatedTransform;
|
||||
private readonly Transform endTransform;
|
||||
private readonly bool onlyUseEndTransformInVr;
|
||||
private readonly bool playInVr;
|
||||
private readonly string playerViewAnimationName;
|
||||
private readonly string playerViewInterpolateAnimParam;
|
||||
private readonly string animParam;
|
||||
private readonly string interpolateAnimParam;
|
||||
private readonly float interpolationTime;
|
||||
private readonly float interpolationTimeOut;
|
||||
private readonly string receiversAnimParam;
|
||||
private readonly GameObject[] animParamReceivers;
|
||||
private readonly bool interpolateDuringAnimation;
|
||||
private readonly Animator animator;
|
||||
|
||||
// Currently we don't sync playerController.informGameObject but no problem could be found while testing.
|
||||
public CinematicControllerPrefab(PlayerCinematicController playerController)
|
||||
{
|
||||
animatedTransform = playerController.animatedTransform;
|
||||
endTransform = playerController.endTransform;
|
||||
onlyUseEndTransformInVr = playerController.onlyUseEndTransformInVr;
|
||||
playInVr = playerController.playInVr;
|
||||
playerViewAnimationName = playerController.playerViewAnimationName;
|
||||
playerViewInterpolateAnimParam = playerController.playerViewInterpolateAnimParam;
|
||||
animParam = playerController.animParam;
|
||||
interpolateAnimParam = playerController.interpolateAnimParam;
|
||||
interpolationTime = playerController.interpolationTime;
|
||||
interpolationTimeOut = playerController.interpolationTimeOut;
|
||||
receiversAnimParam = playerController.receiversAnimParam;
|
||||
animParamReceivers = playerController.animParamReceivers;
|
||||
interpolateDuringAnimation = playerController.interpolateDuringAnimation;
|
||||
animator = playerController.animator;
|
||||
}
|
||||
|
||||
public void PopulateRemoteController(RemotePlayerCinematicController remoteController)
|
||||
{
|
||||
remoteController.animatedTransform = animatedTransform;
|
||||
remoteController.informGameObject = null;
|
||||
remoteController.endTransform = endTransform;
|
||||
remoteController.onlyUseEndTransformInVr = onlyUseEndTransformInVr;
|
||||
remoteController.playInVr = playInVr;
|
||||
remoteController.playerViewAnimationName = playerViewAnimationName;
|
||||
remoteController.playerViewInterpolateAnimParam = playerViewInterpolateAnimParam;
|
||||
remoteController.animParam = animParam;
|
||||
remoteController.interpolateAnimParam = interpolateAnimParam;
|
||||
remoteController.interpolationTime = interpolationTime;
|
||||
remoteController.interpolationTimeOut = interpolationTimeOut;
|
||||
remoteController.receiversAnimParam = receiversAnimParam;
|
||||
remoteController.animParamReceivers = animParamReceivers;
|
||||
remoteController.interpolateDuringAnimation = interpolateDuringAnimation;
|
||||
remoteController.animator = animator;
|
||||
}
|
||||
}
|
@@ -0,0 +1,81 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NitroxClient.GameLogic;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using NitroxModel.DataStructures.GameLogic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.CinematicController;
|
||||
|
||||
public class MultiplayerCinematicReference : MonoBehaviour
|
||||
{
|
||||
private readonly Dictionary<string, Dictionary<int, MultiplayerCinematicController>> controllerByKey = new();
|
||||
|
||||
private bool isEscapePod;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// TODO: Currently only single EscapePod is supported, therefor EscapePod.main. Can probably be removed after we use one pod per intro sequence
|
||||
isEscapePod = gameObject == EscapePod.main.gameObject;
|
||||
}
|
||||
|
||||
public void CallStartCinematicMode(string key, int identifier, RemotePlayer player)
|
||||
{
|
||||
if(isEscapePod && this.Resolve<LocalPlayer>().IntroCinematicMode is IntroCinematicMode.PLAYING or IntroCinematicMode.SINGLEPLAYER) return;
|
||||
|
||||
if (!controllerByKey.TryGetValue(key, out Dictionary<int, MultiplayerCinematicController> controllers))
|
||||
{
|
||||
throw new KeyNotFoundException($"There was no entry for the key {key} at {gameObject.GetFullHierarchyPath()}");
|
||||
}
|
||||
|
||||
if (!controllers.TryGetValue(identifier, out MultiplayerCinematicController controller))
|
||||
{
|
||||
throw new KeyNotFoundException($"There was no entry for the identifier {identifier} at {gameObject.GetFullHierarchyPath()}");
|
||||
}
|
||||
|
||||
controller.CallStartCinematicMode(player);
|
||||
}
|
||||
|
||||
public void CallCinematicModeEnd(string key, int identifier, RemotePlayer player)
|
||||
{
|
||||
if(isEscapePod && this.Resolve<LocalPlayer>().IntroCinematicMode is IntroCinematicMode.PLAYING or IntroCinematicMode.SINGLEPLAYER) return;
|
||||
|
||||
if (!controllerByKey.TryGetValue(key, out Dictionary<int, MultiplayerCinematicController> controllers))
|
||||
{
|
||||
throw new KeyNotFoundException($"There was no entry for the key {key} at {gameObject.GetFullHierarchyPath()}");
|
||||
}
|
||||
|
||||
if (!controllers.TryGetValue(identifier, out MultiplayerCinematicController controller))
|
||||
{
|
||||
throw new KeyNotFoundException($"There was no entry for the identifier {identifier} at {gameObject.GetFullHierarchyPath()}");
|
||||
}
|
||||
|
||||
controller.CallCinematicModeEnd(player);
|
||||
}
|
||||
|
||||
public static int GetCinematicControllerIdentifier(GameObject controller, GameObject reference) => controller.gameObject.GetHierarchyPath(reference).GetHashCode();
|
||||
|
||||
public void AddController(PlayerCinematicController playerController)
|
||||
{
|
||||
MultiplayerCinematicController[] allControllers = controllerByKey.SelectMany(n => n.Value.Select(x => x.Value)).ToArray();
|
||||
|
||||
if (!controllerByKey.TryGetValue(playerController.playerViewAnimationName, out Dictionary<int, MultiplayerCinematicController> controllers))
|
||||
{
|
||||
controllers = new Dictionary<int, MultiplayerCinematicController>();
|
||||
controllerByKey.Add(playerController.playerViewAnimationName, controllers);
|
||||
}
|
||||
|
||||
int identifier = GetCinematicControllerIdentifier(playerController.gameObject, gameObject);
|
||||
|
||||
if (controllers.ContainsKey(identifier))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MultiplayerCinematicController controller = MultiplayerCinematicController.Initialize(playerController);
|
||||
controller.AddOtherControllers(allControllers);
|
||||
allControllers.ForEach(x => x.AddOtherControllers(new[] { controller }));
|
||||
|
||||
controllers.Add(identifier, controller);
|
||||
}
|
||||
}
|
@@ -0,0 +1,471 @@
|
||||
using System;
|
||||
using NitroxClient.GameLogic;
|
||||
using UnityEngine;
|
||||
using static PlayerCinematicController;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.CinematicController;
|
||||
|
||||
/// <summary>
|
||||
/// Override for <see cref="PlayerCinematicController"/>
|
||||
/// </summary>
|
||||
public class RemotePlayerCinematicController : MonoBehaviour, IManagedUpdateBehaviour, IManagedLateUpdateBehaviour
|
||||
{
|
||||
[AssertNotNull] public Transform animatedTransform;
|
||||
|
||||
public GameObject informGameObject;
|
||||
|
||||
public Transform endTransform;
|
||||
|
||||
public bool onlyUseEndTransformInVr;
|
||||
|
||||
public bool playInVr;
|
||||
|
||||
public string playerViewAnimationName = "";
|
||||
|
||||
public string playerViewInterpolateAnimParam = "";
|
||||
|
||||
public string animParam = "cinematicMode";
|
||||
|
||||
public string interpolateAnimParam = "";
|
||||
|
||||
public float interpolationTime = 0.25f;
|
||||
|
||||
public float interpolationTimeOut = 0.25f;
|
||||
|
||||
public string receiversAnimParam = "";
|
||||
|
||||
public GameObject[] animParamReceivers;
|
||||
|
||||
public bool interpolateDuringAnimation;
|
||||
|
||||
public bool debug;
|
||||
|
||||
public Animator animator;
|
||||
|
||||
[NonSerialized] public bool cinematicModeActive;
|
||||
|
||||
private Vector3 playerFromPosition = Vector3.zero;
|
||||
|
||||
private Quaternion playerFromRotation = Quaternion.identity;
|
||||
|
||||
private bool onCinematicModeEndCall;
|
||||
|
||||
private float timeStateChanged;
|
||||
|
||||
private State _state;
|
||||
|
||||
private RemotePlayer player;
|
||||
|
||||
private bool subscribed;
|
||||
|
||||
private bool _animState;
|
||||
|
||||
public static int cinematicModeCount { get; private set; }
|
||||
|
||||
public static float cinematicActivityStart { get; private set; }
|
||||
|
||||
private State state
|
||||
{
|
||||
get => _state;
|
||||
set
|
||||
{
|
||||
timeStateChanged = Time.time;
|
||||
_state = value;
|
||||
}
|
||||
}
|
||||
|
||||
public bool animState
|
||||
{
|
||||
get => _animState;
|
||||
private set
|
||||
{
|
||||
if (value == _animState)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (debug)
|
||||
{
|
||||
Debug.Log($"setting cinematic controller {gameObject.name} to: {value}");
|
||||
}
|
||||
|
||||
_animState = value;
|
||||
if (animParam.Length > 0)
|
||||
{
|
||||
SafeAnimator.SetBool(animator, animParam, value);
|
||||
}
|
||||
|
||||
if (receiversAnimParam.Length > 0)
|
||||
{
|
||||
for (int i = 0; i < animParamReceivers.Length; i++)
|
||||
{
|
||||
animParamReceivers[i].GetComponent<IAnimParamReceiver>()?.ForwardAnimationParameterBool(receiversAnimParam, value);
|
||||
}
|
||||
}
|
||||
|
||||
if (playerViewAnimationName.Length > 0 && player != null)
|
||||
{
|
||||
Animator componentInChildren = player.Body.GetComponentInChildren<Animator>();
|
||||
if (componentInChildren != null && componentInChildren.gameObject.activeInHierarchy)
|
||||
{
|
||||
SafeAnimator.SetBool(componentInChildren, playerViewAnimationName, value);
|
||||
}
|
||||
}
|
||||
|
||||
SetVrActiveParam();
|
||||
}
|
||||
}
|
||||
|
||||
public int managedUpdateIndex { get; set; }
|
||||
|
||||
public int managedLateUpdateIndex { get; set; }
|
||||
|
||||
public string GetProfileTag()
|
||||
{
|
||||
return nameof(RemotePlayerCinematicController);
|
||||
}
|
||||
|
||||
public void SetPlayer(RemotePlayer setplayer)
|
||||
{
|
||||
if (subscribed && player != setplayer)
|
||||
{
|
||||
Subscribe(player, false);
|
||||
Subscribe(setplayer, true);
|
||||
}
|
||||
|
||||
player = setplayer;
|
||||
}
|
||||
|
||||
public RemotePlayer GetPlayer()
|
||||
{
|
||||
return player;
|
||||
}
|
||||
|
||||
private void AddToUpdateManager()
|
||||
{
|
||||
BehaviourUpdateUtils.Register((IManagedUpdateBehaviour)this);
|
||||
BehaviourUpdateUtils.Register((IManagedLateUpdateBehaviour)this);
|
||||
}
|
||||
|
||||
private void RemoveFromUpdateManager()
|
||||
{
|
||||
BehaviourUpdateUtils.Deregister((IManagedUpdateBehaviour)this);
|
||||
BehaviourUpdateUtils.Deregister((IManagedLateUpdateBehaviour)this);
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
AddToUpdateManager();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
RemoveFromUpdateManager();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
SetVrActiveParam();
|
||||
}
|
||||
|
||||
private void SetVrActiveParam()
|
||||
{
|
||||
string paramaterName = "vr_active";
|
||||
bool vrAnimationMode = GameOptions.GetVrAnimationMode();
|
||||
if (animator != null)
|
||||
{
|
||||
animator.SetBool(paramaterName, vrAnimationMode);
|
||||
}
|
||||
|
||||
foreach (GameObject animatedObject in animParamReceivers)
|
||||
{
|
||||
animatedObject.GetComponent<IAnimParamReceiver>()?.ForwardAnimationParameterBool(paramaterName, vrAnimationMode);
|
||||
}
|
||||
}
|
||||
|
||||
private bool UseEndTransform()
|
||||
{
|
||||
if (endTransform == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (onlyUseEndTransformInVr)
|
||||
{
|
||||
return GameOptions.GetVrAnimationMode();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void SkipCinematic(RemotePlayer player)
|
||||
{
|
||||
this.player = player;
|
||||
if (player != null)
|
||||
{
|
||||
Transform component = player.Body.GetComponent<Transform>();
|
||||
if (UseEndTransform())
|
||||
{
|
||||
component.position = endTransform.position;
|
||||
component.rotation = endTransform.rotation;
|
||||
}
|
||||
}
|
||||
|
||||
if (informGameObject != null)
|
||||
{
|
||||
informGameObject.SendMessage(nameof(CinematicModeTriggerBase.OnPlayerCinematicModeEnd), this, SendMessageOptions.DontRequireReceiver);
|
||||
}
|
||||
}
|
||||
|
||||
public void StartCinematicMode(RemotePlayer setplayer)
|
||||
{
|
||||
if (debug)
|
||||
{
|
||||
Debug.Log($"{gameObject.name}.StartCinematicMode");
|
||||
}
|
||||
|
||||
if (!cinematicModeActive)
|
||||
{
|
||||
player = null;
|
||||
if (!playInVr && GameOptions.GetVrAnimationMode())
|
||||
{
|
||||
if (debug)
|
||||
{
|
||||
Debug.Log($"{gameObject.name} skip cinematic");
|
||||
}
|
||||
|
||||
SkipCinematic(setplayer);
|
||||
return;
|
||||
}
|
||||
|
||||
animator.cullingMode = AnimatorCullingMode.AlwaysAnimate;
|
||||
cinematicModeActive = true;
|
||||
if (setplayer != null)
|
||||
{
|
||||
SetPlayer(setplayer);
|
||||
Subscribe(player, true);
|
||||
}
|
||||
|
||||
state = State.In;
|
||||
if (informGameObject != null)
|
||||
{
|
||||
informGameObject.SendMessage(nameof(DockedVehicleHandTarget.OnPlayerCinematicModeStart), this, SendMessageOptions.DontRequireReceiver);
|
||||
}
|
||||
|
||||
if (player != null)
|
||||
{
|
||||
Transform component = player.Body.GetComponent<Transform>();
|
||||
playerFromPosition = component.position;
|
||||
playerFromRotation = component.rotation;
|
||||
if (playerViewInterpolateAnimParam.Length > 0)
|
||||
{
|
||||
SafeAnimator.SetBool(player.Body.GetComponentInChildren<Animator>(), playerViewInterpolateAnimParam, true);
|
||||
}
|
||||
}
|
||||
|
||||
if (interpolateAnimParam.Length > 0)
|
||||
{
|
||||
SafeAnimator.SetBool(animator, interpolateAnimParam, true);
|
||||
}
|
||||
|
||||
if (interpolateDuringAnimation)
|
||||
{
|
||||
animState = true;
|
||||
}
|
||||
|
||||
if (debug)
|
||||
{
|
||||
Debug.Log($"{gameObject.name} successfully started cinematic");
|
||||
}
|
||||
|
||||
if (cinematicModeCount == 0)
|
||||
{
|
||||
cinematicActivityStart = Time.time;
|
||||
}
|
||||
|
||||
cinematicModeCount++;
|
||||
}
|
||||
else if (debug)
|
||||
{
|
||||
Debug.Log($"{gameObject.name} cinematic already active!");
|
||||
}
|
||||
}
|
||||
|
||||
public void EndCinematicMode(bool reset = false)
|
||||
{
|
||||
if (cinematicModeActive)
|
||||
{
|
||||
if (reset) // Added by us
|
||||
{
|
||||
animator.Rebind();
|
||||
animator.Update(0f);
|
||||
}
|
||||
|
||||
animator.cullingMode = AnimatorCullingMode.CullCompletely;
|
||||
animState = false;
|
||||
state = State.None;
|
||||
cinematicModeActive = false;
|
||||
cinematicModeCount--;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnPlayerCinematicModeEnd()
|
||||
{
|
||||
if (!cinematicModeActive || onCinematicModeEndCall)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (player != null)
|
||||
{
|
||||
UpdatePlayerPosition();
|
||||
}
|
||||
|
||||
animState = false;
|
||||
if (UseEndTransform())
|
||||
{
|
||||
state = State.Out;
|
||||
if (player != null)
|
||||
{
|
||||
Transform component = player.Body.GetComponent<Transform>();
|
||||
playerFromPosition = component.position;
|
||||
playerFromRotation = component.rotation;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
EndCinematicMode();
|
||||
}
|
||||
|
||||
if (informGameObject != null)
|
||||
{
|
||||
onCinematicModeEndCall = true;
|
||||
informGameObject.SendMessage(nameof(DockedVehicleHandTarget.OnPlayerCinematicModeEnd), this, SendMessageOptions.DontRequireReceiver);
|
||||
onCinematicModeEndCall = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdatePlayerPosition()
|
||||
{
|
||||
Transform component = player.Body.GetComponent<Transform>();
|
||||
component.position = animatedTransform.position;
|
||||
component.rotation = animatedTransform.rotation;
|
||||
}
|
||||
|
||||
public void ManagedLateUpdate()
|
||||
{
|
||||
if (!cinematicModeActive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float num = Time.time - timeStateChanged;
|
||||
float timedOutScalar;
|
||||
Transform transform = null;
|
||||
if (player != null && player.Body)
|
||||
{
|
||||
transform = player.Body.GetComponent<Transform>();
|
||||
}
|
||||
|
||||
bool isVrAnimationMode = !GameOptions.GetVrAnimationMode();
|
||||
switch (state)
|
||||
{
|
||||
case State.In:
|
||||
timedOutScalar = interpolationTime != 0f && isVrAnimationMode ? Mathf.Clamp01(num / interpolationTime) : 1f;
|
||||
if (player != null && transform)
|
||||
{
|
||||
transform.position = Vector3.Lerp(playerFromPosition, animatedTransform.position, timedOutScalar);
|
||||
transform.rotation = Quaternion.Slerp(playerFromRotation, animatedTransform.rotation, timedOutScalar);
|
||||
}
|
||||
|
||||
if (timedOutScalar == 1f)
|
||||
{
|
||||
state = State.Update;
|
||||
animState = true;
|
||||
if (interpolateAnimParam.Length > 0)
|
||||
{
|
||||
SafeAnimator.SetBool(animator, interpolateAnimParam, false);
|
||||
}
|
||||
|
||||
if (playerViewInterpolateAnimParam.Length > 0 && player != null)
|
||||
{
|
||||
SafeAnimator.SetBool(player.Body.GetComponentInChildren<Animator>(), playerViewInterpolateAnimParam, false);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case State.Update:
|
||||
if (player != null)
|
||||
{
|
||||
UpdatePlayerPosition();
|
||||
}
|
||||
|
||||
break;
|
||||
case State.Out:
|
||||
timedOutScalar = interpolationTimeOut != 0f && isVrAnimationMode ? Mathf.Clamp01(num / interpolationTimeOut) : 1f;
|
||||
if (player != null && transform)
|
||||
{
|
||||
transform.position = Vector3.Lerp(playerFromPosition, endTransform.position, timedOutScalar);
|
||||
transform.rotation = Quaternion.Slerp(playerFromRotation, endTransform.rotation, timedOutScalar);
|
||||
}
|
||||
|
||||
if (timedOutScalar == 1f)
|
||||
{
|
||||
EndCinematicMode();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsCinematicModeActive()
|
||||
{
|
||||
return cinematicModeActive;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
RemoveFromUpdateManager();
|
||||
if (subscribed)
|
||||
{
|
||||
Subscribe(player, false);
|
||||
}
|
||||
|
||||
EndCinematicMode();
|
||||
}
|
||||
|
||||
private void OnPlayerDeath(RemotePlayer player)
|
||||
{
|
||||
EndCinematicMode();
|
||||
animator.Rebind();
|
||||
}
|
||||
|
||||
private void Subscribe(RemotePlayer player, bool state)
|
||||
{
|
||||
if (player == null)
|
||||
{
|
||||
subscribed = false;
|
||||
}
|
||||
else if (subscribed != state)
|
||||
{
|
||||
if (state)
|
||||
{
|
||||
player.PlayerDeathEvent.AddHandler(gameObject, OnPlayerDeath);
|
||||
}
|
||||
else
|
||||
{
|
||||
player.PlayerDeathEvent.RemoveHandler(gameObject, OnPlayerDeath);
|
||||
}
|
||||
|
||||
subscribed = state;
|
||||
}
|
||||
}
|
||||
|
||||
public void ManagedUpdate()
|
||||
{
|
||||
if (!cinematicModeActive && subscribed)
|
||||
{
|
||||
Subscribe(player, false);
|
||||
}
|
||||
}
|
||||
}
|
378
NitroxClient/MonoBehaviours/Cyclops/CyclopsMotor.cs
Normal file
378
NitroxClient/MonoBehaviours/Cyclops/CyclopsMotor.cs
Normal 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);
|
||||
}
|
||||
}
|
116
NitroxClient/MonoBehaviours/Cyclops/CyclopsMotorGroundChecker.cs
Normal file
116
NitroxClient/MonoBehaviours/Cyclops/CyclopsMotorGroundChecker.cs
Normal 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;
|
||||
}
|
||||
}
|
177
NitroxClient/MonoBehaviours/Cyclops/NitroxCyclops.cs
Normal file
177
NitroxClient/MonoBehaviours/Cyclops/NitroxCyclops.cs
Normal 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);
|
||||
}
|
||||
}
|
59
NitroxClient/MonoBehaviours/Cyclops/TriggerWorkaround.cs
Normal file
59
NitroxClient/MonoBehaviours/Cyclops/TriggerWorkaround.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
290
NitroxClient/MonoBehaviours/Cyclops/VirtualCyclops.cs
Normal file
290
NitroxClient/MonoBehaviours/Cyclops/VirtualCyclops.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
194
NitroxClient/MonoBehaviours/Discord/DiscordClient.cs
Normal file
194
NitroxClient/MonoBehaviours/Discord/DiscordClient.cs
Normal file
@@ -0,0 +1,194 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using DiscordGameSDKWrapper;
|
||||
using NitroxClient.Communication.Abstract;
|
||||
using NitroxClient.MonoBehaviours.Gui.MainMenu.ServersList;
|
||||
using NitroxModel;
|
||||
using NitroxModel.Core;
|
||||
using NitroxModel.Packets;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.Discord;
|
||||
|
||||
public class DiscordClient : MonoBehaviour
|
||||
{
|
||||
private const long CLIENT_ID = 405122994348752896;
|
||||
private const int RETRY_INTERVAL = 60;
|
||||
|
||||
private static DiscordClient main;
|
||||
private static DiscordGameSDKWrapper.Discord discord;
|
||||
private static ActivityManager activityManager;
|
||||
private static Activity activity;
|
||||
private static bool showingWindow;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (main)
|
||||
{
|
||||
Log.Error($"[Discord] Tried to instantiate a second {nameof(DiscordClient)}");
|
||||
return;
|
||||
}
|
||||
activity = new();
|
||||
main = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
Log.Info("[Discord] Starting Discord client");
|
||||
StartDiscordHook();
|
||||
}
|
||||
|
||||
private void StartDiscordHook()
|
||||
{
|
||||
try
|
||||
{
|
||||
discord = new DiscordGameSDKWrapper.Discord(CLIENT_ID, (ulong)CreateFlags.NoRequireDiscord);
|
||||
discord.SetLogHook(DiscordGameSDKWrapper.LogLevel.Debug, (level, message) => Log.Write((NitroxModel.Logger.LogLevel)level, $"[Discord] {message}"));
|
||||
activityManager = discord.GetActivityManager();
|
||||
|
||||
activityManager.RegisterSteam((uint)GameInfo.Subnautica.SteamAppId);
|
||||
activityManager.OnActivityJoinRequest += ActivityJoinRequest;
|
||||
activityManager.OnActivityJoin += ActivityJoin;
|
||||
if (!string.IsNullOrEmpty(activity.State))
|
||||
{
|
||||
UpdateActivity();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DisposeAndScheduleHookRestart();
|
||||
Log.ErrorOnce($"Encountered an error while starting Discord hook, will retry every {RETRY_INTERVAL} seconds: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
Log.Info("[Discord] Shutdown client");
|
||||
discord?.Dispose();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (main == this)
|
||||
{
|
||||
main = null;
|
||||
activity = default;
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
try
|
||||
{
|
||||
discord?.RunCallbacks();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Happens when Discord is closed while Nitrox has its Discord hook running (and for other reason)
|
||||
DisposeAndScheduleHookRestart();
|
||||
Log.ErrorOnce($"An error occured while running callbacks for Discord, will retry every {RETRY_INTERVAL} seconds: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void DisposeAndScheduleHookRestart()
|
||||
{
|
||||
discord?.Dispose();
|
||||
discord = null;
|
||||
Invoke(nameof(StartDiscordHook), RETRY_INTERVAL);
|
||||
}
|
||||
|
||||
private void ActivityJoin(string secret)
|
||||
{
|
||||
Log.Info("[Discord] Joining Server");
|
||||
|
||||
if (SceneManager.GetActiveScene().name != "StartScreen" || !MainMenuServerListPanel.Main)
|
||||
{
|
||||
Log.InGame(Language.main.Get("Nitrox_DiscordMultiplayerMenu"));
|
||||
Log.Warn("[Discord] Can't join a server outside of the main-menu.");
|
||||
return;
|
||||
}
|
||||
|
||||
string[] splitSecret = secret.Split(':');
|
||||
string ip = string.Join(":", splitSecret.Take(splitSecret.Length - 1));
|
||||
string port = splitSecret.Last();
|
||||
|
||||
if(int.TryParse(port, out int portInt))
|
||||
{
|
||||
Log.Error($"[Discord] Port from received secret can't be parsed as int: {port}");
|
||||
return;
|
||||
}
|
||||
MainMenuServerButton.OpenJoinServerMenuAsync(ip, portInt).ContinueWithHandleError(true);
|
||||
}
|
||||
|
||||
private void ActivityJoinRequest(ref User user)
|
||||
{
|
||||
if (!showingWindow && Multiplayer.Active)
|
||||
{
|
||||
Log.Info($"[Discord] JoinRequest: Name:{user.Username}#{user.Discriminator} UserID:{user.Id}");
|
||||
StartCoroutine(DiscordJoinRequestGui.SpawnGui(user));
|
||||
showingWindow = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warn("[Discord] Request window is already active.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void InitializeRPMenu()
|
||||
{
|
||||
activity.State = Language.main.Get("Nitrox_DiscordMainMenuState");
|
||||
activity.Assets.LargeImage = "icon";
|
||||
UpdateActivity();
|
||||
}
|
||||
|
||||
public static void InitializeRPInGame(string username, int playerCount, int maxConnections)
|
||||
{
|
||||
activity.State = Language.main.Get("Nitrox_DiscordInGameState");
|
||||
activity.Details = Language.main.Get("Nitrox_DiscordInGame").Replace("{PLAYER}", username);
|
||||
activity.Timestamps.Start = 0;
|
||||
activity.Party.Size.CurrentSize = playerCount;
|
||||
activity.Party.Size.MaxSize = maxConnections;
|
||||
UpdateActivity();
|
||||
|
||||
NitroxServiceLocator.LocateService<IPacketSender>().Send(new DiscordRequestIP(string.Empty));
|
||||
}
|
||||
|
||||
public static void UpdateIpPort(string ipPort)
|
||||
{
|
||||
activity.Party.Id = $"NitroxPartyID:{ipPort}";
|
||||
activity.Secrets.Join = ipPort;
|
||||
UpdateActivity();
|
||||
}
|
||||
|
||||
public static void UpdatePartySize(int size)
|
||||
{
|
||||
activity.Party.Size.CurrentSize = size;
|
||||
UpdateActivity();
|
||||
}
|
||||
|
||||
private static void UpdateActivity()
|
||||
{
|
||||
activityManager?.UpdateActivity(activity, (result) =>
|
||||
{
|
||||
if (result != Result.Ok)
|
||||
{
|
||||
Log.Error($"[Discord] {result}: Updating Activity failed");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void RespondJoinRequest(long userID, ActivityJoinRequestReply reply)
|
||||
{
|
||||
showingWindow = false;
|
||||
activityManager?.SendRequestReply(userID, reply, (result) =>
|
||||
{
|
||||
if (result == Result.Ok)
|
||||
{
|
||||
Log.Info($"[Discord] Responded successfully {reply} to {userID}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.InGame($"[Discord] {Language.main.Get("Nitrox_Failure")}");
|
||||
Log.Error($"[Discord] {result}: Failed to send join response");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
104
NitroxClient/MonoBehaviours/Discord/DiscordJoinRequestGui.cs
Normal file
104
NitroxClient/MonoBehaviours/Discord/DiscordJoinRequestGui.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
using System.Collections;
|
||||
using DiscordGameSDKWrapper;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
using UnityEngine.UI;
|
||||
using static NitroxClient.Unity.Helper.AssetBundleLoader;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.Discord;
|
||||
|
||||
public class DiscordJoinRequestGui : uGUI_InputGroup
|
||||
{
|
||||
private readonly WaitForSeconds expireTimeYielder = new(45);
|
||||
|
||||
private static DiscordJoinRequestGui instance;
|
||||
private static User user;
|
||||
|
||||
private static Image profilePicture;
|
||||
private static GameObject pressToFocus;
|
||||
private static GameObject pressButtons;
|
||||
|
||||
public static IEnumerator SpawnGui(User requestingUser)
|
||||
{
|
||||
user = requestingUser;
|
||||
|
||||
yield return LoadUIAsset(NitroxAssetBundle.DISCORD_JOIN_REQUEST, false);
|
||||
|
||||
GameObject guiGameObject = (GameObject)NitroxAssetBundle.DISCORD_JOIN_REQUEST.LoadedAssets[0];
|
||||
|
||||
instance = guiGameObject.AddComponent<DiscordJoinRequestGui>();
|
||||
|
||||
profilePicture = guiGameObject.FindChild("ProfilePicture").GetComponent<Image>();
|
||||
|
||||
pressToFocus = guiGameObject.FindChild("PressToFocus");
|
||||
Text[] texts = pressToFocus.GetComponentsInChildren<Text>();
|
||||
texts[0].text = Language.main.Get("Nitrox_DiscordPressToFocus");
|
||||
texts[3].text = GameInput.GetBinding(GameInput.Device.Keyboard, (GameInput.Button)46, GameInput.BindingSet.Primary);
|
||||
pressToFocus.SetActive(true);
|
||||
|
||||
pressButtons = guiGameObject.FindChild("PressButtons");
|
||||
pressButtons.SetActive(false);
|
||||
|
||||
Text[] buttonTexts = pressButtons.GetComponentsInChildren<Text>(true);
|
||||
buttonTexts[0].text = Language.main.Get("Nitrox_DiscordAccept");
|
||||
buttonTexts[1].text = Language.main.Get("Nitrox_DiscordDecline");
|
||||
|
||||
Button[] buttons = pressButtons.GetComponentsInChildren<Button>(true);
|
||||
buttons[0].onClick.AddListener(instance.AcceptInvite);
|
||||
buttons[1].onClick.AddListener(instance.DeclineInvite);
|
||||
|
||||
Text[] userTexts = guiGameObject.FindChild("UpperText").GetComponentsInChildren<Text>();
|
||||
userTexts[0].text = $"{user.Username}#{user.Discriminator}";
|
||||
userTexts[1].text = Language.main.Get("Nitrox_DiscordRequestText");
|
||||
|
||||
yield return LoadAvatar(user.Id, user.Avatar);
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
StartCoroutine(OnRequestExpired());
|
||||
}
|
||||
|
||||
private void AcceptInvite() => CloseWindow(ActivityJoinRequestReply.Yes);
|
||||
private void DeclineInvite() => CloseWindow(ActivityJoinRequestReply.No);
|
||||
|
||||
private void CloseWindow(ActivityJoinRequestReply reply)
|
||||
{
|
||||
DiscordClient.RespondJoinRequest(user.Id, reply);
|
||||
DestroyImmediate(gameObject);
|
||||
}
|
||||
|
||||
public static void Select()
|
||||
{
|
||||
if (instance)
|
||||
{
|
||||
pressToFocus.SetActive(false);
|
||||
pressButtons.SetActive(true);
|
||||
instance.Select(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerator LoadAvatar(long id, string avatarID)
|
||||
{
|
||||
UnityWebRequest avatarUrl = UnityWebRequestTexture.GetTexture($"https://cdn.discordapp.com/avatars/{id}/{avatarID}.png");
|
||||
yield return avatarUrl.SendWebRequest();
|
||||
|
||||
Texture2D avatar = ((DownloadHandlerTexture)avatarUrl.downloadHandler).texture;
|
||||
|
||||
if (!avatar || avatar.height < 64)
|
||||
{
|
||||
UnityWebRequest standardAvatarUrl = UnityWebRequestTexture.GetTexture("https://discordapp.com/assets/6debd47ed13483642cf09e832ed0bc1b.png");
|
||||
yield return standardAvatarUrl.SendWebRequest();
|
||||
|
||||
avatar = ((DownloadHandlerTexture)standardAvatarUrl.downloadHandler).texture;
|
||||
}
|
||||
|
||||
profilePicture.sprite = Sprite.Create(avatar, new UnityEngine.Rect(0, 0, 128, 128), new Vector2(0.5f, 0.5f));
|
||||
}
|
||||
|
||||
private IEnumerator OnRequestExpired()
|
||||
{
|
||||
yield return expireTimeYielder;
|
||||
CloseWindow(ActivityJoinRequestReply.Ignore);
|
||||
}
|
||||
}
|
114
NitroxClient/MonoBehaviours/EntityPositionBroadcaster.cs
Normal file
114
NitroxClient/MonoBehaviours/EntityPositionBroadcaster.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NitroxClient.Communication.Abstract;
|
||||
using NitroxClient.GameLogic;
|
||||
using NitroxModel.Core;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.Packets;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
using UnityEngine;
|
||||
using static NitroxModel.Packets.EntityTransformUpdates;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours;
|
||||
|
||||
public class EntityPositionBroadcaster : MonoBehaviour
|
||||
{
|
||||
public static readonly float BROADCAST_INTERVAL = 0.25f;
|
||||
|
||||
private static HashSet<NitroxId> watchingEntityIds = new();
|
||||
|
||||
private static Dictionary<NitroxId, SplineTransformUpdate> splineUpdatesById = new();
|
||||
|
||||
private IPacketSender packetSender;
|
||||
|
||||
private float time;
|
||||
|
||||
public void Awake()
|
||||
{
|
||||
packetSender = NitroxServiceLocator.LocateService<IPacketSender>();
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
time += Time.deltaTime;
|
||||
|
||||
// Only do on a specific cadence to avoid hammering server
|
||||
if (time >= BROADCAST_INTERVAL)
|
||||
{
|
||||
time = 0;
|
||||
|
||||
if (watchingEntityIds.Count > 0)
|
||||
{
|
||||
Dictionary<NitroxId, GameObject> nonSplineEntitiesById = NitroxEntity.GetObjectsFrom(watchingEntityIds)
|
||||
.Where(item => !item.Value.GetComponent<SwimBehaviour>() &&
|
||||
!item.Value.GetComponent<WalkBehaviour>())
|
||||
.ToDictionary(item => item.Key, item => item.Value);
|
||||
|
||||
List<EntityTransformUpdate> updates = BuildUpdates(nonSplineEntitiesById);
|
||||
|
||||
if (updates.Count > 0)
|
||||
{
|
||||
packetSender.Send(new EntityTransformUpdates(updates));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<EntityTransformUpdate> BuildUpdates(Dictionary<NitroxId, GameObject> nonSplineEntitiesById)
|
||||
{
|
||||
List<EntityTransformUpdate> updates = new();
|
||||
|
||||
foreach (KeyValuePair<NitroxId, GameObject> gameObjectWithId in nonSplineEntitiesById)
|
||||
{
|
||||
if (gameObjectWithId.Value)
|
||||
{
|
||||
updates.Add(new RawTransformUpdate(gameObjectWithId.Key, gameObjectWithId.Value.transform.position.ToDto(), gameObjectWithId.Value.transform.rotation.ToDto()));
|
||||
}
|
||||
}
|
||||
|
||||
// Only send data for entities still simulated by the local player
|
||||
updates.AddRange(splineUpdatesById.Values.Where(
|
||||
splineUpdate => this.Resolve<SimulationOwnership>().HasAnyLockType(splineUpdate.Id)
|
||||
));
|
||||
|
||||
splineUpdatesById.Clear();
|
||||
|
||||
return updates;
|
||||
}
|
||||
|
||||
public static void WatchEntity(NitroxId id)
|
||||
{
|
||||
watchingEntityIds.Add(id);
|
||||
|
||||
// The game object may not exist at this very moment (due to being spawned in async). This is OK as we will
|
||||
// automatically start sending updates when we finally get it in the world. This behavior will also allow us
|
||||
// to resync or respawn entities while still have broadcasting enabled without doing anything extra.
|
||||
|
||||
if (NitroxEntity.TryGetComponentFrom(id, out RemotelyControlled remotelyControlled))
|
||||
{
|
||||
Object.Destroy(remotelyControlled);
|
||||
}
|
||||
}
|
||||
|
||||
public static void StopWatchingEntity(NitroxId id)
|
||||
{
|
||||
watchingEntityIds.Remove(id);
|
||||
}
|
||||
|
||||
public static void RegisterSplineMovementChange(NitroxId id, GameObject gameObject, Vector3 targetPos, Vector3 targetDir, float velocity)
|
||||
{
|
||||
if (watchingEntityIds.Contains(id))
|
||||
{
|
||||
splineUpdatesById[id] = new(id, gameObject.transform.position.ToDto(), gameObject.transform.rotation.ToDto(), targetPos.ToDto(), targetDir.ToDto(), velocity);
|
||||
}
|
||||
}
|
||||
|
||||
public static void RemoveEntityMovementControl(GameObject gameObject, NitroxId entityId)
|
||||
{
|
||||
if (gameObject.TryGetComponent(out RemotelyControlled remotelyControlled))
|
||||
{
|
||||
Destroy(remotelyControlled);
|
||||
}
|
||||
StopWatchingEntity(entityId);
|
||||
}
|
||||
}
|
174
NitroxClient/MonoBehaviours/FMODEmitterController.cs
Normal file
174
NitroxClient/MonoBehaviours/FMODEmitterController.cs
Normal file
@@ -0,0 +1,174 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FMOD.Studio;
|
||||
using FMODUnity;
|
||||
using NitroxClient.GameLogic.FMOD;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using NitroxModel.GameLogic.FMOD;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours;
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
public class FMODEmitterController : MonoBehaviour
|
||||
{
|
||||
private readonly Dictionary<string, FMOD_CustomEmitter> customEmitters = new();
|
||||
private readonly Dictionary<string, Tuple<FMOD_CustomLoopingEmitter, bool, float>> loopingEmitters = new(); // Tuple<emitter, is3D, radius>
|
||||
private readonly Dictionary<string, FMOD_StudioEventEmitter> studioEmitters = new();
|
||||
private readonly Dictionary<string, EventInstance> eventInstances = new(); // 2D Sounds
|
||||
|
||||
/// <summary>
|
||||
/// When <see cref="GameObject"/>s are copied their Start()/Awake() functions don't get called again.
|
||||
/// So the FMOD Start patch that tried to locate a <see cref="NitroxEntity"/> won't find one and will error later.
|
||||
/// This function can late register missed FMOD MonoBehaviours
|
||||
/// </summary>
|
||||
public void LateRegisterEmitter()
|
||||
{
|
||||
foreach (MonoBehaviour behaviour in gameObject.GetComponentsInChildren<MonoBehaviour>(true))
|
||||
{
|
||||
switch (behaviour)
|
||||
{
|
||||
case FMOD_CustomEmitter customEmitter when this.Resolve<FMODWhitelist>().IsWhitelisted(customEmitter.asset.path, out float maxDistance):
|
||||
AddEmitter(customEmitter.asset.path, customEmitter, maxDistance);
|
||||
break;
|
||||
case FMOD_StudioEventEmitter studioEmitter when this.Resolve<FMODWhitelist>().IsWhitelisted(studioEmitter.asset.path, out float maxDistance):
|
||||
AddEmitter(studioEmitter.asset.path, studioEmitter, maxDistance);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void AddEmitter(string path, FMOD_CustomEmitter customEmitter, float maxDistance)
|
||||
{
|
||||
if (customEmitters.ContainsKey(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
customEmitter.CacheEventInstance();
|
||||
EventInstance evt = customEmitter.GetEventInstance();
|
||||
evt.getDescription(out EventDescription description);
|
||||
description.is3D(out bool is3D);
|
||||
|
||||
if (is3D)
|
||||
{
|
||||
evt.setProperty(EVENT_PROPERTY.MINIMUM_DISTANCE, 1f);
|
||||
evt.setProperty(EVENT_PROPERTY.MAXIMUM_DISTANCE, maxDistance);
|
||||
|
||||
customEmitters.Add(path, customEmitter);
|
||||
|
||||
if (customEmitter is FMOD_CustomLoopingEmitter loopingEmitter)
|
||||
{
|
||||
if (loopingEmitter.assetStart && this.Resolve<FMODWhitelist>().IsWhitelisted(loopingEmitter.assetStart.path, out float radiusStart))
|
||||
{
|
||||
AddEmitter(loopingEmitter.assetStart.path, loopingEmitter, radiusStart);
|
||||
}
|
||||
|
||||
if (loopingEmitter.assetStop && this.Resolve<FMODWhitelist>().IsWhitelisted(loopingEmitter.assetStop.path, out float radiusStop))
|
||||
{
|
||||
AddEmitter(loopingEmitter.assetStop.path, loopingEmitter, radiusStop);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AddEventInstance(customEmitter.asset.path, evt);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddEmitter(string path, FMOD_CustomLoopingEmitter loopingEmitter, float radius)
|
||||
{
|
||||
if (!loopingEmitters.ContainsKey(path))
|
||||
{
|
||||
loopingEmitter.CacheEventInstance();
|
||||
|
||||
loopingEmitter.evt.getDescription(out EventDescription description);
|
||||
description.is3D(out bool is3D);
|
||||
|
||||
loopingEmitters.Add(path, new Tuple<FMOD_CustomLoopingEmitter, bool, float>(loopingEmitter, is3D, radius));
|
||||
}
|
||||
}
|
||||
|
||||
public void AddEmitter(string path, FMOD_StudioEventEmitter studioEmitter, float maxDistance)
|
||||
{
|
||||
if (!customEmitters.ContainsKey(path))
|
||||
{
|
||||
studioEmitter.CacheEventInstance();
|
||||
|
||||
studioEmitters.Add(path, studioEmitter);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddEventInstance(string path, EventInstance eventInstance)
|
||||
{
|
||||
if (!eventInstances.ContainsKey(path))
|
||||
{
|
||||
eventInstances.Add(path, eventInstance);
|
||||
}
|
||||
}
|
||||
|
||||
public void PlayCustomEmitter(string path) => customEmitters[path].AliveOrNull()?.Play();
|
||||
public void SetParameterCustomEmitter(string path, string paramString, float value) => customEmitters[path].AliveOrNull()?.SetParameterValue(paramString, value);
|
||||
public void StopCustomEmitter(string path) => customEmitters[path].AliveOrNull()?.Stop();
|
||||
|
||||
public void PlayStudioEmitter(string path) => studioEmitters[path].AliveOrNull()?.PlayUI();
|
||||
public void StopStudioEmitter(string path, bool allowFadeout) => studioEmitters[path].AliveOrNull()?.Stop(allowFadeout);
|
||||
|
||||
public void PlayCustomLoopingEmitter(string path)
|
||||
{
|
||||
(FMOD_CustomLoopingEmitter loopingEmitter, bool is3D, float radius) = loopingEmitters[path];
|
||||
EventInstance eventInstance = FMODUWE.GetEventImpl(path);
|
||||
|
||||
if (is3D)
|
||||
{
|
||||
eventInstance.set3DAttributes(loopingEmitter.transform.To3DAttributes());
|
||||
eventInstance.setProperty(EVENT_PROPERTY.MINIMUM_DISTANCE, 1f);
|
||||
eventInstance.setProperty(EVENT_PROPERTY.MAXIMUM_DISTANCE, radius);
|
||||
}
|
||||
else
|
||||
{
|
||||
eventInstance.setVolume(FMODSystem.CalculateVolume(loopingEmitter.transform.position, Player.main.transform.position, radius, 1f));
|
||||
}
|
||||
|
||||
eventInstance.start();
|
||||
eventInstance.release();
|
||||
loopingEmitter.timeLastStopSound = Time.time;
|
||||
}
|
||||
|
||||
public void PlayEventInstance(string path, float volume)
|
||||
{
|
||||
EventInstance eventInstance = eventInstances[path];
|
||||
eventInstance.setVolume(volume);
|
||||
eventInstance.start();
|
||||
}
|
||||
|
||||
public void StopEventInstance(string path)
|
||||
{
|
||||
EventInstance eventInstance = eventInstances[path];
|
||||
eventInstance.stop(FMOD.Studio.STOP_MODE.IMMEDIATE);
|
||||
}
|
||||
|
||||
public static void PlayEventOneShot(FMODAsset asset, float radius, Vector3 origin, float volume = 1f) => PlayEventOneShot(asset.path, radius, origin, volume);
|
||||
|
||||
public static void PlayEventOneShot(string path, float radius, Vector3 origin, float volume = 1f)
|
||||
{
|
||||
EventInstance evt = FMODUWE.GetEventImpl(path);
|
||||
evt.getDescription(out EventDescription description);
|
||||
description.is3D(out bool is3D);
|
||||
|
||||
if (is3D)
|
||||
{
|
||||
evt.setProperty(EVENT_PROPERTY.MINIMUM_DISTANCE, 1f);
|
||||
evt.setProperty(EVENT_PROPERTY.MAXIMUM_DISTANCE, radius);
|
||||
evt.setVolume(volume);
|
||||
}
|
||||
else
|
||||
{
|
||||
evt.setVolume(FMODSystem.CalculateVolume(origin, Player.main.transform.position, radius, volume));
|
||||
}
|
||||
|
||||
evt.set3DAttributes(origin.To3DAttributes());
|
||||
evt.start();
|
||||
evt.release();
|
||||
}
|
||||
}
|
147
NitroxClient/MonoBehaviours/Gui/Chat/PlayerChat.cs
Normal file
147
NitroxClient/MonoBehaviours/Gui/Chat/PlayerChat.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NitroxClient.GameLogic.ChatUI;
|
||||
using NitroxClient.GameLogic.Settings;
|
||||
using NitroxModel.Core;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.Gui.Chat
|
||||
{
|
||||
public class PlayerChat : uGUI_InputGroup
|
||||
{
|
||||
private const int LINE_CHAR_LIMIT = 255;
|
||||
private const int MESSAGES_LIMIT = 64;
|
||||
private const float TOGGLED_TRANSPARENCY = 0.4f;
|
||||
public const float CHAT_VISIBILITY_TIME_LENGTH = 6f;
|
||||
|
||||
private static readonly Queue<ChatLogEntry> entries = new Queue<ChatLogEntry>();
|
||||
private Image[] backgroundImages;
|
||||
private CanvasGroup canvasGroup;
|
||||
private InputField inputField;
|
||||
private GameObject logEntryPrefab;
|
||||
|
||||
private PlayerChatManager playerChatManager;
|
||||
private bool transparent;
|
||||
|
||||
public static bool IsReady { get; private set; }
|
||||
|
||||
public string InputText
|
||||
{
|
||||
get => inputField.text;
|
||||
set => inputField.text = value;
|
||||
}
|
||||
|
||||
public IEnumerator SetupChatComponents()
|
||||
{
|
||||
playerChatManager = NitroxServiceLocator.LocateService<PlayerChatManager>();
|
||||
|
||||
canvasGroup = GetComponent<CanvasGroup>();
|
||||
|
||||
logEntryPrefab = GameObject.Find("ChatLogEntryPrefab");
|
||||
logEntryPrefab.AddComponent<PlayerChatLogItem>();
|
||||
logEntryPrefab.SetActive(false);
|
||||
|
||||
GetComponentsInChildren<Button>()[0].onClick.AddListener(ToggleBackgroundTransparency);
|
||||
GetComponentsInChildren<Button>()[1].gameObject.AddComponent<PlayerChatPinButton>();
|
||||
|
||||
inputField = GetComponentInChildren<InputField>();
|
||||
inputField.gameObject.AddComponent<PlayerChatInputField>().InputField = inputField;
|
||||
inputField.GetComponentInChildren<Button>().onClick.AddListener(playerChatManager.SendMessage);
|
||||
|
||||
// We pick any image that's inside the chat component to have all of their opacity lowered
|
||||
backgroundImages = transform.GetComponentsInChildren<Image>();
|
||||
|
||||
yield return new WaitForEndOfFrame(); //Needed so Select() works on initialization
|
||||
IsReady = true;
|
||||
if (NitroxPrefs.SilenceChat.Value)
|
||||
{
|
||||
Log.InGame(Language.main.Get("Nitrox_SilencedChatNotif"));
|
||||
}
|
||||
}
|
||||
|
||||
public void WriteLogEntry(string playerName, string message, Color color)
|
||||
{
|
||||
if (entries.Count == MESSAGES_LIMIT)
|
||||
{
|
||||
Destroy(entries.Dequeue().EntryObject);
|
||||
}
|
||||
|
||||
ChatLogEntry chatLogEntry;
|
||||
GameObject chatLogEntryObject;
|
||||
if (entries.Count != 0 && entries.Last().PlayerName == playerName)
|
||||
{
|
||||
chatLogEntry = entries.Last();
|
||||
chatLogEntry.MessageText += $"{Environment.NewLine}{message}";
|
||||
chatLogEntry.UpdateTime();
|
||||
chatLogEntryObject = chatLogEntry.EntryObject;
|
||||
}
|
||||
else
|
||||
{
|
||||
chatLogEntry = new ChatLogEntry(playerName, SanitizeMessage(message), color);
|
||||
chatLogEntryObject = Instantiate(logEntryPrefab, logEntryPrefab.transform.parent, false);
|
||||
chatLogEntry.EntryObject = chatLogEntryObject;
|
||||
entries.Enqueue(chatLogEntry);
|
||||
}
|
||||
|
||||
chatLogEntryObject.GetComponent<PlayerChatLogItem>().ApplyOnPrefab(chatLogEntry);
|
||||
}
|
||||
|
||||
public void Show()
|
||||
{
|
||||
PlayerChatInputField.ResetTimer();
|
||||
StartCoroutine(ToggleChatFade(true));
|
||||
}
|
||||
|
||||
public void Hide()
|
||||
{
|
||||
StartCoroutine(ToggleChatFade(false));
|
||||
}
|
||||
|
||||
public void Select()
|
||||
{
|
||||
base.Select(true);
|
||||
inputField.Select();
|
||||
inputField.ActivateInputField();
|
||||
}
|
||||
|
||||
private static string SanitizeMessage(string message)
|
||||
{
|
||||
message = message.Trim().TrimEnd('\n').Trim();
|
||||
return message.Length < LINE_CHAR_LIMIT ? message : message.Substring(0, LINE_CHAR_LIMIT);
|
||||
}
|
||||
|
||||
private void ToggleBackgroundTransparency()
|
||||
{
|
||||
float alpha = transparent ? 1f : TOGGLED_TRANSPARENCY;
|
||||
transparent = !transparent;
|
||||
|
||||
foreach (Image backgroundImage in backgroundImages)
|
||||
{
|
||||
backgroundImage.CrossFadeAlpha(alpha, 0.5f, false);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator ToggleChatFade(bool fadeIn)
|
||||
{
|
||||
if (fadeIn)
|
||||
{
|
||||
while (canvasGroup.alpha < 1f)
|
||||
{
|
||||
canvasGroup.alpha += 0.01f;
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
while (canvasGroup.alpha > 0f)
|
||||
{
|
||||
canvasGroup.alpha -= 0.01f;
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
172
NitroxClient/MonoBehaviours/Gui/Chat/PlayerChatInputField.cs
Normal file
172
NitroxClient/MonoBehaviours/Gui/Chat/PlayerChatInputField.cs
Normal file
@@ -0,0 +1,172 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using NitroxClient.GameLogic.ChatUI;
|
||||
using NitroxModel.Core;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.Gui.Chat
|
||||
{
|
||||
public class PlayerChatInputField : MonoBehaviour, ISelectHandler, IDeselectHandler
|
||||
{
|
||||
private PlayerChatManager playerChatManager;
|
||||
private bool selected;
|
||||
private static float timeLeftUntilAutoClose;
|
||||
public static bool FreezeTime;
|
||||
public InputField InputField;
|
||||
|
||||
// Chat history
|
||||
private const int historyLength = 32; // 2^5 messages availables :D
|
||||
|
||||
private List<string> sentMessages;
|
||||
private int _sentMessagesIndex;
|
||||
private int sentMessagesIndex
|
||||
{
|
||||
get { return _sentMessagesIndex; }
|
||||
set
|
||||
{
|
||||
if (sentMessages.Count == 0)
|
||||
{
|
||||
// -1 is the state when there's no message sent
|
||||
_sentMessagesIndex = -1;
|
||||
}
|
||||
else if (value < 1)
|
||||
{
|
||||
sentMessagesIndex = 1;
|
||||
}
|
||||
else if (value > sentMessages.Count)
|
||||
{
|
||||
_sentMessagesIndex = sentMessages.Count;
|
||||
}
|
||||
else
|
||||
{
|
||||
// normal functionning
|
||||
InputField.text = sentMessages[value - 1];
|
||||
_sentMessagesIndex = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
playerChatManager = NitroxServiceLocator.LocateService<PlayerChatManager>();
|
||||
sentMessages = new();
|
||||
sentMessagesIndex = -1;
|
||||
}
|
||||
|
||||
public void OnSelect(BaseEventData eventData)
|
||||
{
|
||||
playerChatManager.SelectChat();
|
||||
selected = true;
|
||||
ResetTimer();
|
||||
}
|
||||
|
||||
public void OnDeselect(BaseEventData eventData)
|
||||
{
|
||||
selected = false;
|
||||
}
|
||||
|
||||
public static void ResetTimer()
|
||||
{
|
||||
timeLeftUntilAutoClose = PlayerChat.CHAT_VISIBILITY_TIME_LENGTH;
|
||||
FreezeTime = false;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (FreezeTime)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (selected)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(InputField.text))
|
||||
{
|
||||
if (UnityEngine.Input.GetKey(KeyCode.Return))
|
||||
{
|
||||
if (UnityEngine.Input.GetKey(KeyCode.LeftShift))
|
||||
{
|
||||
if (!InputField.text.EndsWith("\n"))
|
||||
{
|
||||
InputField.ActivateInputField();
|
||||
InputField.text += "\n";
|
||||
StartCoroutine(MoveToEndOfText());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Detect if there's a ghost message on top of the list (one that wasn't sent but still saved)
|
||||
if (sentMessagesIndex != sentMessages.Count && sentMessages.Count > 0)
|
||||
{
|
||||
sentMessages.RemoveAt(sentMessages.Count - 1);
|
||||
}
|
||||
|
||||
// If the list is too long, we'll just remove the first message of the list
|
||||
if (sentMessages.Count > historyLength)
|
||||
{
|
||||
sentMessages.RemoveAt(0);
|
||||
}
|
||||
|
||||
sentMessages.Add(InputField.text);
|
||||
_sentMessagesIndex = sentMessages.Count;
|
||||
playerChatManager.SendMessage();
|
||||
playerChatManager.DeselectChat(); // return to game after message sent
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (UnityEngine.Input.GetKey(KeyCode.Return))
|
||||
{
|
||||
ResetTimer();
|
||||
playerChatManager.DeselectChat();
|
||||
}
|
||||
}
|
||||
|
||||
// Chat history stuff
|
||||
// GetKeyDown means it's only getting executed once per press
|
||||
if (UnityEngine.Input.GetKeyDown(KeyCode.UpArrow))
|
||||
{
|
||||
// If we're currently on the newest message, we want to save it to be able to come back to it (a ghost message)
|
||||
if (sentMessagesIndex == sentMessages.Count && sentMessages.Count > 0)
|
||||
{
|
||||
sentMessages.Add(InputField.text);
|
||||
_sentMessagesIndex = sentMessages.Count;
|
||||
}
|
||||
sentMessagesIndex--;
|
||||
}
|
||||
else if(UnityEngine.Input.GetKeyDown(KeyCode.DownArrow))
|
||||
{
|
||||
// We shouldn't execute this check if we're already on top of the list
|
||||
if (sentMessagesIndex < sentMessages.Count)
|
||||
{
|
||||
sentMessagesIndex++;
|
||||
// If we're back to the newest message, we can delete it from the list because it has not been sent yet
|
||||
if (sentMessagesIndex == sentMessages.Count && sentMessages.Count > 0)
|
||||
{
|
||||
sentMessages.RemoveAt(sentMessages.Count - 1);
|
||||
_sentMessagesIndex = sentMessages.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
timeLeftUntilAutoClose -= Time.unscaledDeltaTime;
|
||||
if (timeLeftUntilAutoClose <= 0)
|
||||
{
|
||||
playerChatManager.HideChat();
|
||||
FreezeTime = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator MoveToEndOfText()
|
||||
{
|
||||
yield return null;
|
||||
InputField.MoveTextEnd(false);
|
||||
}
|
||||
}
|
||||
}
|
35
NitroxClient/MonoBehaviours/Gui/Chat/PlayerChatLogItem.cs
Normal file
35
NitroxClient/MonoBehaviours/Gui/Chat/PlayerChatLogItem.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using NitroxClient.GameLogic.ChatUI;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.Gui.Chat
|
||||
{
|
||||
public class PlayerChatLogItem : MonoBehaviour
|
||||
{
|
||||
private Text playerName;
|
||||
private Text time;
|
||||
private Text message;
|
||||
|
||||
private void SetupComponents()
|
||||
{
|
||||
Text[] textFields = gameObject.GetComponentsInChildren<Text>();
|
||||
playerName = textFields[0];
|
||||
time = textFields[1];
|
||||
message = textFields[2];
|
||||
}
|
||||
|
||||
public void ApplyOnPrefab(ChatLogEntry chatLogEntry)
|
||||
{
|
||||
if (playerName == null)
|
||||
{
|
||||
SetupComponents();
|
||||
}
|
||||
|
||||
playerName.text = chatLogEntry.PlayerName;
|
||||
playerName.color = chatLogEntry.PlayerColor;
|
||||
time.text = chatLogEntry.Time;
|
||||
message.text = chatLogEntry.MessageText;
|
||||
gameObject.SetActive(true);
|
||||
}
|
||||
}
|
||||
}
|
66
NitroxClient/MonoBehaviours/Gui/Chat/PlayerChatPinButton.cs
Normal file
66
NitroxClient/MonoBehaviours/Gui/Chat/PlayerChatPinButton.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using NitroxClient.GameLogic.ChatUI;
|
||||
using NitroxModel.Core;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.Gui.Chat
|
||||
{
|
||||
public class PlayerChatPinButton : MonoBehaviour, IPointerDownHandler, IPointerUpHandler
|
||||
{
|
||||
private static PlayerChatManager playerChatManager;
|
||||
|
||||
private readonly Camera mainCamera = Camera.main;
|
||||
private Vector2 screenRes = new Vector2(1920f, 1200f);
|
||||
private Vector2 chatSize;
|
||||
private Vector4 screenBorder;
|
||||
private Vector2 offset;
|
||||
private bool drag;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
playerChatManager = NitroxServiceLocator.LocateService<PlayerChatManager>();
|
||||
chatSize = transform.parent.parent.GetComponent<RectTransform>().sizeDelta;
|
||||
}
|
||||
|
||||
public void OnPointerDown(PointerEventData eventData)
|
||||
{
|
||||
screenRes.y = (screenRes.x / Screen.width) * Screen.height;
|
||||
offset = GetMouseWorldPosition() - (Vector2)playerChatManager.PlayerChaTransform.localPosition;
|
||||
screenBorder = new Vector4(-(screenRes.x - chatSize.x) / 2f, (screenRes.x - chatSize.x) / 2f, -(screenRes.y - chatSize.y) / 2f, (screenRes.y - chatSize.y) / 2f);
|
||||
|
||||
drag = true;
|
||||
PlayerChatInputField.FreezeTime = true;
|
||||
}
|
||||
|
||||
public void OnPointerUp(PointerEventData eventData)
|
||||
{
|
||||
drag = false;
|
||||
PlayerChatInputField.FreezeTime = false;
|
||||
PlayerChatInputField.ResetTimer();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (drag)
|
||||
{
|
||||
playerChatManager.PlayerChaTransform.localPosition = GetChatPosition();
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2 GetMouseWorldPosition()
|
||||
{
|
||||
Vector3 position = mainCamera.ScreenToViewportPoint(UnityEngine.Input.mousePosition);
|
||||
position.x = (position.x - 0.5f) * screenRes.x;
|
||||
position.y = (position.y - 0.5f) * screenRes.y;
|
||||
return position;
|
||||
}
|
||||
|
||||
private Vector2 GetChatPosition()
|
||||
{
|
||||
Vector2 position = GetMouseWorldPosition() - offset;
|
||||
position.x = Mathf.Clamp(position.x, screenBorder.x, screenBorder.y);
|
||||
position.y = Mathf.Clamp(position.y, screenBorder.z, screenBorder.w);
|
||||
return position;
|
||||
}
|
||||
}
|
||||
}
|
20
NitroxClient/MonoBehaviours/Gui/HUD/DenyOwnershipHand.cs
Normal file
20
NitroxClient/MonoBehaviours/Gui/HUD/DenyOwnershipHand.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.Gui.HUD;
|
||||
|
||||
public class DenyOwnershipHand : MonoBehaviour
|
||||
{
|
||||
private void Start()
|
||||
{
|
||||
// Forces the message to go away after a few seconds.
|
||||
Destroy(this, 2);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
//TODO: Check if this should be Hand
|
||||
HandReticle.main.SetText(HandReticle.TextType.Hand, "Nitrox_DenyOwnershipHand", true);
|
||||
HandReticle.main.SetText(HandReticle.TextType.HandSubscript, string.Empty, false);
|
||||
HandReticle.main.SetIcon(HandReticle.IconType.HandDeny);
|
||||
}
|
||||
}
|
287
NitroxClient/MonoBehaviours/Gui/HUD/RemotePlayerVitals.cs
Normal file
287
NitroxClient/MonoBehaviours/Gui/HUD/RemotePlayerVitals.cs
Normal file
@@ -0,0 +1,287 @@
|
||||
using System;
|
||||
using NitroxClient.GameLogic;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.Gui.HUD;
|
||||
|
||||
public class RemotePlayerVitals : MonoBehaviour
|
||||
{
|
||||
private static readonly Color OXYGEN_BAR_COLOR = new(0.168f, 0.666f, 0.60f, 1.0f);
|
||||
private static readonly Color OXYGEN_BAR_BORDER_COLOR = new(0.227f, 0.949f, 0.969f, 1.0f);
|
||||
private static readonly Color HEALTH_BAR_COLOR = new(0.859f, 0.373f, 0.251f, 1.0f);
|
||||
private static readonly Color HEALTH_BAR_BORDER_COLOR = new(0.824f, 0.651f, 0.424f, 1.0f);
|
||||
private static readonly Color FOOD_BAR_COLOR = new(0.965f, 0.655f, 0.149f, 1.0f);
|
||||
private static readonly Color FOOD_BAR_BORDER_COLOR = new(0.957f, 0.914f, 0.251f, 1.0f);
|
||||
private static readonly Color WATER_BAR_COLOR = new(0.212f, 0.663f, 0.855f, 1.0f);
|
||||
private static readonly Color WATER_BAR_BORDER_COLOR = new(0.227f, 0.949f, 0.969f, 1.0f);
|
||||
private Canvas canvas;
|
||||
private Bar foodBar;
|
||||
private Bar healthBar;
|
||||
private Bar oxygenBar;
|
||||
|
||||
private string playerName;
|
||||
|
||||
private Bar waterBar;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a player vitals UI elements for the player id.
|
||||
/// </summary>
|
||||
/// <param name="playerId">Unique player id to create the vitals UI elements for.</param>
|
||||
public static RemotePlayerVitals CreateForPlayer(RemotePlayer remotePlayer)
|
||||
{
|
||||
RemotePlayerVitals vitals = new GameObject("RemotePlayerVitals").AddComponent<RemotePlayerVitals>();
|
||||
|
||||
try
|
||||
{
|
||||
vitals.canvas = vitals.CreateCanvas(remotePlayer.Body.transform);
|
||||
|
||||
vitals.playerName = remotePlayer.PlayerName;
|
||||
vitals.CreatePlayerName(vitals.canvas);
|
||||
vitals.CreateStats(vitals.canvas);
|
||||
} catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, $"Encountered an error while creating vitals for player {remotePlayer.PlayerId}, destroying them.");
|
||||
Destroy(vitals.gameObject);
|
||||
return null;
|
||||
}
|
||||
|
||||
return vitals;
|
||||
}
|
||||
|
||||
public void SetStatsVisible(bool visible)
|
||||
{
|
||||
oxygenBar.SetVisible(visible);
|
||||
healthBar.SetVisible(visible);
|
||||
foodBar.SetVisible(visible);
|
||||
waterBar.SetVisible(visible);
|
||||
}
|
||||
|
||||
public void SetOxygen(float oxygen, float maxOxygen)
|
||||
{
|
||||
oxygenBar.SetTargetValue(oxygen);
|
||||
oxygenBar.SetMaxValue(maxOxygen);
|
||||
}
|
||||
|
||||
public void SetHealth(float health)
|
||||
{
|
||||
healthBar.SetTargetValue(health);
|
||||
}
|
||||
|
||||
public void SetFood(float food)
|
||||
{
|
||||
foodBar.SetTargetValue(food);
|
||||
}
|
||||
|
||||
public void SetWater(float water)
|
||||
{
|
||||
waterBar.SetTargetValue(water);
|
||||
}
|
||||
|
||||
public void LateUpdate()
|
||||
{
|
||||
oxygenBar.UpdateVisual();
|
||||
healthBar.UpdateVisual();
|
||||
foodBar.UpdateVisual();
|
||||
waterBar.UpdateVisual();
|
||||
|
||||
// Make canvas face camera.
|
||||
Camera camera = Camera.main;
|
||||
if (canvas && camera)
|
||||
{
|
||||
canvas.transform.forward = camera.transform.forward;
|
||||
}
|
||||
}
|
||||
|
||||
private Canvas CreateCanvas(Transform playerTransform)
|
||||
{
|
||||
// Canvas
|
||||
transform.SetParent(playerTransform, false);
|
||||
transform.localPosition = new Vector3(0, 0, 0);
|
||||
|
||||
Canvas vitalsCanvas = gameObject.AddComponent<Canvas>();
|
||||
vitalsCanvas.renderMode = RenderMode.WorldSpace;
|
||||
|
||||
CanvasScaler scaler = gameObject.AddComponent<CanvasScaler>();
|
||||
scaler.dynamicPixelsPerUnit = 100;
|
||||
|
||||
return vitalsCanvas;
|
||||
}
|
||||
|
||||
private void CreateStats(Canvas canvas)
|
||||
{
|
||||
// uGUI is a script at the topmost of the uGUI(Clone) object which contains the uGUI_ classes whe're looking for
|
||||
uGUI uGUI = uGUI.main;
|
||||
if (!uGUI)
|
||||
{
|
||||
throw new NullReferenceException($"[{nameof(RemotePlayerVitals)}] Couldn't find uGUI main instance when creating vitals");
|
||||
}
|
||||
healthBar = CreateBar(uGUI.GetComponentInChildren<uGUI_HealthBar>(true), canvas);
|
||||
oxygenBar = CreateBar(uGUI.GetComponentInChildren<uGUI_OxygenBar>(true), canvas);
|
||||
foodBar = CreateBar(uGUI.GetComponentInChildren<uGUI_FoodBar>(true), canvas);
|
||||
waterBar = CreateBar(uGUI.GetComponentInChildren<uGUI_WaterBar>(true), canvas);
|
||||
}
|
||||
|
||||
private Bar CreateBar<T>(T barBehaviour, Canvas canvas) where T : MonoBehaviour
|
||||
{
|
||||
GameObject originalBar = barBehaviour.gameObject;
|
||||
GameObject cloned = Instantiate(originalBar, canvas.transform, true);
|
||||
|
||||
uGUI_CircularBar newBar = cloned.GetComponentInChildren<uGUI_CircularBar>(true);
|
||||
newBar.texture = originalBar.GetComponentInChildren<uGUI_CircularBar>(true).texture;
|
||||
newBar.overlay = originalBar.GetComponentInChildren<uGUI_CircularBar>(true).overlay;
|
||||
|
||||
cloned.transform.localRotation = Quaternion.identity;
|
||||
// From uGUI_OxygenBar.Awake
|
||||
if (cloned.TryGetComponentInChildren(out TextMeshProUGUI text, true))
|
||||
{
|
||||
text.enableCulling = true;
|
||||
}
|
||||
|
||||
switch (barBehaviour)
|
||||
{
|
||||
case uGUI_HealthBar:
|
||||
newBar.color = HEALTH_BAR_COLOR;
|
||||
newBar.borderColor = HEALTH_BAR_BORDER_COLOR;
|
||||
cloned.transform.localPosition = new Vector3(-0.05f, 0.33f, 0f);
|
||||
cloned.transform.localScale = new Vector3(0.0014f, 0.0014f, 1f);
|
||||
break;
|
||||
case uGUI_OxygenBar:
|
||||
newBar.color = OXYGEN_BAR_COLOR;
|
||||
newBar.borderColor = OXYGEN_BAR_BORDER_COLOR;
|
||||
cloned.transform.localPosition = new Vector3(0.05f, 0.33f, 0f);
|
||||
cloned.transform.localScale = new Vector3(0.0006f, 0.0006f, 1f);
|
||||
// PulseWave is only present for uGUI_OxygenBar
|
||||
Destroy(cloned.FindChild("PulseWave"));
|
||||
break;
|
||||
case uGUI_FoodBar:
|
||||
newBar.color = FOOD_BAR_COLOR;
|
||||
newBar.borderColor = FOOD_BAR_BORDER_COLOR;
|
||||
cloned.transform.localPosition = new Vector3(-0.05f, 0.255f, 0f);
|
||||
cloned.transform.localScale = new Vector3(0.0014f, 0.0014f, 1f);
|
||||
break;
|
||||
case uGUI_WaterBar:
|
||||
newBar.color = WATER_BAR_COLOR;
|
||||
newBar.borderColor = WATER_BAR_BORDER_COLOR;
|
||||
cloned.transform.localPosition = new Vector3(0.05f, 0.255f, 0f);
|
||||
cloned.transform.localScale = new Vector3(0.0014f, 0.0014f, 1f);
|
||||
break;
|
||||
default:
|
||||
Log.Info($"Unhandled bar type: {barBehaviour.GetType()}");
|
||||
break;
|
||||
}
|
||||
|
||||
Destroy(cloned.FindChild("PulseHalo"));
|
||||
Destroy(cloned.GetComponent<T>());
|
||||
cloned.SetActive(true);
|
||||
return new Bar(cloned, 100f, 100f, 0.1f);
|
||||
}
|
||||
|
||||
private void CreatePlayerName(Canvas canvas)
|
||||
{
|
||||
// Text
|
||||
GameObject nameObject = new("RemotePlayerName");
|
||||
nameObject.transform.parent = canvas.transform;
|
||||
|
||||
Text nameText = nameObject.AddComponent<Text>();
|
||||
nameText.font = Resources.GetBuiltinResource<Font>("Arial.ttf");
|
||||
nameText.text = playerName;
|
||||
Transform nameTransform = nameText.transform;
|
||||
nameTransform.localScale = new Vector3(0.015f, 0.015f, 1f);
|
||||
nameTransform.rotation = canvas.transform.rotation;
|
||||
nameText.fontSize = 14;
|
||||
nameText.alignment = TextAnchor.MiddleCenter;
|
||||
|
||||
// Text position
|
||||
RectTransform namePosition = nameObject.GetComponent<RectTransform>();
|
||||
namePosition.localPosition = new Vector3(0, 0.4f, 0);
|
||||
namePosition.sizeDelta = new Vector2(200, 100);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
// Must stay optional in case the destroy originates from a broken object
|
||||
oxygenBar?.Dispose();
|
||||
healthBar?.Dispose();
|
||||
foodBar?.Dispose();
|
||||
waterBar?.Dispose();
|
||||
}
|
||||
|
||||
private class Bar : IDisposable
|
||||
{
|
||||
private readonly GameObject gameObject;
|
||||
private readonly uGUI_CircularBar circularBar;
|
||||
private readonly TextMeshProUGUI text;
|
||||
|
||||
private bool isDisposed;
|
||||
private float vel;
|
||||
private float current;
|
||||
private float target;
|
||||
private float maximum;
|
||||
private float smoothTime;
|
||||
|
||||
public Bar(GameObject gameObject, float current, float maximum, float smoothTime)
|
||||
{
|
||||
this.gameObject = gameObject;
|
||||
this.current = current;
|
||||
target = current;
|
||||
this.maximum = maximum;
|
||||
this.smoothTime = smoothTime;
|
||||
|
||||
circularBar = gameObject.GetComponentInChildren<uGUI_CircularBar>(true);
|
||||
// text can be null
|
||||
text = gameObject.GetComponentInChildren<TextMeshProUGUI>(true);
|
||||
}
|
||||
|
||||
public void SetTargetValue(float value)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
target = value;
|
||||
}
|
||||
|
||||
public void SetMaxValue(float maxValue)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
maximum = maxValue;
|
||||
}
|
||||
|
||||
public void UpdateVisual()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
// Adapted from uGUI_OxygenBar
|
||||
float percentage = Mathf.Clamp01(target / maximum);
|
||||
current = Mathf.SmoothDamp(current, percentage, ref vel, smoothTime);
|
||||
circularBar.value = current;
|
||||
if (text)
|
||||
{
|
||||
text.SetText(IntStringCache.GetStringForInt(Mathf.RoundToInt(target)));
|
||||
}
|
||||
}
|
||||
|
||||
public void SetVisible(bool visible)
|
||||
{
|
||||
gameObject.SetActive(visible);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
isDisposed = true;
|
||||
Destroy(gameObject);
|
||||
}
|
||||
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
if (isDisposed)
|
||||
{
|
||||
throw new ObjectDisposedException("Tried to update visual on a disposed player stat.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,44 @@
|
||||
using NitroxModel.Helper;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.XR;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.Gui.MainMenu;
|
||||
|
||||
public static class LoadingScreenVersionText
|
||||
{
|
||||
private static GameObject buildWatermark => uGUI.main.overlays.transform.parent.GetComponentInChildren<uGUI_BuildWatermark>().gameObject;
|
||||
|
||||
private static uGUI_TextFade loadingScreenWarning;
|
||||
private static uGUI_TextFade versionText;
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
versionText = AddTextToLoadingScreen("LoadingScreenVersionText", $"\nNitrox {NitroxEnvironment.ReleasePhase} V{NitroxEnvironment.Version}");
|
||||
loadingScreenWarning = AddTextToLoadingScreen("LoadingScreenWarnText", $"\n\n{Language.main.Get("Nitrox_LoadingScreenWarn")}");
|
||||
}
|
||||
|
||||
private static uGUI_TextFade AddTextToLoadingScreen(string name, string text)
|
||||
{
|
||||
GameObject gameObject = Object.Instantiate(buildWatermark, buildWatermark.transform.parent);
|
||||
gameObject.name = name;
|
||||
Object.Destroy(gameObject.GetComponent<uGUI_BuildWatermark>());
|
||||
|
||||
uGUI_TextFade textFade = gameObject.AddComponent<uGUI_TextFade>();
|
||||
textFade.SetAlignment(TextAlignmentOptions.TopRight);
|
||||
textFade.SetColor(Color.white.WithAlpha(0.5f));
|
||||
textFade.SetText(text);
|
||||
textFade.FadeIn(1f, null);
|
||||
|
||||
return textFade;
|
||||
}
|
||||
|
||||
public static void DisableWarningText()
|
||||
{
|
||||
loadingScreenWarning.FadeOut(1f, null);
|
||||
if (XRSettings.enabled)
|
||||
{
|
||||
versionText.FadeOut(1f, null);
|
||||
}
|
||||
}
|
||||
}
|
40
NitroxClient/MonoBehaviours/Gui/Input/KeyBindingManager.cs
Normal file
40
NitroxClient/MonoBehaviours/Gui/Input/KeyBindingManager.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NitroxClient.MonoBehaviours.Gui.Input.KeyBindings;
|
||||
using NitroxClient.MonoBehaviours.Gui.Input.KeyBindings.Actions;
|
||||
using NitroxClient.Serialization;
|
||||
using NitroxModel.Helper;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.Gui.Input
|
||||
{
|
||||
public class KeyBindingManager
|
||||
{
|
||||
public List<KeyBinding> KeyboardKeyBindings { get; }
|
||||
|
||||
public KeyBindingManager()
|
||||
{
|
||||
ClientConfig cfg = ClientConfig.Load(NitroxUser.AppDataPath);
|
||||
KeyboardKeyBindings = new List<KeyBinding>
|
||||
{
|
||||
// new bindings should not be set to a value equivalent to a pre-existing GameInput.Button enum or another custom binding
|
||||
new((int)KeyBindingValues.CHAT, "Chat", GameInput.Device.Keyboard, new ChatKeyBindingAction(), cfg.OpenChatKeybindPrimary, cfg.OpenChatKeybindSecondary),
|
||||
new((int)KeyBindingValues.FOCUS_DISCORD, "Focus Discord (Alt +)", GameInput.Device.Keyboard, new DiscordFocusBindingAction(), cfg.FocusDiscordKeybindPrimary, cfg.FocusDiscordKeybindSecondary),
|
||||
};
|
||||
}
|
||||
|
||||
// Returns highest custom key binding value. If no custom key bindings, returns 0.
|
||||
public int GetHighestKeyBindingValue()
|
||||
{
|
||||
return KeyboardKeyBindings.Select(keyBinding => (int)keyBinding.Button).DefaultIfEmpty(0).Max();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refers the keybinding values used for button creation in the options menu, to a more clear form
|
||||
/// </summary>
|
||||
public enum KeyBindingValues
|
||||
{
|
||||
CHAT = 45,
|
||||
FOCUS_DISCORD = 46
|
||||
}
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
using NitroxClient.GameLogic.ChatUI;
|
||||
using NitroxModel.Core;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.Gui.Input.KeyBindings.Actions
|
||||
{
|
||||
public class ChatKeyBindingAction : KeyBindingAction
|
||||
{
|
||||
public override void Execute()
|
||||
{
|
||||
// If no other UWE input field is currently active then allow chat to open.
|
||||
if (FPSInputModule.current.lastGroup == null)
|
||||
{
|
||||
NitroxServiceLocator.LocateService<PlayerChatManager>().SelectChat();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
using NitroxClient.MonoBehaviours.Discord;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.Gui.Input.KeyBindings.Actions;
|
||||
|
||||
public class DiscordFocusBindingAction : KeyBindingAction
|
||||
{
|
||||
public override void Execute()
|
||||
{
|
||||
if (UnityEngine.Input.GetKey(KeyCode.LeftAlt))
|
||||
{
|
||||
DiscordJoinRequestGui.Select();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
namespace NitroxClient.MonoBehaviours.Gui.Input.KeyBindings.Actions
|
||||
{
|
||||
public abstract class KeyBindingAction
|
||||
{
|
||||
public abstract void Execute();
|
||||
}
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
namespace NitroxClient.MonoBehaviours.Gui.Input.KeyBindings
|
||||
{
|
||||
public class DefaultKeyBinding
|
||||
{
|
||||
public string Binding { get; }
|
||||
public GameInput.BindingSet BindingSet { get; }
|
||||
|
||||
public DefaultKeyBinding(string defaultBinding, GameInput.BindingSet defaultBindingSet)
|
||||
{
|
||||
Binding = defaultBinding;
|
||||
BindingSet = defaultBindingSet;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
using NitroxClient.MonoBehaviours.Gui.Input.KeyBindings.Actions;
|
||||
using NitroxModel.Helper;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.Gui.Input.KeyBindings;
|
||||
|
||||
public class KeyBinding
|
||||
{
|
||||
public GameInput.Button Button { get; }
|
||||
public GameInput.Device Device { get; }
|
||||
public string Label { get; }
|
||||
public string PrimaryKey { get; }
|
||||
public string SecondaryKey { get; }
|
||||
public KeyBindingAction Action { get; }
|
||||
|
||||
public KeyBinding(int keyBindingValue, string buttonLabel, GameInput.Device buttonDevice, KeyBindingAction buttonAction, string primaryKey, string secondaryKey = null)
|
||||
{
|
||||
Validate.NotNull(primaryKey);
|
||||
|
||||
Button = (GameInput.Button)keyBindingValue;
|
||||
Device = buttonDevice;
|
||||
Label = buttonLabel;
|
||||
Action = buttonAction;
|
||||
PrimaryKey = primaryKey;
|
||||
SecondaryKey = secondaryKey;
|
||||
}
|
||||
}
|
@@ -0,0 +1,182 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.ResourceManagement.AsyncOperations;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.Gui.MainMenu;
|
||||
|
||||
public class MainMenuNotificationPanel : MonoBehaviour, uGUI_INavigableIconGrid, uGUI_IButtonReceiver
|
||||
{
|
||||
public const string NAME = "MultiplayerNotification";
|
||||
|
||||
private static MainMenuNotificationPanel instance;
|
||||
|
||||
private Image loadingCircle;
|
||||
private TextMeshProUGUI text;
|
||||
private GameObject confirmObject;
|
||||
private Button confirmButton;
|
||||
private mGUI_Change_Legend_On_Select confirmButtonLegend;
|
||||
private LegendButtonData[] savedLegendData;
|
||||
|
||||
private string returningMenuPanel;
|
||||
private Action continuationAction;
|
||||
|
||||
public static void ShowLoading()
|
||||
{
|
||||
if (!instance)
|
||||
{
|
||||
Log.Error($"Tried to use {nameof(ShowLoading)} while {nameof(MainMenuNotificationPanel)} was not ready");
|
||||
return;
|
||||
}
|
||||
|
||||
instance.confirmObject.SetActive(false);
|
||||
instance.loadingCircle.gameObject.SetActive(true);
|
||||
instance.text.text = Language.main.Get("Nitrox_Loading");
|
||||
uGUI_MainMenu.main.ShowPrimaryOptions(true);
|
||||
MainMenuRightSide.main.OpenGroup(NAME);
|
||||
instance.confirmButtonLegend.legendButtonConfiguration = [];
|
||||
}
|
||||
|
||||
public static void ShowMessage(string message, string returningMenuPanel, Action continuationAction = null)
|
||||
{
|
||||
if (!instance)
|
||||
{
|
||||
Log.Error("Tried to use ShowMessage() while MainMenuJoinServerNotificationPanel was not ready");
|
||||
return;
|
||||
}
|
||||
|
||||
instance.text.text = message;
|
||||
instance.returningMenuPanel = returningMenuPanel;
|
||||
instance.continuationAction = continuationAction;
|
||||
instance.confirmObject.SetActive(true);
|
||||
instance.loadingCircle.gameObject.SetActive(false);
|
||||
uGUI_MainMenu.main.ShowPrimaryOptions(true);
|
||||
MainMenuRightSide.main.OpenGroup(NAME);
|
||||
instance.confirmButtonLegend.legendButtonConfiguration = instance.savedLegendData;
|
||||
}
|
||||
|
||||
public void Setup(GameObject savedGamesRef)
|
||||
{
|
||||
instance = this;
|
||||
Destroy(transform.RequireGameObject("Scroll View"));
|
||||
Destroy(GetComponentInChildren<TranslationLiveUpdate>());
|
||||
|
||||
text = GetComponentInChildren<TextMeshProUGUI>();
|
||||
text.horizontalAlignment = HorizontalAlignmentOptions.Center;
|
||||
text.verticalAlignment = VerticalAlignmentOptions.Top;
|
||||
text.transform.localPosition = new Vector3(-375, 350, 0);
|
||||
text.GetComponent<RectTransform>().sizeDelta = new Vector2(350, 280);
|
||||
|
||||
GameObject multiplayerButtonRef = savedGamesRef.RequireGameObject("Scroll View/Viewport/SavedGameAreaContent/NewGame");
|
||||
|
||||
confirmObject = Instantiate(multiplayerButtonRef, transform, false);
|
||||
confirmObject.transform.localPosition = new Vector3(-200, 50, 0);
|
||||
confirmObject.transform.localScale = new Vector3(1.25f, 1.25f, 1.25f);
|
||||
confirmObject.transform.GetChild(0).GetComponent<RectTransform>().sizeDelta = new Vector2(200, 40);
|
||||
confirmObject.GetComponentInChildren<TextMeshProUGUI>().text = Language.main.Get("Nitrox_OK");
|
||||
confirmButton = confirmObject.RequireTransform("NewGameButton").GetComponent<Button>();
|
||||
confirmButton.onClick = new Button.ButtonClickedEvent();
|
||||
confirmButton.onClick.AddListener(() =>
|
||||
{
|
||||
continuationAction?.Invoke();
|
||||
if (!string.IsNullOrEmpty(returningMenuPanel))
|
||||
{
|
||||
MainMenuRightSide.main.OpenGroup(returningMenuPanel);
|
||||
}
|
||||
});
|
||||
|
||||
confirmButtonLegend = confirmButton.GetComponent<mGUI_Change_Legend_On_Select>();
|
||||
savedLegendData = confirmButtonLegend.legendButtonConfiguration.Take(1).ToArray();
|
||||
|
||||
GameObject loadingCircleObject = new("LoadingCircle");
|
||||
loadingCircle = loadingCircleObject.AddComponent<Image>();
|
||||
loadingCircleObject.transform.SetParent(transform);
|
||||
loadingCircleObject.transform.localPosition = new Vector3(-200, 180, 0);
|
||||
loadingCircleObject.transform.localRotation = Quaternion.identity;
|
||||
loadingCircleObject.transform.localScale = Vector3.one;
|
||||
}
|
||||
|
||||
private IEnumerator Start()
|
||||
{
|
||||
AsyncOperationHandle<Texture2D> request = AddressablesUtility.LoadAsync<Texture2D>("Assets/uGUI/Sources/Sprites/HUD/Progress.png");
|
||||
yield return request;
|
||||
|
||||
Texture2D tex = request.Result;
|
||||
loadingCircle.sprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f));
|
||||
loadingCircle.type = Image.Type.Filled;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (loadingCircle)
|
||||
{
|
||||
loadingCircle.transform.localRotation = Quaternion.Euler(0, 0, Time.time % 360 * 250); // 250 is the speed
|
||||
loadingCircle.fillAmount = Mathf.Lerp(0.05f, 0.95f, Math.Abs(Time.time % 6 - 3) * 0.333f); // Lerps t fades from 0 to 1 and back to 0
|
||||
uGUI_LegendBar.ClearButtons();
|
||||
}
|
||||
}
|
||||
|
||||
bool uGUI_INavigableIconGrid.ShowSelector => false;
|
||||
bool uGUI_INavigableIconGrid.EmulateRaycast => false;
|
||||
bool uGUI_INavigableIconGrid.SelectItemClosestToPosition(Vector3 worldPos) => false;
|
||||
uGUI_INavigableIconGrid uGUI_INavigableIconGrid.GetNavigableGridInDirection(int dirX, int dirY) => null;
|
||||
Graphic uGUI_INavigableIconGrid.GetSelectedIcon() => null;
|
||||
|
||||
object uGUI_INavigableIconGrid.GetSelectedItem() => confirmObject ? confirmObject : null;
|
||||
|
||||
public bool SelectItemInDirection(int dirX, int dirY) => SelectFirstItem();
|
||||
|
||||
public bool SelectFirstItem()
|
||||
{
|
||||
if (confirmObject)
|
||||
{
|
||||
SelectItem(confirmObject);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void SelectItem(object item)
|
||||
{
|
||||
DeselectItem();
|
||||
GameObject selectedItem = item as GameObject;
|
||||
|
||||
if (selectedItem && selectedItem == confirmObject)
|
||||
{
|
||||
confirmButton.Select();
|
||||
confirmButtonLegend.SyncLegendBarToGUISelection();
|
||||
}
|
||||
}
|
||||
|
||||
public void DeselectItem()
|
||||
{
|
||||
if (confirmObject)
|
||||
{
|
||||
EventSystem.current.SetSelectedGameObject(null);
|
||||
confirmObject.GetComponentInChildren<uGUI_BasicColorSwap>().makeTextWhite();
|
||||
}
|
||||
uGUI_LegendBar.ClearButtons();
|
||||
}
|
||||
|
||||
public bool OnButtonDown(GameInput.Button button)
|
||||
{
|
||||
switch (button)
|
||||
{
|
||||
case GameInput.Button.UISubmit:
|
||||
case GameInput.Button.UICancel:
|
||||
if (confirmObject.activeSelf)
|
||||
{
|
||||
confirmButton.onClick.Invoke();
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,83 @@
|
||||
using NitroxClient.MonoBehaviours.Discord;
|
||||
using NitroxClient.MonoBehaviours.Gui.MainMenu.ServerJoin;
|
||||
using NitroxClient.MonoBehaviours.Gui.MainMenu.ServersList;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.Gui.MainMenu;
|
||||
|
||||
public class NitroxMainMenuModifications : MonoBehaviour
|
||||
{
|
||||
private MainMenuRightSide rightSide;
|
||||
|
||||
private void OnEnable() => SceneManager.sceneLoaded += SceneManager_sceneLoaded;
|
||||
|
||||
private void OnDisable() => SceneManager.sceneLoaded -= SceneManager_sceneLoaded;
|
||||
|
||||
private void SceneManager_sceneLoaded(Scene scene, LoadSceneMode loadMode)
|
||||
{
|
||||
if (scene.name == "XMenu")
|
||||
{
|
||||
rightSide = MainMenuRightSide.main;
|
||||
MultiplayerMenuMods();
|
||||
DiscordClient.InitializeRPMenu();
|
||||
}
|
||||
}
|
||||
|
||||
private void MultiplayerMenuMods()
|
||||
{
|
||||
GameObject startButton = GameObjectHelper.RequireGameObject("Menu canvas/Panel/MainMenu/PrimaryOptions/MenuButtons/ButtonPlay");
|
||||
GameObject showLoadedMultiplayer = Instantiate(startButton, startButton.transform.parent);
|
||||
showLoadedMultiplayer.name = "ButtonMultiplayer";
|
||||
showLoadedMultiplayer.transform.SetSiblingIndex(3);
|
||||
|
||||
TextMeshProUGUI buttonText = showLoadedMultiplayer.RequireGameObject("Circle/Bar/Text").GetComponent<TextMeshProUGUI>();
|
||||
buttonText.text = Language.main.Get("Nitrox_Multiplayer");
|
||||
buttonText.GetComponent<TranslationLiveUpdate>().translationKey = "Nitrox_Multiplayer";
|
||||
|
||||
Button showLoadedMultiplayerButton = showLoadedMultiplayer.GetComponent<Button>();
|
||||
showLoadedMultiplayerButton.onClick = new Button.ButtonClickedEvent();
|
||||
showLoadedMultiplayerButton.onClick.AddListener(() => rightSide.OpenGroup(MainMenuServerListPanel.NAME));
|
||||
|
||||
GameObject savedGamesRef = rightSide.gameObject.RequireGameObject("SavedGames");
|
||||
|
||||
GameObject CloneMainMenuLoadPanel(string panelName, string translationKey)
|
||||
{
|
||||
GameObject menuPanel = Instantiate(savedGamesRef, rightSide.transform);
|
||||
menuPanel.name = panelName;
|
||||
Transform header = menuPanel.RequireTransform("Header");
|
||||
header.GetComponent<TextMeshProUGUI>().text = Language.main.Get(translationKey);
|
||||
header.GetComponent<TranslationLiveUpdate>().translationKey = translationKey;
|
||||
Destroy(menuPanel.RequireGameObject("Scroll View/Viewport/SavedGameAreaContent/NewGame"));
|
||||
Destroy(menuPanel.GetComponent<MainMenuLoadPanel>());
|
||||
Destroy(menuPanel.GetComponentInChildren<MainMenuLoadMenu>());
|
||||
|
||||
rightSide.groups.Add(menuPanel.GetComponent<MainMenuGroup>());
|
||||
return menuPanel;
|
||||
}
|
||||
|
||||
GameObject serverJoinNotification = CloneMainMenuLoadPanel(MainMenuNotificationPanel.NAME, string.Empty);
|
||||
serverJoinNotification.AddComponent<MainMenuNotificationPanel>().Setup(savedGamesRef);
|
||||
|
||||
GameObject serverJoin = CloneMainMenuLoadPanel(MainMenuJoinServerPanel.NAME, "Nitrox_JoinServer");
|
||||
serverJoin.AddComponent<MainMenuJoinServerPanel>().Setup(savedGamesRef);
|
||||
|
||||
GameObject serverPasswordEnter = CloneMainMenuLoadPanel(MainMenuEnterPasswordPanel.NAME, "Nitrox_JoinServerPasswordHeader");
|
||||
serverPasswordEnter.AddComponent<MainMenuEnterPasswordPanel>().Setup(savedGamesRef);
|
||||
|
||||
GameObject serverList = CloneMainMenuLoadPanel(MainMenuServerListPanel.NAME, "Nitrox_Multiplayer");
|
||||
serverList.AddComponent<MainMenuServerListPanel>().Setup(savedGamesRef);
|
||||
|
||||
GameObject serverCreate = CloneMainMenuLoadPanel(MainMenuCreateServerPanel.NAME, "Nitrox_AddServer");
|
||||
serverCreate.AddComponent<MainMenuCreateServerPanel>().Setup(savedGamesRef);
|
||||
|
||||
#if RELEASE
|
||||
// Remove singleplayer button because SP is broken when Nitrox is injected.
|
||||
// TODO: Allow SP to work and co-exist with Nitrox MP in the future
|
||||
startButton.SetActive(false);
|
||||
#endif
|
||||
}
|
||||
}
|
@@ -0,0 +1,186 @@
|
||||
using System.Collections;
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using NitroxClient.Communication.Abstract;
|
||||
using NitroxClient.Communication.Exceptions;
|
||||
using NitroxClient.Communication.MultiplayerSession;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerPreferences;
|
||||
using NitroxClient.MonoBehaviours.Gui.MainMenu.ServersList;
|
||||
using NitroxModel.Core;
|
||||
using NitroxModel.DataStructures.Util;
|
||||
using NitroxModel.Helper;
|
||||
using NitroxModel.MultiplayerSession;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.Gui.MainMenu.ServerJoin;
|
||||
|
||||
public static class JoinServerBackend
|
||||
{
|
||||
private static PlayerPreferenceManager preferencesManager;
|
||||
private static PlayerPreference activePlayerPreference;
|
||||
private static IMultiplayerSession multiplayerSession;
|
||||
|
||||
private static GameObject multiplayerClient;
|
||||
|
||||
private static string serverIp;
|
||||
private static int serverPort;
|
||||
|
||||
public static void RequestSessionReservation(string playerName, Color playerColor)
|
||||
{
|
||||
preferencesManager.SetPreference(serverIp, new PlayerPreference(playerName, playerColor));
|
||||
|
||||
Optional<string> opPassword = MainMenuEnterPasswordPanel.LastEnteredPassword;
|
||||
AuthenticationContext authenticationContext = new(playerName, opPassword);
|
||||
|
||||
multiplayerSession.RequestSessionReservation(new PlayerSettings(playerColor.ToDto()), authenticationContext);
|
||||
}
|
||||
|
||||
private static void SessionConnectionStateChangedHandler(IMultiplayerSessionConnectionState state)
|
||||
{
|
||||
switch (state.CurrentStage)
|
||||
{
|
||||
case MultiplayerSessionConnectionStage.ESTABLISHING_SERVER_POLICY:
|
||||
Log.Info("Requesting session policy info");
|
||||
Log.InGame(Language.main.Get("Nitrox_RequestingSessionPolicy"));
|
||||
break;
|
||||
|
||||
case MultiplayerSessionConnectionStage.AWAITING_RESERVATION_CREDENTIALS:
|
||||
Color.RGBToHSV(activePlayerPreference.PreferredColor(), out float hue, out float saturation, out float brightness); // HSV => Hue Saturation Value, HSB => Hue Saturation Brightness
|
||||
MainMenuJoinServerPanel.Instance.UpdatePlayerPanelValues(activePlayerPreference.PlayerName, new Vector3(hue, saturation, brightness));
|
||||
|
||||
if (multiplayerSession.SessionPolicy.RequiresServerPassword)
|
||||
{
|
||||
Log.Info("Waiting for server password input");
|
||||
Log.InGame(Language.main.Get("Nitrox_WaitingPassword"));
|
||||
MainMenuEnterPasswordPanel.ResetLastEnteredPassword();
|
||||
MainMenuRightSide.main.OpenGroup(MainMenuEnterPasswordPanel.NAME);
|
||||
MainMenuEnterPasswordPanel.Instance.FocusPasswordField();
|
||||
break;
|
||||
}
|
||||
|
||||
Log.Info("Waiting for user input");
|
||||
Log.InGame(Language.main.Get("Nitrox_WaitingUserInput"));
|
||||
MainMenuRightSide.main.OpenGroup(MainMenuJoinServerPanel.NAME);
|
||||
MainMenuJoinServerPanel.Instance.FocusNameInputField();
|
||||
break;
|
||||
|
||||
case MultiplayerSessionConnectionStage.SESSION_RESERVED:
|
||||
Log.Info("Launching game");
|
||||
Log.InGame(Language.main.Get("Nitrox_LaunchGame"));
|
||||
multiplayerSession.ConnectionStateChanged -= SessionConnectionStateChangedHandler;
|
||||
preferencesManager.Save();
|
||||
StartGame();
|
||||
break;
|
||||
|
||||
case MultiplayerSessionConnectionStage.SESSION_RESERVATION_REJECTED:
|
||||
Log.Info("Reservation rejected");
|
||||
Log.InGame(Language.main.Get("Nitrox_RejectedSessionPolicy"));
|
||||
|
||||
MultiplayerSessionReservationState reservationState = multiplayerSession.Reservation.ReservationState;
|
||||
|
||||
string reservationRejectionNotification = reservationState.Describe();
|
||||
|
||||
MainMenuNotificationPanel.ShowMessage(reservationRejectionNotification, null, () =>
|
||||
{
|
||||
multiplayerSession.Disconnect();
|
||||
multiplayerSession.ConnectAsync(serverIp, serverPort);
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
case MultiplayerSessionConnectionStage.DISCONNECTED:
|
||||
Log.Info(Language.main.Get("Nitrox_DisconnectedSession"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task StartMultiplayerClientAsync(IPAddress ip, int port)
|
||||
{
|
||||
serverIp = ip.ToString();
|
||||
serverPort = port;
|
||||
NitroxServiceLocator.BeginNewLifetimeScope();
|
||||
|
||||
preferencesManager = NitroxServiceLocator.LocateService<PlayerPreferenceManager>();
|
||||
activePlayerPreference = preferencesManager.GetPreference(serverIp);
|
||||
multiplayerSession = NitroxServiceLocator.LocateService<IMultiplayerSession>();
|
||||
|
||||
if (!multiplayerClient)
|
||||
{
|
||||
multiplayerClient = new GameObject("Nitrox Multiplayer Client");
|
||||
multiplayerClient.AddComponent<Multiplayer>();
|
||||
multiplayerSession.ConnectionStateChanged += SessionConnectionStateChangedHandler;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await multiplayerSession.ConnectAsync(serverIp, serverPort);
|
||||
}
|
||||
catch (ClientConnectionFailedException ex)
|
||||
{
|
||||
Log.ErrorSensitive("Unable to contact the remote server at: {ip}:{port}", serverIp, serverPort);
|
||||
string msg = $"{Language.main.Get("Nitrox_UnableToConnect")} {serverIp}:{serverPort}";
|
||||
|
||||
if (ip.IsLocalhost())
|
||||
{
|
||||
if (Process.GetProcessesByName("NitroxServer-Subnautica").Length == 0)
|
||||
{
|
||||
Log.Error("No server process was found while address was localhost");
|
||||
msg += $"\n{Language.main.Get("Nitrox_StartServer")}";
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error(ex);
|
||||
msg += $"\n{Language.main.Get("Nitrox_FirewallInterfering")}";
|
||||
}
|
||||
}
|
||||
|
||||
Log.InGame(msg);
|
||||
StopMultiplayerClient();
|
||||
MainMenuNotificationPanel.ShowMessage(msg, MainMenuServerListPanel.NAME);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method starts a connection with the provided server but leaves handling the session negotiation for the caller.
|
||||
/// </summary>
|
||||
public static async Task StartDetachedMultiplayerClientAsync(IPAddress ip, int port, MultiplayerSessionConnectionStateChangedEventHandler sessionHandler)
|
||||
{
|
||||
multiplayerClient = new GameObject("Nitrox Multiplayer Client");
|
||||
Task task = StartMultiplayerClientAsync(ip, port);
|
||||
multiplayerClient.AddComponent<Multiplayer>();
|
||||
multiplayerSession.ConnectionStateChanged += sessionHandler;
|
||||
await task;
|
||||
}
|
||||
|
||||
public static void StartGame()
|
||||
{
|
||||
#pragma warning disable CS0618 // God Damn it UWE...
|
||||
Multiplayer.SubnauticaLoadingStarted();
|
||||
IEnumerator startNewGame = uGUI_MainMenu.main.StartNewGame(GameMode.Survival);
|
||||
#pragma warning restore CS0618 // God damn it UWE...
|
||||
UWE.CoroutineHost.StartCoroutine(startNewGame);
|
||||
LoadingScreenVersionText.Initialize();
|
||||
}
|
||||
|
||||
public static void StopMultiplayerClient()
|
||||
{
|
||||
if (!multiplayerClient || !Multiplayer.Main)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (multiplayerSession.CurrentState.CurrentStage != MultiplayerSessionConnectionStage.DISCONNECTED)
|
||||
{
|
||||
multiplayerSession.Disconnect();
|
||||
}
|
||||
multiplayerSession.ConnectionStateChanged -= SessionConnectionStateChangedHandler;
|
||||
|
||||
Multiplayer.Main.StopCurrentSession();
|
||||
NitroxServiceLocator.EndCurrentLifetimeScope(); //Always do this last.
|
||||
|
||||
Object.Destroy(multiplayerClient);
|
||||
multiplayerClient = null;
|
||||
}
|
||||
}
|
@@ -0,0 +1,59 @@
|
||||
using NitroxClient.Unity.Helper;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.Gui.MainMenu.ServerJoin;
|
||||
|
||||
public class MainMenuColorPickerPreview : MonoBehaviour, IPointerDownHandler, IPointerUpHandler
|
||||
{
|
||||
private Image previewImage;
|
||||
private CanvasGroup cg;
|
||||
|
||||
public void Init(uGUI_ColorPicker colorPicker)
|
||||
{
|
||||
GameObject colorPreview = new("ColorPreview");
|
||||
colorPreview.transform.SetParent(colorPicker.pointer.transform);
|
||||
colorPreview.transform.localPosition = new Vector3(-30, 30, 0);
|
||||
colorPreview.transform.localRotation = Quaternion.identity;
|
||||
colorPreview.transform.localScale = new Vector3(0.4f, 0.4f, 0.4f);
|
||||
previewImage = colorPreview.AddComponent<Image>();
|
||||
previewImage.sprite = CreateCircleSprite();
|
||||
cg = colorPreview.AddComponent<CanvasGroup>();
|
||||
cg.alpha = 0;
|
||||
|
||||
colorPicker.onColorChange.AddListener(OnColorPickerDrag);
|
||||
}
|
||||
|
||||
private static Sprite CreateCircleSprite()
|
||||
{
|
||||
const int HALF_SIZE = 50;
|
||||
const int RADIUS = 42;
|
||||
Texture2D tex = new(HALF_SIZE * 2, HALF_SIZE * 2);
|
||||
for (int y = -HALF_SIZE; y <= HALF_SIZE; y++)
|
||||
{
|
||||
for (int x = -HALF_SIZE; x <= HALF_SIZE; x++)
|
||||
{
|
||||
bool isInsideCircle = x * x + y * y <= RADIUS * RADIUS;
|
||||
tex.SetPixel(HALF_SIZE + x, HALF_SIZE + y, isInsideCircle ? Color.white : Color.clear);
|
||||
}
|
||||
}
|
||||
|
||||
tex.Apply();
|
||||
return Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f), 200);
|
||||
}
|
||||
|
||||
private void OnColorPickerDrag(ColorChangeEventData data) => previewImage.color = data.color;
|
||||
|
||||
public void OnPointerDown(PointerEventData _)
|
||||
{
|
||||
StopAllCoroutines();
|
||||
StartCoroutine(cg.ShiftAlpha(1, 0.25f, 1.5f, true));
|
||||
}
|
||||
|
||||
public void OnPointerUp(PointerEventData _)
|
||||
{
|
||||
StopAllCoroutines();
|
||||
StartCoroutine(cg.ShiftAlpha(0, 0.25f, 1.5f, false));
|
||||
}
|
||||
}
|
@@ -0,0 +1,251 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
using FMODUnity;
|
||||
using NitroxClient.MonoBehaviours.Gui.MainMenu.ServersList;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using NitroxModel.DataStructures.Util;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.Gui.MainMenu.ServerJoin;
|
||||
|
||||
public class MainMenuEnterPasswordPanel : MonoBehaviour, uGUI_INavigableIconGrid, uGUI_IButtonReceiver
|
||||
{
|
||||
public const string NAME = "MultiplayerEnterPassword";
|
||||
|
||||
public static MainMenuEnterPasswordPanel Instance { get; private set; }
|
||||
|
||||
private TMP_InputField passwordInput;
|
||||
private mGUI_Change_Legend_On_Select legendChange;
|
||||
|
||||
private GameObject selectedItem;
|
||||
private GameObject[] selectableItems;
|
||||
|
||||
private static string lastEnteredPassword;
|
||||
public static Optional<string> LastEnteredPassword => lastEnteredPassword != null ? Optional.Of(lastEnteredPassword) : Optional.Empty;
|
||||
public static void ResetLastEnteredPassword() => lastEnteredPassword = null;
|
||||
|
||||
public void Setup(GameObject savedGamesRef)
|
||||
{
|
||||
Instance = this;
|
||||
|
||||
GameObject multiplayerButtonRef = savedGamesRef.RequireGameObject("Scroll View/Viewport/SavedGameAreaContent/NewGame");
|
||||
GameObject generalTextRef = multiplayerButtonRef.GetComponentInChildren<TextMeshProUGUI>().gameObject;
|
||||
GameObject inputFieldRef = GameObject.Find("/Menu canvas/Panel/MainMenu/RightSide/Home/EmailBox/InputField");
|
||||
|
||||
GameObject passwordInputGameObject = Instantiate(inputFieldRef, transform, false);
|
||||
passwordInputGameObject.transform.localPosition = new Vector3(-160, 300, 0);
|
||||
passwordInputGameObject.GetComponent<RectTransform>().sizeDelta = new Vector2(300, 40);
|
||||
passwordInput = passwordInputGameObject.GetComponent<TMP_InputField>();
|
||||
passwordInput.characterValidation = TMP_InputField.CharacterValidation.None;
|
||||
passwordInput.onSubmit = new TMP_InputField.SubmitEvent();
|
||||
passwordInput.onSubmit.AddListener(_ => OnConfirmButtonClicked());
|
||||
passwordInput.placeholder.GetComponent<TranslationLiveUpdate>().translationKey = Language.main.Get("Nitrox_JoinServerPlaceholder");
|
||||
GameObject passwordInputDesc = Instantiate(generalTextRef, passwordInputGameObject.transform, false);
|
||||
passwordInputDesc.transform.localPosition = new Vector3(-200, 0, 0);
|
||||
passwordInputDesc.GetComponent<TextMeshProUGUI>().text = Language.main.Get("Nitrox_JoinServerPassword");
|
||||
|
||||
GameObject confirmButton = Instantiate(multiplayerButtonRef, transform, false);
|
||||
confirmButton.transform.localPosition = new Vector3(-200, 90, 0);
|
||||
confirmButton.transform.GetChild(0).GetComponent<RectTransform>().sizeDelta = new Vector2(200, 40);
|
||||
confirmButton.GetComponentInChildren<TextMeshProUGUI>().text = Language.main.Get("Nitrox_Confirm");
|
||||
Button confirmButtonButton = confirmButton.RequireTransform("NewGameButton").GetComponent<Button>();
|
||||
confirmButtonButton.onClick = new Button.ButtonClickedEvent();
|
||||
confirmButtonButton.onClick.AddListener(OnConfirmButtonClicked);
|
||||
|
||||
GameObject backButton = Instantiate(multiplayerButtonRef, transform, false);
|
||||
backButton.transform.localPosition = new Vector3(-200, 40, 0);
|
||||
backButton.transform.GetChild(0).GetComponent<RectTransform>().sizeDelta = new Vector2(200, 40);
|
||||
backButton.GetComponentInChildren<TextMeshProUGUI>().text = Language.main.Get("Nitrox_Cancel");
|
||||
Button backButtonButton = backButton.RequireTransform("NewGameButton").GetComponent<Button>();
|
||||
backButtonButton.onClick = new Button.ButtonClickedEvent();
|
||||
backButtonButton.onClick.AddListener(OnCancelClick);
|
||||
|
||||
selectableItems = [passwordInputGameObject, confirmButton, backButton];
|
||||
Destroy(transform.Find("Scroll View").gameObject);
|
||||
|
||||
legendChange = gameObject.AddComponent<mGUI_Change_Legend_On_Select>();
|
||||
legendChange.legendButtonConfiguration = confirmButtonButton.GetComponent<mGUI_Change_Legend_On_Select>().legendButtonConfiguration.Take(1).ToArray();
|
||||
}
|
||||
|
||||
public void FocusPasswordField()
|
||||
{
|
||||
StartCoroutine(Coroutine());
|
||||
|
||||
IEnumerator Coroutine()
|
||||
{
|
||||
passwordInput.Select();
|
||||
EventSystem.current.SetSelectedGameObject(passwordInput.gameObject);
|
||||
yield return null;
|
||||
passwordInput.MoveToEndOfLine(false, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnConfirmButtonClicked()
|
||||
{
|
||||
lastEnteredPassword = passwordInput.text;
|
||||
MainMenuRightSide.main.OpenGroup(MainMenuJoinServerPanel.NAME);
|
||||
MainMenuJoinServerPanel.Instance.FocusNameInputField();
|
||||
}
|
||||
|
||||
private static void OnCancelClick()
|
||||
{
|
||||
JoinServerBackend.StopMultiplayerClient();
|
||||
MainMenuRightSide.main.OpenGroup(MainMenuServerListPanel.NAME);
|
||||
}
|
||||
|
||||
public bool OnButtonDown(GameInput.Button button)
|
||||
{
|
||||
switch (button)
|
||||
{
|
||||
case GameInput.Button.UISubmit:
|
||||
OnConfirm();
|
||||
return true;
|
||||
case GameInput.Button.UICancel:
|
||||
OnBack();
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnConfirm()
|
||||
{
|
||||
if (selectedItem.TryGetComponentInChildren(out TMP_InputField inputField))
|
||||
{
|
||||
inputField.ActivateInputField();
|
||||
}
|
||||
|
||||
if (selectedItem.TryGetComponentInChildren(out Button button))
|
||||
{
|
||||
button.onClick.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnBack()
|
||||
{
|
||||
passwordInput.text = string.Empty;
|
||||
ResetLastEnteredPassword();
|
||||
DeselectAllItems();
|
||||
MainMenuRightSide.main.OpenGroup(MainMenuServerListPanel.NAME);
|
||||
}
|
||||
|
||||
bool uGUI_INavigableIconGrid.ShowSelector => false;
|
||||
bool uGUI_INavigableIconGrid.EmulateRaycast => false;
|
||||
bool uGUI_INavigableIconGrid.SelectItemClosestToPosition(Vector3 worldPos) => false;
|
||||
uGUI_INavigableIconGrid uGUI_INavigableIconGrid.GetNavigableGridInDirection(int dirX, int dirY) => null;
|
||||
Graphic uGUI_INavigableIconGrid.GetSelectedIcon() => null;
|
||||
|
||||
object uGUI_INavigableIconGrid.GetSelectedItem() => selectedItem;
|
||||
|
||||
public void SelectItem(object item)
|
||||
{
|
||||
DeselectItem();
|
||||
selectedItem = item as GameObject;
|
||||
|
||||
legendChange.SyncLegendBarToGUISelection();
|
||||
|
||||
if (!selectedItem)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedItem.TryGetComponent(out TMP_InputField selectedInputField))
|
||||
{
|
||||
selectedInputField.Select();
|
||||
}
|
||||
else // Buttons
|
||||
{
|
||||
selectedItem.GetComponentInChildren<uGUI_BasicColorSwap>().makeTextBlack();
|
||||
selectedItem.transform.GetChild(0).GetComponent<Image>().sprite = MainMenuServerListPanel.SelectedSprite;
|
||||
}
|
||||
|
||||
if (!EventSystem.current.alreadySelecting)
|
||||
{
|
||||
EventSystem.current.SetSelectedGameObject(selectedItem);
|
||||
}
|
||||
|
||||
RuntimeManager.PlayOneShot(MainMenuServerListPanel.HoverSound.path);
|
||||
}
|
||||
|
||||
public void DeselectItem()
|
||||
{
|
||||
if (!selectedItem)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedItem.TryGetComponent(out TMP_InputField selectedInputField))
|
||||
{
|
||||
selectedInputField.DeactivateInputField();
|
||||
selectedInputField.ReleaseSelection();
|
||||
}
|
||||
else // Buttons
|
||||
{
|
||||
selectedItem.GetComponentInChildren<uGUI_BasicColorSwap>().makeTextWhite();
|
||||
selectedItem.transform.GetChild(0).GetComponent<Image>().sprite = MainMenuServerListPanel.NormalSprite;
|
||||
}
|
||||
|
||||
if (!EventSystem.current.alreadySelecting)
|
||||
{
|
||||
EventSystem.current.SetSelectedGameObject(null);
|
||||
}
|
||||
|
||||
selectedItem = null;
|
||||
}
|
||||
|
||||
public void DeselectAllItems()
|
||||
{
|
||||
foreach (GameObject child in selectableItems)
|
||||
{
|
||||
selectedItem = child;
|
||||
DeselectItem();
|
||||
}
|
||||
}
|
||||
|
||||
public bool SelectFirstItem()
|
||||
{
|
||||
SelectItem(selectableItems[0]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool SelectItemInDirection(int dirX, int dirY)
|
||||
{
|
||||
if (!selectedItem)
|
||||
{
|
||||
return SelectFirstItem();
|
||||
}
|
||||
|
||||
if (dirX == dirY)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int dir = dirX + dirY > 0 ? 1 : -1;
|
||||
for (int newIndex = GetSelectedIndex() + dir; newIndex >= 0 && newIndex < selectableItems.Length; newIndex += dir)
|
||||
{
|
||||
if (SelectItemByIndex(newIndex))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private int GetSelectedIndex() => selectedItem ? Array.IndexOf(selectableItems, selectedItem) : -1;
|
||||
|
||||
private bool SelectItemByIndex(int selectedIndex)
|
||||
{
|
||||
if (selectedIndex >= 0 && selectedIndex < selectableItems.Length)
|
||||
{
|
||||
SelectItem(selectableItems[selectedIndex]);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@@ -0,0 +1,363 @@
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using FMODUnity;
|
||||
using NitroxClient.MonoBehaviours.Gui.MainMenu.ServersList;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.ResourceManagement.AsyncOperations;
|
||||
using UnityEngine.UI;
|
||||
using UWE;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.Gui.MainMenu.ServerJoin;
|
||||
|
||||
public class MainMenuJoinServerPanel : MonoBehaviour, uGUI_INavigableIconGrid, uGUI_IButtonReceiver, uGUI_IScrollReceiver, uGUI_IAdjustReceiver
|
||||
{
|
||||
public const string NAME = "MultiplayerJoinServer";
|
||||
|
||||
public static MainMenuJoinServerPanel Instance { get; private set; }
|
||||
|
||||
private GameObject playerSettingsPanel;
|
||||
private TextMeshProUGUI header;
|
||||
private uGUI_ColorPicker colorPicker;
|
||||
private MainMenuColorPickerPreview colorPickerPreview;
|
||||
private Slider saturationSlider;
|
||||
private uGUI_InputField playerNameInputField;
|
||||
|
||||
private GameObject selectedItem;
|
||||
private GameObject[] selectableItems;
|
||||
|
||||
public void Setup(GameObject savedGamesRef)
|
||||
{
|
||||
Instance = this;
|
||||
Destroy(transform.RequireGameObject("Scroll View"));
|
||||
Destroy(GetComponentInChildren<TranslationLiveUpdate>());
|
||||
header = GetComponentInChildren<TextMeshProUGUI>();
|
||||
|
||||
CoroutineHost.StartCoroutine(AsyncSetup(savedGamesRef)); // As JoinServer waits for AsyncSetup to be completed we can't use normal Unity IEnumerator Start()
|
||||
}
|
||||
|
||||
private IEnumerator AsyncSetup(GameObject savedGamesRef)
|
||||
{
|
||||
AsyncOperationHandle<GameObject> request = AddressablesUtility.LoadAsync<GameObject>("Assets/Prefabs/Base/GeneratorPieces/BaseMoonpoolUpgradeConsole.prefab");
|
||||
yield return request;
|
||||
GameObject colorPickerPanelPrototype = request.Result.RequireGameObject("EditScreen/Active");
|
||||
|
||||
RectTransform parent = GetComponent<RectTransform>();
|
||||
|
||||
GameObject newGameButtonRef = savedGamesRef.RequireGameObject("Scroll View/Viewport/SavedGameAreaContent/NewGame/NewGameButton");
|
||||
LegendButtonData[] defaultLegend = newGameButtonRef.GetComponent<mGUI_Change_Legend_On_Select>().legendButtonConfiguration.Take(1).ToArray();
|
||||
|
||||
//Create a clone of the RocketBase color picker panel.
|
||||
playerSettingsPanel = Instantiate(colorPickerPanelPrototype, parent);
|
||||
|
||||
//Prepares name input field
|
||||
GameObject inputField = playerSettingsPanel.RequireGameObject("InputField");
|
||||
inputField.transform.SetParent(parent);
|
||||
inputField.transform.localPosition = new Vector3(-200, 310, 0);
|
||||
inputField.transform.localScale = Vector3.one;
|
||||
inputField.AddComponent<mGUI_Change_Legend_On_Select>().legendButtonConfiguration = defaultLegend;
|
||||
playerNameInputField = inputField.GetComponent<uGUI_InputField>();
|
||||
((TextMeshProUGUI)playerNameInputField.placeholder).text = Language.main.Get("Nitrox_EnterName");
|
||||
playerNameInputField.textComponent.fontSizeMin = 17;
|
||||
playerNameInputField.textComponent.fontSizeMax = 21;
|
||||
playerNameInputField.textComponent.GetComponent<RectTransform>().sizeDelta = new Vector2(-20, 42);
|
||||
playerNameInputField.characterLimit = 25; // See this.OnJoinClick()
|
||||
playerNameInputField.onFocusSelectAll = false;
|
||||
playerNameInputField.onSubmit.AddListener(_ => OnJoinClick());
|
||||
playerNameInputField.onSubmit.AddListener(_ => DeselectAllItems());
|
||||
playerNameInputField.ActivateInputField();
|
||||
|
||||
//Prepares player color picker
|
||||
GameObject colorPickerObject = playerSettingsPanel.RequireGameObject("ColorPicker");
|
||||
colorPickerObject.transform.SetParent(parent);
|
||||
colorPickerObject.transform.localPosition = new Vector3(-268, 175, 0);
|
||||
colorPickerObject.transform.localScale = new Vector3(1.1f, 0.75f, 1);
|
||||
colorPicker = colorPickerObject.GetComponentInChildren<uGUI_ColorPicker>();
|
||||
colorPicker.pointer.localScale = new Vector3(1f, 1.46f, 1);
|
||||
saturationSlider = colorPicker.saturationSlider;
|
||||
saturationSlider.transform.localPosition = new Vector3(197, 0, 0);
|
||||
colorPickerPreview = colorPicker.gameObject.AddComponent<MainMenuColorPickerPreview>();
|
||||
colorPickerPreview.Init(colorPicker);
|
||||
|
||||
GameObject buttonLeft = Instantiate(newGameButtonRef, parent);
|
||||
buttonLeft.GetComponent<RectTransform>().sizeDelta = new Vector2(160, 45);
|
||||
buttonLeft.GetComponent<mGUI_Change_Legend_On_Select>().legendButtonConfiguration = defaultLegend;
|
||||
GameObject buttonRight = Instantiate(buttonLeft, parent);
|
||||
|
||||
//Prepares cancel button
|
||||
buttonLeft.transform.SetParent(parent);
|
||||
buttonLeft.transform.localPosition = new Vector3(-285, 40, 0);
|
||||
buttonLeft.GetComponentInChildren<TextMeshProUGUI>().text = Language.main.Get("Nitrox_Cancel");
|
||||
Button cancelButton = buttonLeft.GetComponent<Button>();
|
||||
cancelButton.onClick = new Button.ButtonClickedEvent();
|
||||
cancelButton.onClick.AddListener(OnCancelClick);
|
||||
cancelButton.onClick.AddListener(DeselectAllItems);
|
||||
|
||||
//Prepares join button
|
||||
buttonRight.transform.localPosition = new Vector3(-115, 40, 0);
|
||||
buttonRight.GetComponentInChildren<TextMeshProUGUI>().text = Language.main.Get("Nitrox_Join");
|
||||
Button joinButton = buttonRight.GetComponent<Button>();
|
||||
joinButton.onClick = new Button.ButtonClickedEvent();
|
||||
joinButton.onClick.AddListener(OnJoinClick);
|
||||
joinButton.onClick.AddListener(DeselectAllItems);
|
||||
|
||||
selectableItems = [inputField, colorPicker.gameObject, saturationSlider.gameObject, buttonLeft, buttonRight];
|
||||
Destroy(playerSettingsPanel);
|
||||
}
|
||||
|
||||
private void OnJoinClick()
|
||||
{
|
||||
string playerName = playerNameInputField.text;
|
||||
|
||||
//https://regex101.com/r/eTWiEs/2/
|
||||
if (!Regex.IsMatch(playerName, "^[a-zA-Z0-9._-]{3,25}$"))
|
||||
{
|
||||
MainMenuNotificationPanel.ShowMessage(Language.main.Get("Nitrox_InvalidUserName"), NAME);
|
||||
return;
|
||||
}
|
||||
|
||||
JoinServerBackend.RequestSessionReservation(playerName, colorPicker.currentColor);
|
||||
}
|
||||
|
||||
private static void OnCancelClick()
|
||||
{
|
||||
JoinServerBackend.StopMultiplayerClient();
|
||||
MainMenuRightSide.main.OpenGroup(MainMenuServerListPanel.NAME);
|
||||
}
|
||||
|
||||
public void UpdatePanelValues(string serverName) => header.text = $" {Language.main.Get("Nitrox_JoinServer")} {serverName}";
|
||||
|
||||
public void UpdatePlayerPanelValues(string playerName, Vector3 hsb)
|
||||
{
|
||||
playerNameInputField.text = playerName;
|
||||
colorPicker.SetHSB(hsb);
|
||||
}
|
||||
|
||||
public void FocusNameInputField()
|
||||
{
|
||||
StartCoroutine(Coroutine());
|
||||
IEnumerator Coroutine()
|
||||
{
|
||||
SelectFirstItem();
|
||||
yield return new WaitForEndOfFrame();
|
||||
playerNameInputField.MoveToEndOfLine(false, true);
|
||||
}
|
||||
}
|
||||
|
||||
public bool OnButtonDown(GameInput.Button button)
|
||||
{
|
||||
if (button != GameInput.Button.UISubmit || !selectedItem)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selectedItem.TryGetComponentInChildren(out TMP_InputField inputField))
|
||||
{
|
||||
inputField.Select();
|
||||
inputField.ActivateInputField();
|
||||
}
|
||||
else if (selectedItem.TryGetComponentInChildren(out Button buttonComponent))
|
||||
{
|
||||
buttonComponent.onClick.Invoke();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool OnScroll(float scrollDelta, float speedMultiplier)
|
||||
{
|
||||
if (EventSystem.current != null &&
|
||||
EventSystem.current.currentSelectedGameObject == selectedItem &&
|
||||
selectedItem.TryGetComponent(out Slider slider))
|
||||
{
|
||||
slider.value += scrollDelta * speedMultiplier * 0.01f;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool OnAdjust(Vector2 adjustDelta)
|
||||
{
|
||||
if (selectedItem && selectedItem.TryGetComponent(out uGUI_ColorPicker selectedColorPicker))
|
||||
{
|
||||
return selectedColorPicker.OnAdjust(adjustDelta);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
object uGUI_INavigableIconGrid.GetSelectedItem() => selectedItem;
|
||||
|
||||
bool uGUI_INavigableIconGrid.ShowSelector => false;
|
||||
bool uGUI_INavigableIconGrid.EmulateRaycast => false;
|
||||
bool uGUI_INavigableIconGrid.SelectItemClosestToPosition(Vector3 worldPos) => false;
|
||||
uGUI_INavigableIconGrid uGUI_INavigableIconGrid.GetNavigableGridInDirection(int dirX, int dirY) => null;
|
||||
Graphic uGUI_INavigableIconGrid.GetSelectedIcon() => null;
|
||||
|
||||
public void SelectItem(object item)
|
||||
{
|
||||
DeselectItem();
|
||||
selectedItem = item as GameObject;
|
||||
|
||||
if (!selectedItem)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedItem.TryGetComponent(out mGUI_Change_Legend_On_Select changeLegend))
|
||||
{
|
||||
changeLegend.SyncLegendBarToGUISelection();
|
||||
}
|
||||
else
|
||||
{
|
||||
uGUI_LegendBar.ClearButtons();
|
||||
}
|
||||
|
||||
if (selectedItem == selectableItems[1])
|
||||
{
|
||||
colorPicker.pointer.GetComponent<Image>().color = Color.cyan;
|
||||
if (GameInput.GetPrimaryDevice() == GameInput.Device.Controller)
|
||||
{
|
||||
colorPickerPreview.OnPointerDown(null);
|
||||
}
|
||||
}
|
||||
else if (selectedItem == selectableItems[3] || selectedItem == selectableItems[4])
|
||||
{
|
||||
selectedItem.GetComponentInChildren<uGUI_BasicColorSwap>().makeTextBlack();
|
||||
}
|
||||
|
||||
if (selectedItem.TryGetComponentInChildren(out Selectable selectable))
|
||||
{
|
||||
selectable.Select();
|
||||
}
|
||||
|
||||
if (!EventSystem.current.alreadySelecting)
|
||||
{
|
||||
EventSystem.current.SetSelectedGameObject(selectedItem);
|
||||
}
|
||||
|
||||
RuntimeManager.PlayOneShot(MainMenuServerListPanel.HoverSound.path);
|
||||
}
|
||||
|
||||
public void DeselectItem()
|
||||
{
|
||||
if (!selectedItem)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedItem.TryGetComponent(out TMP_InputField selectedInputField))
|
||||
{
|
||||
//This line need to be before selectedInputField.ReleaseSelection() as it will call this method recursive leading to NRE
|
||||
selectedInputField.DeactivateInputField();
|
||||
selectedInputField.ReleaseSelection();
|
||||
}
|
||||
else if (selectedItem.TryGetComponent(out uGUI_ColorPicker selectedColorPicker))
|
||||
{
|
||||
Image colorPickerPointer = selectedColorPicker.pointer.GetComponent<Image>();
|
||||
|
||||
if (colorPickerPointer.color != Color.white &&
|
||||
GameInput.GetPrimaryDevice() == GameInput.Device.Controller)
|
||||
{
|
||||
colorPickerPreview.OnPointerUp(null);
|
||||
}
|
||||
colorPickerPointer.color = Color.white;
|
||||
}
|
||||
else if (selectedItem.TryGetComponentInChildren(out uGUI_BasicColorSwap colorSwap))
|
||||
{
|
||||
colorSwap.makeTextWhite();
|
||||
}
|
||||
|
||||
if (!EventSystem.current.alreadySelecting)
|
||||
{
|
||||
EventSystem.current.SetSelectedGameObject(null);
|
||||
}
|
||||
|
||||
selectedItem = null;
|
||||
}
|
||||
|
||||
public void DeselectAllItems()
|
||||
{
|
||||
foreach (GameObject item in selectableItems)
|
||||
{
|
||||
selectedItem = item;
|
||||
DeselectItem();
|
||||
}
|
||||
}
|
||||
|
||||
public bool SelectFirstItem()
|
||||
{
|
||||
SelectItem(selectableItems[0]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool SelectItemInDirection(int dirX, int dirY)
|
||||
{
|
||||
if (!selectedItem)
|
||||
{
|
||||
return SelectFirstItem();
|
||||
}
|
||||
|
||||
if (selectedItem == selectableItems[0]) //Name input
|
||||
{
|
||||
switch (dirY)
|
||||
{
|
||||
case < 0:
|
||||
SelectItem(selectableItems[^2]);
|
||||
return true;
|
||||
case > 0:
|
||||
SelectItem(selectableItems[1]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedItem == selectableItems[1] || selectedItem == selectableItems[2]) // ColorPicker and SaturationSlider
|
||||
{
|
||||
switch (dirY)
|
||||
{
|
||||
case < 0:
|
||||
SelectItem(selectableItems[0]);
|
||||
return true;
|
||||
case > 0:
|
||||
SelectItem(selectableItems[3]);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (dirX != 0)
|
||||
{
|
||||
int direction = selectedItem == selectableItems[1] ? 0 : 1;
|
||||
direction = (direction + dirX) % 2;
|
||||
|
||||
SelectItem(selectableItems[1 + direction]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedItem == selectableItems[3] || selectedItem == selectableItems[4]) // CancelButton and ConfirmButton
|
||||
{
|
||||
switch (dirY)
|
||||
{
|
||||
case < 0:
|
||||
SelectItem(selectableItems[1]);
|
||||
return true;
|
||||
case > 0:
|
||||
SelectItem(selectableItems[0]);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (dirX != 0)
|
||||
{
|
||||
int direction = selectedItem == selectableItems[3] ? 0 : 1;
|
||||
direction = (direction + dirX) % 2;
|
||||
|
||||
SelectItem(selectableItems[3 + direction]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@@ -0,0 +1,292 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
using FMODUnity;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using NitroxModel.Serialization;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.Gui.MainMenu.ServersList;
|
||||
|
||||
public class MainMenuCreateServerPanel : MonoBehaviour, uGUI_INavigableIconGrid, uGUI_IButtonReceiver
|
||||
{
|
||||
public const string NAME = "MultiplayerCreateServer";
|
||||
private const string DEFAULT_PORT = "11000";
|
||||
|
||||
private TMP_InputField serverNameInput, serverAddressInput, serverPortInput;
|
||||
private mGUI_Change_Legend_On_Select legendChange;
|
||||
|
||||
private GameObject selectedItem;
|
||||
private GameObject[] selectableItems;
|
||||
|
||||
public void Setup(GameObject savedGamesRef)
|
||||
{
|
||||
GameObject multiplayerButtonRef = savedGamesRef.RequireGameObject("Scroll View/Viewport/SavedGameAreaContent/NewGame");
|
||||
GameObject generalTextRef = multiplayerButtonRef.GetComponentInChildren<TextMeshProUGUI>().gameObject;
|
||||
|
||||
GameObject inputFieldRef = GameObject.Find("/Menu canvas/Panel/MainMenu/RightSide/Home/EmailBox/InputField");
|
||||
GameObject inputFieldBlueprint = Instantiate(inputFieldRef, transform, false);
|
||||
|
||||
inputFieldBlueprint.GetComponent<RectTransform>().sizeDelta = new Vector2(300, 40);
|
||||
TMP_InputField inputFieldBlueprintInput = inputFieldBlueprint.GetComponent<TMP_InputField>();
|
||||
inputFieldBlueprintInput.characterValidation = TMP_InputField.CharacterValidation.None;
|
||||
inputFieldBlueprintInput.onSubmit = new TMP_InputField.SubmitEvent();
|
||||
inputFieldBlueprintInput.onSubmit.AddListener(_ => { SelectItemInDirection(0, 1); });
|
||||
|
||||
GameObject serverName = Instantiate(inputFieldBlueprint, transform, false);
|
||||
serverName.transform.localPosition = new Vector3(-160, 300, 0);
|
||||
serverNameInput = serverName.GetComponent<TMP_InputField>();
|
||||
serverNameInput.placeholder.GetComponent<TranslationLiveUpdate>().translationKey = Language.main.Get("Nitrox_AddServer_NamePlaceholder");
|
||||
GameObject serverNameDesc = Instantiate(generalTextRef, serverName.transform, false);
|
||||
serverNameDesc.transform.localPosition = new Vector3(-200, 0, 0);
|
||||
serverNameDesc.GetComponent<TextMeshProUGUI>().text = Language.main.Get("Nitrox_AddServer_NameDescription");
|
||||
|
||||
GameObject serverAddress = Instantiate(inputFieldBlueprint, transform, false);
|
||||
serverAddress.transform.localPosition = new Vector3(-160, 225, 0);
|
||||
serverAddressInput = serverAddress.GetComponent<TMP_InputField>();
|
||||
serverAddressInput.placeholder.GetComponent<TranslationLiveUpdate>().translationKey = Language.main.Get("Nitrox_AddServer_AddressPlaceholder");
|
||||
GameObject serverAddressDesc = Instantiate(generalTextRef, serverAddress.transform, false);
|
||||
serverAddressDesc.transform.localPosition = new Vector3(-200, 0, 0);
|
||||
serverAddressDesc.GetComponent<TextMeshProUGUI>().text = Language.main.Get("Nitrox_AddServer_AddressDescription");
|
||||
|
||||
GameObject serverPort = Instantiate(inputFieldBlueprint, transform, false);
|
||||
serverPort.transform.localPosition = new Vector3(-160, 150, 0);
|
||||
serverPortInput = serverPort.GetComponent<TMP_InputField>();
|
||||
serverPortInput.characterValidation = TMP_InputField.CharacterValidation.Integer;
|
||||
serverPortInput.placeholder.GetComponent<TranslationLiveUpdate>().translationKey = Language.main.Get("Nitrox_AddServer_PortPlaceholder");
|
||||
serverPortInput.text = DEFAULT_PORT;
|
||||
GameObject serverPortDesc = Instantiate(generalTextRef, serverPort.transform, false);
|
||||
serverPortDesc.transform.localPosition = new Vector3(-200, 0, 0);
|
||||
serverPortDesc.GetComponent<TextMeshProUGUI>().text = Language.main.Get("Nitrox_AddServer_PortDescription");
|
||||
|
||||
GameObject confirmButton = Instantiate(multiplayerButtonRef, transform, false);
|
||||
confirmButton.transform.localPosition = new Vector3(-200, 90, 0);
|
||||
confirmButton.transform.GetChild(0).GetComponent<RectTransform>().sizeDelta = new Vector2(200, 40);
|
||||
confirmButton.GetComponentInChildren<TextMeshProUGUI>().text = Language.main.Get("Nitrox_AddServer_Confirm");
|
||||
Button confirmButtonButton = confirmButton.RequireTransform("NewGameButton").GetComponent<Button>();
|
||||
confirmButtonButton.onClick = new Button.ButtonClickedEvent();
|
||||
confirmButtonButton.onClick.AddListener(SaveServer);
|
||||
|
||||
GameObject backButton = Instantiate(multiplayerButtonRef, transform, false);
|
||||
backButton.transform.localPosition = new Vector3(-200, 40, 0);
|
||||
backButton.transform.GetChild(0).GetComponent<RectTransform>().sizeDelta = new Vector2(200, 40);
|
||||
backButton.GetComponentInChildren<TextMeshProUGUI>().text = Language.main.Get("Nitrox_Cancel");
|
||||
Button backButtonButton = backButton.RequireTransform("NewGameButton").GetComponent<Button>();
|
||||
backButtonButton.onClick = new Button.ButtonClickedEvent();
|
||||
backButtonButton.onClick.AddListener(OnBack);
|
||||
|
||||
selectableItems = [serverName, serverAddress, serverPort, confirmButton, backButton];
|
||||
Destroy(inputFieldBlueprint);
|
||||
Destroy(transform.Find("Scroll View").gameObject);
|
||||
|
||||
legendChange = gameObject.AddComponent<mGUI_Change_Legend_On_Select>();
|
||||
legendChange.legendButtonConfiguration = confirmButtonButton.GetComponent<mGUI_Change_Legend_On_Select>().legendButtonConfiguration.Take(2).ToArray();
|
||||
}
|
||||
|
||||
private void SaveServer()
|
||||
{
|
||||
string serverNameText = serverNameInput.text.Trim();
|
||||
string serverHostText = serverAddressInput.text.Trim();
|
||||
string serverPortText = serverPortInput.text.Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(serverNameText) ||
|
||||
string.IsNullOrWhiteSpace(serverHostText) ||
|
||||
string.IsNullOrWhiteSpace(serverPortText) ||
|
||||
!int.TryParse(serverPortText, out int serverPort))
|
||||
{
|
||||
Log.InGame(Language.main.Get("Nitrox_AddServer_InvalidInput"));
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject newEntry = MainMenuServerListPanel.Main.CreateServerButton(serverNameText, serverHostText, serverPort);
|
||||
ServerList.Instance.Add(new ServerList.Entry(serverNameText, serverHostText, serverPort));
|
||||
ServerList.Instance.Save();
|
||||
OnBack();
|
||||
MainMenuServerListPanel.Main.StartCoroutine(DelayedScrollToNewEntry());
|
||||
Log.InGame(Language.main.Get("Nitrox_AddServer_CreatedSuccessful"));
|
||||
return;
|
||||
|
||||
IEnumerator DelayedScrollToNewEntry()
|
||||
{
|
||||
yield return new WaitForEndOfFrame();
|
||||
UIUtils.ScrollToShowItemInCenter(newEntry.transform);
|
||||
}
|
||||
}
|
||||
|
||||
public bool OnButtonDown(GameInput.Button button)
|
||||
{
|
||||
switch (button)
|
||||
{
|
||||
case GameInput.Button.UISubmit:
|
||||
OnConfirm();
|
||||
return true;
|
||||
case GameInput.Button.UICancel:
|
||||
OnBack();
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (GameInput.GetKeyDown(KeyCode.Tab))
|
||||
{
|
||||
if (GameInput.GetKey(KeyCode.LeftShift))
|
||||
{
|
||||
SelectItemInDirection(-1, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectItemInDirection(1, 0);
|
||||
}
|
||||
}
|
||||
else if (selectedItem && GameInput.GetKeyDown(KeyCode.Return))
|
||||
{
|
||||
OnConfirm();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnBack()
|
||||
{
|
||||
serverNameInput.text = string.Empty;
|
||||
serverAddressInput.text = string.Empty;
|
||||
serverPortInput.text = string.Empty;
|
||||
DeselectAllItems();
|
||||
MainMenuRightSide.main.OpenGroup(MainMenuServerListPanel.NAME);
|
||||
}
|
||||
|
||||
public void OnConfirm()
|
||||
{
|
||||
if (selectedItem.TryGetComponentInChildren(out TMP_InputField inputField))
|
||||
{
|
||||
inputField.ActivateInputField();
|
||||
}
|
||||
|
||||
if (selectedItem.TryGetComponentInChildren(out Button button))
|
||||
{
|
||||
button.onClick.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
object uGUI_INavigableIconGrid.GetSelectedItem() => selectedItem;
|
||||
|
||||
bool uGUI_INavigableIconGrid.ShowSelector => false;
|
||||
|
||||
bool uGUI_INavigableIconGrid.EmulateRaycast => false;
|
||||
bool uGUI_INavigableIconGrid.SelectItemClosestToPosition(Vector3 worldPos) => false;
|
||||
uGUI_INavigableIconGrid uGUI_INavigableIconGrid.GetNavigableGridInDirection(int dirX, int dirY) => null;
|
||||
|
||||
Graphic uGUI_INavigableIconGrid.GetSelectedIcon() => null;
|
||||
|
||||
public void SelectItem(object item)
|
||||
{
|
||||
DeselectItem();
|
||||
selectedItem = item as GameObject;
|
||||
|
||||
legendChange.SyncLegendBarToGUISelection();
|
||||
|
||||
if (!selectedItem)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedItem.TryGetComponent(out TMP_InputField selectedInputField))
|
||||
{
|
||||
selectedInputField.Select();
|
||||
}
|
||||
else // Button
|
||||
{
|
||||
selectedItem.transform.GetChild(0).GetComponent<Image>().sprite = MainMenuServerListPanel.SelectedSprite;
|
||||
selectedItem.GetComponentInChildren<uGUI_BasicColorSwap>().makeTextBlack();
|
||||
}
|
||||
|
||||
if (!EventSystem.current.alreadySelecting)
|
||||
{
|
||||
EventSystem.current.SetSelectedGameObject(selectedItem);
|
||||
}
|
||||
|
||||
RuntimeManager.PlayOneShot(MainMenuServerListPanel.HoverSound.path);
|
||||
}
|
||||
|
||||
public void DeselectItem()
|
||||
{
|
||||
if (!selectedItem)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedItem.TryGetComponent(out TMP_InputField selectedInputField))
|
||||
{
|
||||
selectedInputField.DeactivateInputField();
|
||||
selectedInputField.ReleaseSelection();
|
||||
}
|
||||
else // Button
|
||||
{
|
||||
selectedItem.transform.GetChild(0).GetComponent<Image>().sprite = MainMenuServerListPanel.NormalSprite;
|
||||
selectedItem.transform.GetChild(0).GetComponent<uGUI_BasicColorSwap>().makeTextWhite();
|
||||
}
|
||||
|
||||
if (!EventSystem.current.alreadySelecting)
|
||||
{
|
||||
EventSystem.current.SetSelectedGameObject(null);
|
||||
}
|
||||
|
||||
selectedItem = null;
|
||||
}
|
||||
|
||||
public void DeselectAllItems()
|
||||
{
|
||||
foreach (GameObject child in selectableItems)
|
||||
{
|
||||
selectedItem = child;
|
||||
DeselectItem();
|
||||
}
|
||||
}
|
||||
|
||||
public bool SelectFirstItem()
|
||||
{
|
||||
SelectItem(selectableItems[0]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool SelectItemInDirection(int dirX, int dirY)
|
||||
{
|
||||
if (!selectedItem)
|
||||
{
|
||||
return SelectFirstItem();
|
||||
}
|
||||
|
||||
if (dirX == dirY)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int dir = dirX + dirY > 0 ? 1 : -1;
|
||||
for (int newIndex = GetSelectedIndex() + dir; newIndex >= 0 && newIndex < selectableItems.Length; newIndex += dir)
|
||||
{
|
||||
if (SelectItemByIndex(newIndex))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private int GetSelectedIndex() => selectedItem ? Array.IndexOf(selectableItems, selectedItem) : -1;
|
||||
|
||||
private bool SelectItemByIndex(int selectedIndex)
|
||||
{
|
||||
if (selectedIndex >= 0 && selectedIndex < selectableItems.Length)
|
||||
{
|
||||
SelectItem(selectableItems[selectedIndex]);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
namespace NitroxClient.MonoBehaviours.Gui.MainMenu.ServersList;
|
||||
|
||||
public class MainMenuDeleteServer : uGUI_NavigableControlGrid, uGUI_IButtonReceiver
|
||||
{
|
||||
public MainMenuServerButton serverButton;
|
||||
|
||||
private void Start() => interGridNavigation = new uGUI_InterGridNavigation();
|
||||
|
||||
public bool OnButtonDown(GameInput.Button button)
|
||||
{
|
||||
if (button != GameInput.Button.UICancel)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
OnBack();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void OnBack() => serverButton.CancelDelete();
|
||||
}
|
@@ -0,0 +1,220 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using NitroxClient.GameLogic.Settings;
|
||||
using NitroxClient.MonoBehaviours.Gui.MainMenu.ServerJoin;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using NitroxModel.Serialization;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.Gui.MainMenu.ServersList;
|
||||
|
||||
public class MainMenuServerButton : MonoBehaviour
|
||||
{
|
||||
private static MainMenuLoadButton loadButtonRef;
|
||||
private static LegendButtonData[] confirmButtonLegendData;
|
||||
private static GameObject deleteButtonRef;
|
||||
|
||||
private CanvasGroup loadCg;
|
||||
private CanvasGroup deleteCg;
|
||||
private Button cancelDeleteButton;
|
||||
|
||||
private string joinIp;
|
||||
private int joinPort;
|
||||
private string joinServerName;
|
||||
|
||||
public static void Setup(MainMenuLoadButton _loadButtonRef)
|
||||
{
|
||||
loadButtonRef = _loadButtonRef;
|
||||
confirmButtonLegendData = _loadButtonRef.GetComponent<mGUI_Change_Legend_On_Select>().legendButtonConfiguration;
|
||||
deleteButtonRef = _loadButtonRef.deleteButton;
|
||||
}
|
||||
|
||||
public void Init(string serverName, string ip, int port, bool isReadOnly)
|
||||
{
|
||||
joinIp = ip;
|
||||
joinPort = port;
|
||||
joinServerName = serverName;
|
||||
|
||||
Transform loadTransform = this.RequireTransform("Load");
|
||||
loadCg = loadTransform.gameObject.AddComponent<CanvasGroup>();
|
||||
Transform newGameButtonTransform = loadTransform.RequireTransform("NewGameButton");
|
||||
|
||||
TextMeshProUGUI tmp = newGameButtonTransform.RequireTransform("Text").GetComponent<TextMeshProUGUI>();
|
||||
Destroy(tmp.GetComponent<TranslationLiveUpdate>());
|
||||
StringBuilder buttonText = new(Language.main.Get("Nitrox_ConnectTo"));
|
||||
buttonText.Append(" <b>").Append(serverName).AppendLine("</b>");
|
||||
if (NitroxPrefs.HideIp.Value)
|
||||
{
|
||||
buttonText.AppendLine("***.***.***.***:*****");
|
||||
}
|
||||
else
|
||||
{
|
||||
buttonText.Append(ip[^Math.Min(ip.Length, 25)..]).Append(':').Append(port);
|
||||
}
|
||||
tmp.text = buttonText.ToString();
|
||||
|
||||
Button multiplayerJoinButton = newGameButtonTransform.GetComponent<Button>();
|
||||
multiplayerJoinButton.onClick = new Button.ButtonClickedEvent();
|
||||
multiplayerJoinButton.onClick.AddListener(() => _ = OnJoinButtonClicked());
|
||||
|
||||
gameObject.GetComponent<mGUI_Change_Legend_On_Select>().legendButtonConfiguration = confirmButtonLegendData;
|
||||
|
||||
// We don't want servers that are discovered automatically to be deleted
|
||||
if (isReadOnly)
|
||||
{
|
||||
Destroy(transform.Find("Delete").gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject delete = Instantiate(deleteButtonRef, loadTransform, false);
|
||||
Button deleteButtonButton = delete.GetComponent<Button>();
|
||||
deleteButtonButton.onClick = new Button.ButtonClickedEvent();
|
||||
deleteButtonButton.onClick.AddListener(RequestDelete);
|
||||
|
||||
Transform deleteTransform = this.RequireTransform("Delete");
|
||||
Destroy(deleteTransform.GetComponent<MainMenuDeleteGame>());
|
||||
Destroy(deleteTransform.GetComponent<TranslationLiveUpdate>());
|
||||
deleteCg = deleteTransform.GetComponent<CanvasGroup>();
|
||||
cancelDeleteButton = deleteTransform.RequireTransform("DeleteCancelButton").GetComponent<Button>();
|
||||
cancelDeleteButton.onClick = new Button.ButtonClickedEvent();
|
||||
cancelDeleteButton.onClick.AddListener(CancelDelete);
|
||||
Button confirmDeleteButton = deleteTransform.RequireTransform("DeleteConfirmButton").GetComponent<Button>();
|
||||
confirmDeleteButton.onClick = new Button.ButtonClickedEvent();
|
||||
confirmDeleteButton.onClick.AddListener(Delete);
|
||||
|
||||
deleteTransform.gameObject.AddComponent<MainMenuDeleteServer>().serverButton = this;
|
||||
TextMeshProUGUI warningTmp = deleteTransform.RequireTransform("DeleteWarningText").GetComponent<TextMeshProUGUI>();
|
||||
warningTmp.text = Language.main.Get("Nitrox_ServerEntry_DeleteWarning");
|
||||
}
|
||||
|
||||
public void RequestDelete()
|
||||
{
|
||||
uGUI_MainMenu.main.OnRightSideOpened(deleteCg.gameObject);
|
||||
uGUI_LegendBar.ClearButtons();
|
||||
uGUI_LegendBar.ChangeButton(0, uGUI.FormatButton(GameInput.Button.UICancel, gamePadOnly: true), Language.main.GetFormat("Back"));
|
||||
uGUI_LegendBar.ChangeButton(1, uGUI.FormatButton(GameInput.Button.UISubmit, gamePadOnly: true), Language.main.GetFormat("ItemSelectorSelect"));
|
||||
StartCoroutine(loadButtonRef.ShiftAlpha(loadCg, 0.0f, loadButtonRef.animTime, loadButtonRef.alphaPower, false));
|
||||
StartCoroutine(loadButtonRef.ShiftAlpha(deleteCg, 1f, loadButtonRef.animTime, loadButtonRef.alphaPower, true, cancelDeleteButton));
|
||||
StartCoroutine(loadButtonRef.ShiftPos(loadCg, MainMenuLoadButton.target.left, MainMenuLoadButton.target.centre, loadButtonRef.animTime, loadButtonRef.posPower));
|
||||
StartCoroutine(loadButtonRef.ShiftPos(deleteCg, MainMenuLoadButton.target.centre, MainMenuLoadButton.target.right, loadButtonRef.animTime, loadButtonRef.posPower));
|
||||
}
|
||||
|
||||
public void CancelDelete()
|
||||
{
|
||||
MainMenuRightSide.main.OpenGroup(MainMenuServerListPanel.NAME);
|
||||
if (GameInput.IsPrimaryDeviceGamepad())
|
||||
MainMenuServerListPanel.Main.SelectItemByIndex(MainMenuServerListPanel.Main.GetSelectedIndex());
|
||||
StartCoroutine(loadButtonRef.ShiftAlpha(loadCg, 1f, loadButtonRef.animTime, loadButtonRef.alphaPower, true));
|
||||
StartCoroutine(loadButtonRef.ShiftAlpha(deleteCg, 0.0f, loadButtonRef.animTime, loadButtonRef.alphaPower, false));
|
||||
StartCoroutine(loadButtonRef.ShiftPos(loadCg, MainMenuLoadButton.target.centre, MainMenuLoadButton.target.left, loadButtonRef.animTime, loadButtonRef.posPower));
|
||||
StartCoroutine(loadButtonRef.ShiftPos(deleteCg, MainMenuLoadButton.target.right, MainMenuLoadButton.target.centre, loadButtonRef.animTime, loadButtonRef.posPower));
|
||||
}
|
||||
|
||||
public void ResetLoadDeleteView()
|
||||
{
|
||||
loadCg.alpha = 1;
|
||||
loadCg.interactable = loadCg.blocksRaycasts = true;
|
||||
|
||||
RectTransform loadTransform = loadCg.GetComponent<RectTransform>();
|
||||
float loadPosX = loadTransform.sizeDelta.x * 0.5f;
|
||||
loadTransform.localPosition = new Vector3(loadPosX, loadTransform.localPosition.y, 0);
|
||||
|
||||
if (deleteCg) // Read only server entries
|
||||
{
|
||||
RectTransform deleteTransform = deleteCg.GetComponent<RectTransform>();
|
||||
float deletePosX = deleteTransform.sizeDelta.x * 0.5f;
|
||||
deleteTransform.localPosition = new Vector3(deletePosX, deleteTransform.localPosition.y, 0);
|
||||
|
||||
deleteCg.alpha = 0;
|
||||
deleteCg.interactable = deleteCg.blocksRaycasts = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Delete()
|
||||
{
|
||||
MainMenuRightSide.main.OpenGroup(MainMenuServerListPanel.NAME);
|
||||
int scrollIndex = MainMenuServerListPanel.Main.GetSelectedIndex();
|
||||
if (GameInput.IsPrimaryDeviceGamepad() && !MainMenuServerListPanel.Main.SelectItemInYDirection(scrollIndex, 1))
|
||||
{
|
||||
MainMenuServerListPanel.Main.SelectItemInYDirection(scrollIndex, -1);
|
||||
}
|
||||
StartCoroutine(loadButtonRef.ShiftPos(deleteCg, MainMenuLoadButton.target.left, MainMenuLoadButton.target.centre, loadButtonRef.animTime, loadButtonRef.posPower));
|
||||
StartCoroutine(loadButtonRef.ShiftAlpha(deleteCg, 0.0f, loadButtonRef.animTime, loadButtonRef.alphaPower, false));
|
||||
|
||||
ServerList.Instance.RemoveAt(transform.GetSiblingIndex() - 1);
|
||||
ServerList.Instance.Save();
|
||||
|
||||
Destroy(gameObject);
|
||||
}
|
||||
|
||||
public async Task OnJoinButtonClicked()
|
||||
{
|
||||
if (MainMenuServerListPanel.Main.IsJoining)
|
||||
{
|
||||
return; // Do not attempt to join multiple servers.
|
||||
}
|
||||
|
||||
MainMenuServerListPanel.Main.IsJoining = true;
|
||||
MainMenuServerListPanel.Main.DeselectAllItems();
|
||||
await OpenJoinServerMenuAsync(joinIp, joinPort).ContinueWith(_ => { MainMenuServerListPanel.Main.IsJoining = false; });
|
||||
MainMenuJoinServerPanel.Instance.UpdatePanelValues(joinServerName);
|
||||
}
|
||||
|
||||
public static async Task OpenJoinServerMenuAsync(string serverIp, int serverPort)
|
||||
{
|
||||
if (!MainMenuServerListPanel.Main)
|
||||
{
|
||||
Log.Error("MainMenuServerListPanel is not instantiated although OpenJoinServerMenuAsync is called.");
|
||||
return;
|
||||
}
|
||||
|
||||
IPEndPoint endpoint = ResolveIPEndPoint(serverIp, serverPort);
|
||||
if (endpoint == null)
|
||||
{
|
||||
Log.InGame($"{Language.main.Get("Nitrox_UnableToConnect")}: {serverIp}:{serverPort}");
|
||||
return;
|
||||
}
|
||||
|
||||
MainMenuNotificationPanel.ShowLoading();
|
||||
await JoinServerBackend.StartMultiplayerClientAsync(endpoint.Address, endpoint.Port);
|
||||
}
|
||||
|
||||
private static IPEndPoint ResolveIPEndPoint(string serverIp, int serverPort)
|
||||
{
|
||||
UriHostNameType hostType = Uri.CheckHostName(serverIp);
|
||||
IPAddress address;
|
||||
switch (hostType)
|
||||
{
|
||||
case UriHostNameType.IPv4:
|
||||
case UriHostNameType.IPv6:
|
||||
IPAddress.TryParse(serverIp, out address);
|
||||
break;
|
||||
case UriHostNameType.Dns:
|
||||
address = ResolveHostName(serverIp, serverPort);
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
return address != null ? new IPEndPoint(address, serverPort) : null;
|
||||
|
||||
static IPAddress ResolveHostName(string hostname, int serverPort)
|
||||
{
|
||||
try
|
||||
{
|
||||
IPHostEntry hostEntry = Dns.GetHostEntry(hostname);
|
||||
return hostEntry.AddressList[0];
|
||||
}
|
||||
catch (SocketException ex)
|
||||
{
|
||||
Log.ErrorSensitive(ex, "Unable to resolve the address {hostname}:{serverPort}", hostname, serverPort);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,355 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using FMODUnity;
|
||||
using NitroxClient.Communication;
|
||||
using NitroxClient.GameLogic.Settings;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using NitroxModel;
|
||||
using NitroxModel.Serialization;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
using UWE;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.Gui.MainMenu.ServersList;
|
||||
|
||||
public class MainMenuServerListPanel : MonoBehaviour, uGUI_INavigableIconGrid, uGUI_IButtonReceiver
|
||||
{
|
||||
public const string NAME = "MultiplayerServerList";
|
||||
|
||||
public static MainMenuServerListPanel Main;
|
||||
public static Sprite NormalSprite;
|
||||
public static Sprite SelectedSprite;
|
||||
public static FMODAsset HoverSound;
|
||||
|
||||
private GameObject multiplayerNewServerButtonRef;
|
||||
private GameObject multiplayerServerButtonRef;
|
||||
private Transform serverAreaContent;
|
||||
private GameObject selectedServerItem;
|
||||
private ScrollRect scrollRect;
|
||||
private GameObject scrollBar;
|
||||
|
||||
public bool IsJoining { get; set; }
|
||||
|
||||
public void Setup(GameObject savedGamesRef)
|
||||
{
|
||||
Main = this;
|
||||
|
||||
MainMenuLoadMenu loadMenu = savedGamesRef.GetComponentInChildren<MainMenuLoadMenu>();
|
||||
NormalSprite = loadMenu.normalSprite;
|
||||
SelectedSprite = loadMenu.selectedSprite;
|
||||
HoverSound = loadMenu.hoverSound;
|
||||
|
||||
multiplayerNewServerButtonRef = savedGamesRef.RequireGameObject("Scroll View/Viewport/SavedGameAreaContent/NewGame");
|
||||
serverAreaContent = transform.RequireTransform("Scroll View/Viewport/SavedGameAreaContent");
|
||||
serverAreaContent.gameObject.name = "ServerAreaContent";
|
||||
serverAreaContent.GetComponent<GridLayoutGroup>().spacing = new Vector2(0, 5);
|
||||
|
||||
scrollRect = transform.RequireGameObject("Scroll View").GetComponent<ScrollRect>();
|
||||
scrollBar = scrollRect.RequireGameObject("Scrollbar");
|
||||
|
||||
multiplayerServerButtonRef = savedGamesRef.GetComponent<MainMenuLoadPanel>().saveInstance;
|
||||
MainMenuServerButton.Setup(multiplayerServerButtonRef.GetComponent<MainMenuLoadButton>());
|
||||
|
||||
RefreshServerEntries();
|
||||
}
|
||||
|
||||
public bool OnButtonDown(GameInput.Button button)
|
||||
{
|
||||
switch (button)
|
||||
{
|
||||
case GameInput.Button.UISubmit:
|
||||
OnConfirm();
|
||||
return true;
|
||||
case GameInput.Button.UICancel:
|
||||
OnBack();
|
||||
return true;
|
||||
case GameInput.Button.UIClear:
|
||||
OnClear();
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnBack()
|
||||
{
|
||||
DeselectAllItems();
|
||||
MainMenuRightSide.main.OpenGroup("Home");
|
||||
}
|
||||
|
||||
public void OnClear()
|
||||
{
|
||||
if (selectedServerItem && selectedServerItem.TryGetComponent(out MainMenuServerButton serverButton))
|
||||
{
|
||||
serverButton.RequestDelete();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnConfirm()
|
||||
{
|
||||
if (!selectedServerItem)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedServerItem.gameObject.name == "NewServer")
|
||||
{
|
||||
DeselectAllItems();
|
||||
MainMenuRightSide.main.OpenGroup(MainMenuCreateServerPanel.NAME);
|
||||
}
|
||||
else if (selectedServerItem.TryGetComponent(out MainMenuServerButton serverButton))
|
||||
{
|
||||
_ = serverButton.OnJoinButtonClicked().ContinueWithHandleError(Log.Error);
|
||||
}
|
||||
}
|
||||
|
||||
object uGUI_INavigableIconGrid.GetSelectedItem() => selectedServerItem;
|
||||
|
||||
bool uGUI_INavigableIconGrid.ShowSelector => false;
|
||||
bool uGUI_INavigableIconGrid.EmulateRaycast => false;
|
||||
bool uGUI_INavigableIconGrid.SelectItemClosestToPosition(Vector3 worldPos) => false;
|
||||
uGUI_INavigableIconGrid uGUI_INavigableIconGrid.GetNavigableGridInDirection(int dirX, int dirY) => null;
|
||||
|
||||
Graphic uGUI_INavigableIconGrid.GetSelectedIcon() => null;
|
||||
|
||||
public void SelectItem(object item)
|
||||
{
|
||||
DeselectItem();
|
||||
selectedServerItem = item as GameObject;
|
||||
|
||||
if (!selectedServerItem)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedServerItem.TryGetComponentInChildren(out mGUI_Change_Legend_On_Select componentInChildren))
|
||||
{
|
||||
componentInChildren.SyncLegendBarToGUISelection();
|
||||
}
|
||||
|
||||
if (selectedServerItem == serverAreaContent.GetChild(0).gameObject) // Server Create Button
|
||||
{
|
||||
selectedServerItem.transform.Find("NewGameButton").GetComponent<Image>().sprite = SelectedSprite;
|
||||
}
|
||||
else
|
||||
{
|
||||
selectedServerItem.transform.Find("Load/NewGameButton").GetComponent<Image>().sprite = SelectedSprite;
|
||||
}
|
||||
|
||||
selectedServerItem.GetComponentInChildren<uGUI_BasicColorSwap>();
|
||||
|
||||
UIUtils.ScrollToShowItemInCenter(selectedServerItem.transform);
|
||||
RuntimeManager.PlayOneShot(HoverSound.path);
|
||||
}
|
||||
|
||||
public void DeselectItem()
|
||||
{
|
||||
if (!selectedServerItem)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedServerItem == serverAreaContent.GetChild(0).gameObject) // Server Create Button
|
||||
{
|
||||
selectedServerItem.transform.Find("NewGameButton").GetComponent<Image>().sprite = NormalSprite;
|
||||
}
|
||||
else
|
||||
{
|
||||
selectedServerItem.transform.Find("Load/NewGameButton").GetComponent<Image>().sprite = NormalSprite;
|
||||
}
|
||||
|
||||
selectedServerItem.GetComponentInChildren<uGUI_BasicColorSwap>().makeTextWhite();
|
||||
selectedServerItem = null;
|
||||
}
|
||||
|
||||
public void DeselectAllItems()
|
||||
{
|
||||
// Create ServerEntry button
|
||||
serverAreaContent.GetChild(0).Find("NewGameButton").GetComponent<Image>().sprite = NormalSprite;
|
||||
serverAreaContent.GetChild(0).GetComponentInChildren<uGUI_BasicColorSwap>().makeTextWhite();
|
||||
|
||||
// Server buttons
|
||||
for (int i = 1; i < serverAreaContent.childCount; i++)
|
||||
{
|
||||
Transform child = serverAreaContent.GetChild(i);
|
||||
child.Find("Load/NewGameButton").GetComponent<Image>().sprite = NormalSprite;
|
||||
child.GetComponentInChildren<uGUI_BasicColorSwap>().makeTextWhite();
|
||||
child.GetComponent<MainMenuServerButton>().ResetLoadDeleteView();
|
||||
}
|
||||
}
|
||||
|
||||
public bool SelectFirstItem()
|
||||
{
|
||||
MainMenuServerButton firstServerObject = serverAreaContent.GetComponentInChildren<MainMenuServerButton>();
|
||||
if (firstServerObject)
|
||||
{
|
||||
SelectItem(firstServerObject.gameObject);
|
||||
return true;
|
||||
}
|
||||
|
||||
Transform serverCreationButton = serverAreaContent.GetChild(0);
|
||||
if (serverCreationButton && serverCreationButton.name == "NewServer")
|
||||
{
|
||||
SelectItem(serverCreationButton.gameObject);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool SelectItemInDirection(int dirX, int dirY)
|
||||
{
|
||||
if (selectedServerItem)
|
||||
{
|
||||
return dirY != 0 && SelectItemInYDirection(GetSelectedIndex(), dirY);
|
||||
}
|
||||
|
||||
return SelectFirstItem();
|
||||
}
|
||||
|
||||
public int GetSelectedIndex() => selectedServerItem ? selectedServerItem.transform.GetSiblingIndex() : -1;
|
||||
|
||||
public bool SelectItemInYDirection(int selectedIndex, int dirY)
|
||||
{
|
||||
int dir = dirY > 0 ? 1 : -1;
|
||||
for (int newIndex = selectedIndex + dir; newIndex >= 0 && newIndex < serverAreaContent.childCount; newIndex += dir)
|
||||
{
|
||||
if (SelectItemByIndex(newIndex))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool SelectItemByIndex(int selectedIndex)
|
||||
{
|
||||
if (selectedIndex < serverAreaContent.childCount && selectedIndex >= 0)
|
||||
{
|
||||
SelectItem(serverAreaContent.GetChild(selectedIndex).gameObject);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void LoadSavedServers()
|
||||
{
|
||||
ServerList.Refresh();
|
||||
foreach (ServerList.Entry entry in ServerList.Instance.Entries)
|
||||
{
|
||||
CreateServerButton(entry.Name, entry.Address, entry.Port);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator FindLANServers()
|
||||
{
|
||||
void LateAddButton(IPEndPoint serverEndPoint)
|
||||
{
|
||||
if (!ServerList.Instance.Entries.Any(e => e.Address == serverEndPoint.Address.ToString() && e.Port == serverEndPoint.Port))
|
||||
{
|
||||
Log.Info($"Adding LAN server: {serverEndPoint}");
|
||||
// Add ServerList entry to keep indices in sync with servers UI, to enable removal by index
|
||||
ServerList.Instance.Add(new ServerList.Entry("LAN Server", serverEndPoint.Address, serverEndPoint.Port, false));
|
||||
CreateServerButton("LAN Server", serverEndPoint.Address.ToString(), serverEndPoint.Port, true);
|
||||
}
|
||||
}
|
||||
|
||||
using Task<IEnumerable<IPEndPoint>> searchTask = LANBroadcastClient.SearchAsync();
|
||||
while (!searchTask.IsCompleted)
|
||||
{
|
||||
while (LANBroadcastClient.DiscoveredServers.TryDequeue(out IPEndPoint endPoint))
|
||||
{
|
||||
LateAddButton(endPoint);
|
||||
}
|
||||
yield return null;
|
||||
}
|
||||
while (LANBroadcastClient.DiscoveredServers.TryDequeue(out IPEndPoint endPoint))
|
||||
{
|
||||
LateAddButton(endPoint);
|
||||
}
|
||||
ServerList.Instance.Save();
|
||||
}
|
||||
|
||||
public GameObject CreateServerButton(string serverName, string address, int port, bool isReadOnly = false)
|
||||
{
|
||||
GameObject multiplayerButtonInst = Instantiate(multiplayerServerButtonRef, serverAreaContent, false);
|
||||
multiplayerButtonInst.name = $"NitroxServer_{serverAreaContent.childCount - 2}";
|
||||
DestroyImmediate(multiplayerButtonInst.RequireGameObject("Load")); // Needs to be deleted before MainMenuServerButton.Init() below
|
||||
Destroy(multiplayerButtonInst.GetComponent<MainMenuLoadButton>());
|
||||
|
||||
GameObject multiplayerLoadButtonInst = Instantiate(multiplayerNewServerButtonRef, multiplayerButtonInst.transform, false);
|
||||
multiplayerLoadButtonInst.name = "Load";
|
||||
|
||||
MainMenuServerButton serverButton = multiplayerButtonInst.AddComponent<MainMenuServerButton>();
|
||||
serverButton.Init(serverName, address, port, isReadOnly);
|
||||
|
||||
scrollBar.SetActive(serverAreaContent.childCount >= 4);
|
||||
foreach (EventTrigger eventTrigger in multiplayerButtonInst.GetComponentsInChildren<EventTrigger>(true))
|
||||
{
|
||||
ForwardTriggerScrollToScrollRect(eventTrigger);
|
||||
}
|
||||
|
||||
return multiplayerButtonInst;
|
||||
}
|
||||
|
||||
private void CreateAddServerButton()
|
||||
{
|
||||
GameObject multiplayerButtonInst = Instantiate(multiplayerNewServerButtonRef, serverAreaContent, false);
|
||||
multiplayerButtonInst.name = "NewServer"; // "NewServer" is important, see OnConfirm()
|
||||
TextMeshProUGUI txt = multiplayerButtonInst.RequireTransform("NewGameButton/Text").GetComponent<TextMeshProUGUI>();
|
||||
txt.text = "Nitrox_AddServer";
|
||||
txt.fontSize *= 1.5f;
|
||||
txt.fontStyle = FontStyles.Bold;
|
||||
|
||||
Button multiplayerButtonButton = multiplayerButtonInst.RequireTransform("NewGameButton").GetComponent<Button>();
|
||||
multiplayerButtonButton.onClick = new Button.ButtonClickedEvent();
|
||||
multiplayerButtonButton.onClick.AddListener(OpenAddServerGroup);
|
||||
|
||||
ForwardTriggerScrollToScrollRect(multiplayerButtonButton.GetComponent<EventTrigger>());
|
||||
}
|
||||
|
||||
private void ForwardTriggerScrollToScrollRect(EventTrigger eventTrigger)
|
||||
{
|
||||
eventTrigger.triggers.RemoveAll(trigger => trigger.eventID == EventTriggerType.Scroll);
|
||||
|
||||
EventTrigger.TriggerEvent callback = new();
|
||||
callback.AddListener(x => scrollRect.Scroll(((PointerEventData)x).scrollDelta.y, 5f));
|
||||
|
||||
eventTrigger.triggers.Add(new EventTrigger.Entry
|
||||
{
|
||||
eventID = EventTriggerType.Scroll,
|
||||
callback = callback
|
||||
});
|
||||
}
|
||||
|
||||
public void OpenAddServerGroup()
|
||||
{
|
||||
DeselectAllItems();
|
||||
MainMenuRightSide.main.OpenGroup(MainMenuCreateServerPanel.NAME);
|
||||
}
|
||||
|
||||
public void RefreshServerEntries()
|
||||
{
|
||||
if (!serverAreaContent)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (Transform child in serverAreaContent)
|
||||
{
|
||||
Destroy(child.gameObject);
|
||||
}
|
||||
|
||||
CreateAddServerButton();
|
||||
LoadSavedServers();
|
||||
CoroutineHost.StartCoroutine(FindLANServers());
|
||||
}
|
||||
}
|
36
NitroxClient/MonoBehaviours/Gui/Modals/ConfirmModal.cs
Normal file
36
NitroxClient/MonoBehaviours/Gui/Modals/ConfirmModal.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.Gui.Modals;
|
||||
|
||||
public class ConfirmModal : Modal
|
||||
{
|
||||
private Action yesCallback;
|
||||
|
||||
public ConfirmModal() : base(yesButtonText: "Confirm", hideNoButton: false, noButtonText: "Cancel", isAvoidable: true, transparency: 0.93f)
|
||||
{ }
|
||||
|
||||
public void Show(string actionText, Action yesCallback)
|
||||
{
|
||||
ModalText = actionText;
|
||||
this.yesCallback = yesCallback;
|
||||
Show();
|
||||
}
|
||||
|
||||
public override void ClickYes()
|
||||
{
|
||||
yesCallback?.Invoke();
|
||||
Hide();
|
||||
OnDeselect();
|
||||
}
|
||||
|
||||
public override void ClickNo()
|
||||
{
|
||||
Hide();
|
||||
OnDeselect();
|
||||
}
|
||||
|
||||
public override void OnDeselect()
|
||||
{
|
||||
yesCallback = null;
|
||||
}
|
||||
}
|
27
NitroxClient/MonoBehaviours/Gui/Modals/InfoModal.cs
Normal file
27
NitroxClient/MonoBehaviours/Gui/Modals/InfoModal.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System.Collections;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.Gui.Modals;
|
||||
|
||||
public class InfoModal : Modal
|
||||
{
|
||||
public InfoModal() : base(yesButtonText: "Ok", isAvoidable: false, transparency: 0.93f, height: 400f)
|
||||
{ }
|
||||
|
||||
public void Show(string actionText)
|
||||
{
|
||||
ModalText = actionText;
|
||||
Show();
|
||||
}
|
||||
|
||||
public override void ClickYes()
|
||||
{
|
||||
Hide();
|
||||
OnDeselect();
|
||||
}
|
||||
|
||||
public IEnumerator ShowAsync(string actionText)
|
||||
{
|
||||
ModalText = actionText;
|
||||
yield return ShowAsync();
|
||||
}
|
||||
}
|
20
NitroxClient/MonoBehaviours/Gui/Modals/KickedModal.cs
Normal file
20
NitroxClient/MonoBehaviours/Gui/Modals/KickedModal.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace NitroxClient.MonoBehaviours.Gui.Modals;
|
||||
|
||||
public class KickedModal : Modal
|
||||
{
|
||||
// When disconnected from the server, we don't want to keep playing
|
||||
public KickedModal() : base(yesButtonText: "OK", freezeGame: true, transparency: 1.0f)
|
||||
{
|
||||
}
|
||||
|
||||
public void Show(string reason)
|
||||
{
|
||||
ModalText = reason;
|
||||
Show();
|
||||
}
|
||||
|
||||
public override void ClickYes()
|
||||
{
|
||||
IngameMenu.main.QuitGame(false);
|
||||
}
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
namespace NitroxClient.MonoBehaviours.Gui.Modals;
|
||||
|
||||
/// <summary>
|
||||
/// Extends the IngameMenu with a disconnect popup.
|
||||
/// </summary>
|
||||
public class LostConnectionModal : Modal
|
||||
{
|
||||
public LostConnectionModal() : base(yesButtonText: "OK", modalText: Language.main.Get("Nitrox_LostConnection"), freezeGame: true, transparency: 1.0f)
|
||||
{
|
||||
}
|
||||
|
||||
public override void ClickYes()
|
||||
{
|
||||
IngameMenu.main.QuitGame(false);
|
||||
}
|
||||
}
|
218
NitroxClient/MonoBehaviours/Gui/Modals/Modal.cs
Normal file
218
NitroxClient/MonoBehaviours/Gui/Modals/Modal.cs
Normal file
@@ -0,0 +1,218 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UWE;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.Gui.Modals;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for Modal components, which are dialog boxes that appear in the middle of the screen
|
||||
/// </summary>
|
||||
public abstract class Modal
|
||||
{
|
||||
/// <summary>
|
||||
/// Get a Modal by its type at any time (static)
|
||||
/// </summary>
|
||||
public static Dictionary<Type, Modal> Modals = new();
|
||||
/// <summary>
|
||||
/// Current modal that is visible on the screen
|
||||
/// </summary>
|
||||
public static Modal CurrentModal;
|
||||
|
||||
private GameObject modalSubWindow;
|
||||
private TextMeshProUGUI text;
|
||||
|
||||
// All the properties that will be overriden by new instances that inherit this class
|
||||
public string SubWindowName { get; init; }
|
||||
public string ModalText { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Makes it possible to dismiss the modal by clicking outside of the modal or pressing escape (default false).
|
||||
/// </summary>
|
||||
public bool IsAvoidable { get; init; }
|
||||
public bool HideNoButton { get; init; }
|
||||
|
||||
public string YesButtonText { get; init; }
|
||||
public string NoButtonText { get; init; }
|
||||
|
||||
public bool FreezeGame { get; init; }
|
||||
|
||||
public float Transparency { get; init; }
|
||||
|
||||
public float Height { get; init; }
|
||||
|
||||
// Is useful for calling IngameMenu::OnDeselect() from a modal class (in Hide() for example)
|
||||
public bool IsAvoidableBypass = false;
|
||||
|
||||
public Modal(string yesButtonText = "YES", bool hideNoButton = true, string noButtonText = "NO", string modalText = "", bool isAvoidable = false, bool freezeGame = false, float transparency = 0.392f, float height = 195f)
|
||||
{
|
||||
Type type = GetType();
|
||||
if (Modals.ContainsKey(type))
|
||||
{
|
||||
throw new NotSupportedException($"You cannot set two modals to have the same Type");
|
||||
}
|
||||
|
||||
SubWindowName = GetType().Name;
|
||||
YesButtonText = yesButtonText;
|
||||
HideNoButton = hideNoButton;
|
||||
NoButtonText = noButtonText;
|
||||
ModalText = modalText;
|
||||
IsAvoidable = isAvoidable;
|
||||
FreezeGame = freezeGame;
|
||||
Transparency = transparency; // 0.392 is the default transparency for Subnautica's modal
|
||||
Height = height;
|
||||
|
||||
Log.Debug($"Registered Modal {SubWindowName} of type {type}");
|
||||
Modals.Add(type, this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the Modal to the screen
|
||||
/// </summary>
|
||||
public void Show()
|
||||
{
|
||||
CoroutineHost.StartCoroutine(ShowAsync());
|
||||
}
|
||||
|
||||
public IEnumerator ShowAsync()
|
||||
{
|
||||
CurrentModal?.Hide();
|
||||
CurrentModal = this;
|
||||
yield return ShowImplementation();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the Modal from the screen
|
||||
/// </summary>
|
||||
public void Hide()
|
||||
{
|
||||
CurrentModal = null;
|
||||
if (FreezeGame)
|
||||
{
|
||||
FreezeTime.End(FreezeTime.Id.Quit);
|
||||
}
|
||||
if (IsAvoidable)
|
||||
{
|
||||
IngameMenu.main.OnDeselect();
|
||||
}
|
||||
else
|
||||
{
|
||||
IsAvoidableBypass = true;
|
||||
IngameMenu.main.OnDeselect();
|
||||
IsAvoidableBypass = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when this modal is deselected (only when pressing outside of the modal)
|
||||
/// </summary>
|
||||
public virtual void OnDeselect() { }
|
||||
|
||||
/// <summary>
|
||||
/// This creates the modal when showing it for the first time, you can't modify it afterwards
|
||||
/// </summary>
|
||||
private void InitSubWindow()
|
||||
{
|
||||
if (!IngameMenu.main)
|
||||
{
|
||||
throw new NotSupportedException($"Cannot show ingame subwindow {SubWindowName} because the ingame window does not exist.");
|
||||
}
|
||||
|
||||
if (!modalSubWindow)
|
||||
{
|
||||
GameObject derivedSubWindow = IngameMenu.main.transform.Find("QuitConfirmation").gameObject;
|
||||
modalSubWindow = UnityEngine.Object.Instantiate(derivedSubWindow, IngameMenu.main.transform, false);
|
||||
modalSubWindow.name = SubWindowName;
|
||||
|
||||
// Styling.
|
||||
RectTransform main = modalSubWindow.GetComponent<RectTransform>();
|
||||
main.sizeDelta = new Vector2(700, Height);
|
||||
|
||||
RectTransform messageTransform = modalSubWindow.FindChild("Header").GetComponent<RectTransform>();
|
||||
messageTransform.sizeDelta = new Vector2(700, Height);
|
||||
messageTransform.anchoredPosition = new Vector2(0, 50 - Height / 2);
|
||||
}
|
||||
|
||||
modalSubWindow.GetComponent<Image>().color = Color.white.WithAlpha(Transparency);
|
||||
|
||||
// Will happen either it's initialized or not
|
||||
UpdateModal();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the modal with informations that may change from one Show() to another
|
||||
/// </summary>
|
||||
private void UpdateModal()
|
||||
{
|
||||
text = modalSubWindow.FindChild("Header").GetComponent<TextMeshProUGUI>();
|
||||
text.text = ModalText;
|
||||
|
||||
GameObject buttonYesObject = modalSubWindow.FindChild("ButtonYes");
|
||||
GameObject buttonNoObject = modalSubWindow.FindChild("ButtonNo");
|
||||
Button yesButton = buttonYesObject.GetComponent<Button>();
|
||||
|
||||
// We need to reinitialize onClick to avoid keeping Persisted Events (which are set manually inside Unity's Editor)
|
||||
yesButton.onClick = new Button.ButtonClickedEvent();
|
||||
yesButton.onClick.AddListener(ClickYes);
|
||||
buttonYesObject.GetComponentInChildren<TextMeshProUGUI>().text = YesButtonText;
|
||||
RectTransform yesButtonTransform = buttonYesObject.GetComponent<RectTransform>();
|
||||
yesButtonTransform.anchoredPosition = new Vector2(yesButtonTransform.anchoredPosition.x, 50f - Height);
|
||||
|
||||
|
||||
// TODO: fix yes and no button positions
|
||||
if (HideNoButton)
|
||||
{
|
||||
UnityEngine.Object.Destroy(buttonNoObject);
|
||||
buttonYesObject.transform.position = new Vector3(modalSubWindow.transform.position.x / 2, buttonYesObject.transform.position.y, buttonYesObject.transform.position.z); // Center Button
|
||||
return;
|
||||
}
|
||||
|
||||
if (buttonNoObject)
|
||||
{
|
||||
Button noButton = buttonNoObject.GetComponent<Button>();
|
||||
noButton.onClick = new Button.ButtonClickedEvent();
|
||||
noButton.onClick.AddListener(ClickNo);
|
||||
buttonNoObject.GetComponentInChildren<TextMeshProUGUI>().text = NoButtonText;
|
||||
RectTransform noButtonTransform = buttonNoObject.GetComponent<RectTransform>();
|
||||
noButtonTransform.anchoredPosition = new Vector2(noButtonTransform.anchoredPosition.x, 50f - Height);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void ClickYes() { }
|
||||
public virtual void ClickNo() { }
|
||||
|
||||
private IEnumerator ShowImplementation()
|
||||
{
|
||||
// Execute frame-by-frame to allow UI scripts to initialize.
|
||||
InitSubWindow();
|
||||
yield return new WaitForEndOfFrame();
|
||||
// Equivalent of IngameMenu.main.Open() but without minding for the freeze
|
||||
IngameMenu.main.gameObject.SetActive(true);
|
||||
IngameMenu.main.Select();
|
||||
yield return new WaitForEndOfFrame();
|
||||
IngameMenu.main.ChangeSubscreen(SubWindowName);
|
||||
yield return new WaitForEndOfFrame();
|
||||
if (FreezeGame)
|
||||
{
|
||||
FreezeTime.Begin(FreezeTime.Id.Quit);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lets you get any existing Modal by its Type
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the modal to get</typeparam>
|
||||
/// <returns>An existing instance of the modal if it already exists, else, a new one</returns>
|
||||
public static T Get<T>() where T : Modal
|
||||
{
|
||||
if (Modals.TryGetValue(typeof(T), out Modal modal))
|
||||
{
|
||||
return (T)modal;
|
||||
}
|
||||
// No need to add entry in dictionary as it's done in constructor
|
||||
return (T)Activator.CreateInstance(typeof(T));
|
||||
}
|
||||
}
|
13
NitroxClient/MonoBehaviours/Gui/Modals/ServerStoppedModal.cs
Normal file
13
NitroxClient/MonoBehaviours/Gui/Modals/ServerStoppedModal.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace NitroxClient.MonoBehaviours.Gui.Modals;
|
||||
|
||||
public class ServerStoppedModal : Modal
|
||||
{
|
||||
public ServerStoppedModal() : base(yesButtonText: "OK", modalText: Language.main.Get("Nitrox_ServerStopped"), freezeGame: true, transparency: 1.0f)
|
||||
{
|
||||
}
|
||||
|
||||
public override void ClickYes()
|
||||
{
|
||||
IngameMenu.main.QuitGame(false);
|
||||
}
|
||||
}
|
178
NitroxClient/MonoBehaviours/IntroCinematicUpdater.cs
Normal file
178
NitroxClient/MonoBehaviours/IntroCinematicUpdater.cs
Normal file
@@ -0,0 +1,178 @@
|
||||
using System.Collections;
|
||||
using NitroxClient.GameLogic;
|
||||
using NitroxModel.Core;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours;
|
||||
|
||||
public class IntroCinematicUpdater : MonoBehaviour
|
||||
{
|
||||
public static RemotePlayer Partner;
|
||||
private static Transform modelRoot;
|
||||
|
||||
private static readonly Transform playerTransform = Player.main.transform;
|
||||
private static readonly Quaternion rotateOtherSide = Quaternion.Euler(0, 180, 0);
|
||||
|
||||
private Transform introEndTarget;
|
||||
private Vector3 introRemoteEndPosition;
|
||||
private Transform[] seatPartsLeft;
|
||||
private Transform[] seatPartsRight;
|
||||
private Transform seatArmRestLeft;
|
||||
private Transform seatArmRestRight;
|
||||
private SkinnedMeshRenderer[] seatBarRendererRight = [];
|
||||
|
||||
private SkinnedMeshRenderer remotePlayerHeadRenderer;
|
||||
private GameObject remotePlayerCustomHead;
|
||||
private SkinnedMeshRenderer[] remoteRenders = [];
|
||||
|
||||
public void Awake()
|
||||
{
|
||||
if (Partner == null)
|
||||
{
|
||||
Log.Error($"[{nameof(IntroCinematicUpdater)}.Awake()]: Partner was null. Disabling MonoBehaviour.");
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
modelRoot = EscapePod.main.transform.Find("models/Life_Pod_damaged_03/root");
|
||||
Transform seatLeft = modelRoot.Find("life_pod_seat_01_left_damaged_jnt1");
|
||||
Transform seatRight = modelRoot.Find("life_pod_seat_01_right_damaged_jnt1");
|
||||
|
||||
seatPartsLeft = new[]
|
||||
{
|
||||
seatLeft.Find("life_pod_seat_01_left_damaged_jnt2"),
|
||||
seatLeft.Find("life_pod_seat_01_left_damaged_jnt3")
|
||||
};
|
||||
|
||||
seatPartsRight = new[]
|
||||
{
|
||||
seatRight.Find("life_pod_seat_01_right_damaged_jnt2"),
|
||||
seatRight.Find("life_pod_seat_01_right_damaged_jnt3")
|
||||
};
|
||||
|
||||
seatArmRestLeft = modelRoot.Find("life_pod_seat_01_left_damaged_jnt4/life_pod_seat_01_left_damaged_jnt5");
|
||||
seatArmRestRight = modelRoot.Find("life_pod_seat_01_right_damaged_jnt4/life_pod_seat_01_right_damaged_jnt5");
|
||||
|
||||
seatBarRendererRight = modelRoot.parent.Find("lifepod_damaged_03_geo/life_pod_seat_01_R").GetComponentsInChildren<SkinnedMeshRenderer>();
|
||||
|
||||
remoteRenders = Partner.PlayerModel.GetComponentsInChildren<SkinnedMeshRenderer>();
|
||||
remotePlayerHeadRenderer = Partner.PlayerModel.transform.Find("male_geo/diveSuit/diveSuit_head_geo").GetComponent<SkinnedMeshRenderer>();
|
||||
|
||||
remotePlayerCustomHead = Instantiate(new GameObject("diveSuit_custom_head_geo"), Partner.PlayerModel.transform.Find("export_skeleton/head_rig"));
|
||||
remotePlayerCustomHead.transform.localPosition = new Vector3(1.0941f, -0.1163f, -1.2107f);
|
||||
remotePlayerCustomHead.transform.localRotation = new Quaternion(-0.05750692f, -0.5272675f, -0.6740523f, -0.5141357f);
|
||||
|
||||
MeshFilter mesh = remotePlayerCustomHead.AddComponent<MeshFilter>();
|
||||
MeshRenderer meshRenderer = remotePlayerCustomHead.AddComponent<MeshRenderer>();
|
||||
mesh.mesh = remotePlayerHeadRenderer.sharedMesh;
|
||||
meshRenderer.materials = remotePlayerHeadRenderer.materials;
|
||||
remotePlayerCustomHead.SetActive(false);
|
||||
|
||||
bool isLeftPlayer = NitroxServiceLocator.Cache<LocalPlayer>.Value.PlayerId < Partner.PlayerId;
|
||||
introEndTarget = EscapePod.main.transform.Find("EscapePodCinematics/intro_end");
|
||||
Vector3 introEndDiff = isLeftPlayer ? new Vector3(0, 0, 0.3f) : new Vector3(0, 0, -0.3f);
|
||||
introRemoteEndPosition = introEndTarget.position - introEndDiff;
|
||||
introEndTarget.position += introEndDiff;
|
||||
}
|
||||
|
||||
private Vector3 remotePlayerDestinationPos;
|
||||
private Quaternion remotePlayerDestinationRot;
|
||||
|
||||
private bool shouldDisplaceLocalPlayer;
|
||||
private float localPlayerDisplacementTime;
|
||||
|
||||
private bool shouldDisplaceRemotePlayer;
|
||||
private Vector3 remotePlayerDisplacement = Vector3.zero;
|
||||
|
||||
private IEnumerator Start()
|
||||
{
|
||||
remoteRenders.ForEach(smr => smr.enabled = false);
|
||||
seatBarRendererRight.ForEach(smr => smr.updateWhenOffscreen = true);
|
||||
|
||||
yield return new WaitForSecondsRealtime(11f); //Local player in seat
|
||||
remoteRenders.ForEach(smr => smr.enabled = true);
|
||||
|
||||
yield return new WaitForSecondsRealtime(33.45f - 11f); //Remote Player deadly hit => disable original head
|
||||
remotePlayerHeadRenderer.enabled = false;
|
||||
|
||||
yield return new WaitForSecondsRealtime(33.58f - 33.45f); //Remote Player visible => enable custom head
|
||||
remotePlayerCustomHead.SetActive(true);
|
||||
|
||||
yield return new WaitForSecondsRealtime(40f - 33.58f); //Seatbelt appears => enable position sync & switch to original head
|
||||
remotePlayerCustomHead.SetActive(false);
|
||||
remotePlayerHeadRenderer.enabled = true;
|
||||
Destroy(remotePlayerCustomHead);
|
||||
|
||||
yield return new WaitForSecondsRealtime(50.5f - 40f); //Slowly move remote player backwards
|
||||
shouldDisplaceRemotePlayer = true;
|
||||
|
||||
yield return new WaitForSecondsRealtime(50.72f - 50.5f); //Disable and move remote player
|
||||
shouldDisplaceRemotePlayer = false;
|
||||
remotePlayerDisplacement = Vector3.zero;
|
||||
remoteRenders.ForEach(smr => smr.enabled = false);
|
||||
|
||||
yield return new WaitForSecondsRealtime(53f - 50.72f); //Slowly move local player to modified cin-end
|
||||
shouldDisplaceLocalPlayer = true;
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
Vector3 modelRootPos = modelRoot.position;
|
||||
Quaternion modelRootRot = modelRoot.rotation;
|
||||
|
||||
// Coping the global rotation, inverting the y and adding 180° to it
|
||||
Quaternion InverseRotateAroundEscapePod(Quaternion from)
|
||||
{
|
||||
Quaternion offset = Quaternion.Inverse(modelRootRot) * from;
|
||||
return modelRootRot * rotateOtherSide * new Quaternion(-offset.x, offset.y, offset.z, -offset.w);
|
||||
}
|
||||
|
||||
// Mirror animate the remote like the local player
|
||||
Vector3 posDiff = playerTransform.position - modelRootPos;
|
||||
remotePlayerDestinationPos = modelRootPos + new Vector3(posDiff.x, posDiff.y, -posDiff.z) + remotePlayerDisplacement;
|
||||
remotePlayerDestinationRot = InverseRotateAroundEscapePod(playerTransform.rotation);
|
||||
transform.SetPositionAndRotation(remotePlayerDestinationPos, remotePlayerDestinationRot);
|
||||
|
||||
// Mirror animate the seat parts and stop it before the animation ends so the SN animator resets our manipulations
|
||||
if (!shouldDisplaceLocalPlayer)
|
||||
{
|
||||
for (int i = 0; i < seatPartsLeft.Length; i++)
|
||||
{
|
||||
seatPartsRight[i].localRotation = seatPartsLeft[i].localRotation;
|
||||
}
|
||||
|
||||
seatArmRestRight.localPosition = -seatArmRestLeft.localPosition;
|
||||
}
|
||||
|
||||
if (shouldDisplaceRemotePlayer) remotePlayerDisplacement += new Vector3(0, 0, 0.05f);
|
||||
|
||||
if (shouldDisplaceLocalPlayer)
|
||||
{
|
||||
Vector3 introEndTargetPos = introEndTarget.position;
|
||||
playerTransform.position = Vector3.Lerp(playerTransform.position, introEndTargetPos, localPlayerDisplacementTime);
|
||||
MainCameraControl.main.transform.position = Vector3.Lerp(MainCameraControl.main.transform.position, introEndTargetPos, localPlayerDisplacementTime);
|
||||
|
||||
localPlayerDisplacementTime += 0.01f;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
foreach (SkinnedMeshRenderer renderer in remoteRenders)
|
||||
{
|
||||
if (renderer)
|
||||
{
|
||||
renderer.enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (SkinnedMeshRenderer renderer in seatBarRendererRight)
|
||||
{
|
||||
if (renderer)
|
||||
{
|
||||
renderer.updateWhenOffscreen = false;
|
||||
}
|
||||
}
|
||||
|
||||
transform.position = introRemoteEndPosition;
|
||||
}
|
||||
}
|
178
NitroxClient/MonoBehaviours/MoonpoolManager.cs
Normal file
178
NitroxClient/MonoBehaviours/MoonpoolManager.cs
Normal file
@@ -0,0 +1,178 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using NitroxClient.GameLogic;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.DataStructures.GameLogic.Entities;
|
||||
using NitroxModel.DataStructures.GameLogic.Entities.Bases;
|
||||
using NitroxModel.DataStructures.Util;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours;
|
||||
|
||||
/// <summary>
|
||||
/// Saves the moonpools NitroxEntity of a <see cref="Base"/> to assign them back after each <see cref="Base.RebuildGeometry"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// To recognize pieces even after the base is rebuilt, we use the base anchor (<see cref="Base.anchor"/>) to get an absolute cell value.
|
||||
/// </remarks>
|
||||
public class MoonpoolManager : MonoBehaviour
|
||||
{
|
||||
private Entities entities;
|
||||
|
||||
private Base @base;
|
||||
private NitroxId baseId;
|
||||
private Dictionary<Int3, MoonpoolEntity> moonpoolsByCell;
|
||||
public MoonpoolEntity LatestRegisteredMoonpool { get; private set; }
|
||||
|
||||
public void Awake()
|
||||
{
|
||||
entities = this.Resolve<Entities>();
|
||||
|
||||
if (!TryGetComponent(out @base))
|
||||
{
|
||||
Log.Error($"Tried adding a {nameof(MoonpoolManager)} to a GameObject that isn't a bases, deleting it.");
|
||||
Destroy(this);
|
||||
return;
|
||||
}
|
||||
@base.TryGetNitroxId(out baseId);
|
||||
moonpoolsByCell = new();
|
||||
@base.onPostRebuildGeometry += OnPostRebuildGeometry;
|
||||
}
|
||||
|
||||
public void OnDestroy()
|
||||
{
|
||||
@base.onPostRebuildGeometry -= OnPostRebuildGeometry;
|
||||
}
|
||||
|
||||
public void LateAssignNitroxEntity(NitroxId baseId)
|
||||
{
|
||||
this.baseId = baseId;
|
||||
NitroxId nextId = baseId.Increment(); // To be recognizable, we need it to be deterministic
|
||||
foreach (MoonpoolEntity moonpoolEntity in moonpoolsByCell.Values)
|
||||
{
|
||||
moonpoolEntity.ParentId = baseId;
|
||||
moonpoolEntity.Id = nextId;
|
||||
|
||||
nextId = nextId.Increment();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnPostRebuildGeometry(Base _)
|
||||
{
|
||||
foreach (KeyValuePair<Int3, MoonpoolEntity> moonpoolEntry in moonpoolsByCell)
|
||||
{
|
||||
AssignNitroxEntityToMoonpool(moonpoolEntry.Key, moonpoolEntry.Value.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private void AssignNitroxEntityToMoonpool(Int3 absoluteCell, NitroxId moonpoolId, TaskResult<Optional<GameObject>> result = null)
|
||||
{
|
||||
Int3 relativeCell = Relative(absoluteCell);
|
||||
Transform baseCellTransform = @base.GetCellObject(relativeCell);
|
||||
if (!baseCellTransform)
|
||||
{
|
||||
Log.Warn($"[{nameof(AssignNitroxEntityToMoonpool)}] CellObject not found for RelativeCell: {relativeCell}, AbsoluteCell: {absoluteCell}");
|
||||
return;
|
||||
}
|
||||
if (baseCellTransform.TryGetComponentInChildren(out VehicleDockingBay vehicleDockingBay, true))
|
||||
{
|
||||
result?.Set(vehicleDockingBay.gameObject);
|
||||
NitroxEntity.SetNewId(vehicleDockingBay.gameObject, moonpoolId);
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<GameObject> RegisterMoonpool(Transform constructableTransform, NitroxId moonpoolId)
|
||||
{
|
||||
return RegisterMoonpool(Absolute(constructableTransform.position), moonpoolId);
|
||||
}
|
||||
|
||||
public Optional<GameObject> RegisterMoonpool(Int3 absoluteCell, NitroxId moonpoolId)
|
||||
{
|
||||
moonpoolsByCell[absoluteCell] = new(moonpoolId, baseId, absoluteCell.ToDto());
|
||||
TaskResult<Optional<GameObject>> resultObject = new();
|
||||
AssignNitroxEntityToMoonpool(absoluteCell, moonpoolId, resultObject);
|
||||
LatestRegisteredMoonpool = moonpoolsByCell[absoluteCell];
|
||||
return resultObject.Get();
|
||||
}
|
||||
|
||||
public NitroxId DeregisterMoonpool(Transform constructableTransform)
|
||||
{
|
||||
Int3 absoluteCell = Absolute(constructableTransform.position);
|
||||
if (moonpoolsByCell.TryGetValue(absoluteCell, out MoonpoolEntity moonpoolEntity))
|
||||
{
|
||||
moonpoolsByCell.Remove(absoluteCell);
|
||||
return moonpoolEntity.Id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void LoadMoonpools(IEnumerable<MoonpoolEntity> moonpoolEntities)
|
||||
{
|
||||
moonpoolsByCell.Clear();
|
||||
foreach (MoonpoolEntity moonpoolEntity in moonpoolEntities)
|
||||
{
|
||||
moonpoolsByCell[moonpoolEntity.Cell.ToUnity()] = moonpoolEntity;
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator SpawnVehicles()
|
||||
{
|
||||
foreach (MoonpoolEntity moonpoolEntity in moonpoolsByCell.Values)
|
||||
{
|
||||
VehicleWorldEntity moonpoolVehicleEntity = moonpoolEntity.ChildEntities.OfType<VehicleWorldEntity>().FirstOrFallback(null);
|
||||
if (moonpoolVehicleEntity != null)
|
||||
{
|
||||
yield return entities.SpawnEntityAsync(moonpoolVehicleEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Int3 Absolute(Vector3 position)
|
||||
{
|
||||
return Absolute(@base.WorldToGrid(position));
|
||||
}
|
||||
|
||||
private Int3 Absolute(Int3 baseCell)
|
||||
{
|
||||
return baseCell - @base.GetAnchor();
|
||||
}
|
||||
|
||||
private Int3 Relative(Int3 baseCell)
|
||||
{
|
||||
return baseCell + @base.GetAnchor();
|
||||
}
|
||||
|
||||
public List<MoonpoolEntity> GetSavedMoonpools()
|
||||
{
|
||||
return moonpoolsByCell.Values.ToList();
|
||||
}
|
||||
|
||||
public Dictionary<NitroxId, NitroxInt3> GetMoonpoolsUpdate()
|
||||
{
|
||||
return moonpoolsByCell.ToDictionary(entry => entry.Value.Id, entry => entry.Key.ToDto());
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
public void PrintDebug()
|
||||
{
|
||||
Log.Debug($"MoonpoolManager's registered moonpools (anchor: {@base.GetAnchor()}):");
|
||||
foreach (MoonpoolEntity moonpoolEntity in moonpoolsByCell.Values)
|
||||
{
|
||||
Int3 absoluteCell = moonpoolEntity.Cell.ToUnity();
|
||||
Int3 baseCell = Relative(moonpoolEntity.Cell.ToUnity());
|
||||
Log.Debug($"AbsoluteCell: {absoluteCell}, BaseCell: {baseCell}, id: {moonpoolEntity.Id}");
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerator RestoreMoonpools(IEnumerable<MoonpoolEntity> moonpoolEntities, Base @base)
|
||||
{
|
||||
MoonpoolManager moonpoolManager = @base.gameObject.EnsureComponent<MoonpoolManager>();
|
||||
moonpoolManager.LoadMoonpools(moonpoolEntities);
|
||||
moonpoolManager.OnPostRebuildGeometry(@base);
|
||||
yield return moonpoolManager.SpawnVehicles();
|
||||
}
|
||||
}
|
108
NitroxClient/MonoBehaviours/MovementBroadcaster.cs
Normal file
108
NitroxClient/MonoBehaviours/MovementBroadcaster.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
using System.Collections.Generic;
|
||||
using NitroxClient.Communication.Abstract;
|
||||
using NitroxClient.GameLogic;
|
||||
using NitroxClient.MonoBehaviours.Vehicles;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.Packets;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours;
|
||||
|
||||
public class MovementBroadcaster : MonoBehaviour
|
||||
{
|
||||
public const int BROADCAST_FREQUENCY = 30;
|
||||
public const float BROADCAST_PERIOD = 1f / BROADCAST_FREQUENCY;
|
||||
|
||||
public static MovementBroadcaster Instance;
|
||||
|
||||
public Dictionary<NitroxId, MovementReplicator> Replicators = [];
|
||||
private readonly Dictionary<NitroxId, WatchedEntry> watchedEntries = [];
|
||||
private float latestBroadcastTime;
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (Instance)
|
||||
{
|
||||
Log.Error($"There's already a {nameof(MovementBroadcaster)} Instance alive, destroying the new one.");
|
||||
Destroy(this);
|
||||
return;
|
||||
}
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
public void OnDestroy()
|
||||
{
|
||||
Instance = null;
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
float currentTime = (float)this.Resolve<TimeManager>().RealTimeElapsed;
|
||||
if (currentTime < latestBroadcastTime + BROADCAST_PERIOD)
|
||||
{
|
||||
return;
|
||||
}
|
||||
latestBroadcastTime = currentTime;
|
||||
BroadcastLocalData(currentTime);
|
||||
}
|
||||
|
||||
public void BroadcastLocalData(float time)
|
||||
{
|
||||
List<MovementData> data = [];
|
||||
List<NitroxId> watchedIds = [..watchedEntries.Keys];
|
||||
|
||||
for (int i = watchedIds.Count - 1; i >= 0; i--)
|
||||
{
|
||||
NitroxId entryId = watchedIds[i];
|
||||
WatchedEntry entry = watchedEntries[entryId];
|
||||
|
||||
if (entry.ShouldBroadcastMovement())
|
||||
{
|
||||
data.Add(entry.GetMovementData(entryId));
|
||||
entry.OnBroadcastPosition();
|
||||
}
|
||||
}
|
||||
|
||||
if (data.Count > 0)
|
||||
{
|
||||
this.Resolve<IPacketSender>().Send(new VehicleMovements(data, time));
|
||||
}
|
||||
}
|
||||
|
||||
public static void RegisterWatched(GameObject gameObject, NitroxId entityId)
|
||||
{
|
||||
if (!Instance)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Instance.watchedEntries.ContainsKey(entityId))
|
||||
{
|
||||
Instance.watchedEntries.Add(entityId, new(entityId, gameObject.transform));
|
||||
}
|
||||
}
|
||||
|
||||
public static void UnregisterWatched(NitroxId entityId)
|
||||
{
|
||||
if (Instance)
|
||||
{
|
||||
Instance.watchedEntries.Remove(entityId);
|
||||
}
|
||||
}
|
||||
|
||||
public static void RegisterReplicator(MovementReplicator movementReplicator)
|
||||
{
|
||||
if (Instance)
|
||||
{
|
||||
Instance.Replicators.Add(movementReplicator.objectId, movementReplicator);
|
||||
}
|
||||
}
|
||||
|
||||
public static void UnregisterReplicator(MovementReplicator movementReplicator)
|
||||
{
|
||||
if (Instance)
|
||||
{
|
||||
Instance.Replicators.Remove(movementReplicator.objectId);
|
||||
}
|
||||
}
|
||||
}
|
212
NitroxClient/MonoBehaviours/MovementReplicator.cs
Normal file
212
NitroxClient/MonoBehaviours/MovementReplicator.cs
Normal file
@@ -0,0 +1,212 @@
|
||||
using System.Collections.Generic;
|
||||
using NitroxClient.GameLogic;
|
||||
using NitroxClient.GameLogic.Settings;
|
||||
using NitroxClient.MonoBehaviours.Cyclops;
|
||||
using NitroxClient.MonoBehaviours.Vehicles;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.Packets;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours;
|
||||
|
||||
public abstract class MovementReplicator : MonoBehaviour
|
||||
{
|
||||
public const float INTERPOLATION_TIME = 4 * MovementBroadcaster.BROADCAST_PERIOD;
|
||||
public const float SNAPSHOT_EXPIRATION_TIME = 5f * INTERPOLATION_TIME;
|
||||
|
||||
private readonly LinkedList<Snapshot> buffer = new();
|
||||
/// <summary>
|
||||
/// To ensure a smooth experience, we need a max allowed latency value which should top the incoming latencies at all times.
|
||||
/// Big increments and any decrements of this value will likely cause stutter, so we try to avoid changing this value too much.
|
||||
/// But it is required that after a lag spike, we eventually lower down that value, which is done periodically <see cref="NitroxPrefs.LatencyUpdatePeriod"/>.
|
||||
/// </summary>
|
||||
public float maxAllowedLatency;
|
||||
|
||||
private float latestLatencyBumpTime;
|
||||
private float maxLatencyDetectedRecently;
|
||||
|
||||
/// <summary>
|
||||
/// When encountering a latency bump, we must expect worse happening right after, so we add this margin to our new <see cref="maxAllowedLatency"/>.
|
||||
/// After each periodical latency update (<see cref="LatencyUpdatePeriod"/>), we only want to lower the latency if it's way smaller than the current variable latency.
|
||||
/// The safety threshold is defined by this value.
|
||||
/// </summary>
|
||||
private float SafetyLatencyMargin => NitroxPrefs.SafetyLatencyMargin.Value;
|
||||
|
||||
private float LatencyUpdatePeriod => NitroxPrefs.LatencyUpdatePeriod.Value;
|
||||
|
||||
private Rigidbody rigidbody;
|
||||
public NitroxId objectId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current time must be based on real time to avoid effects from time changes/speed.
|
||||
/// </summary>
|
||||
private float CurrentTime => (float)this.Resolve<TimeManager>().RealTimeElapsed;
|
||||
|
||||
public void AddSnapshot(MovementData movementData, float time)
|
||||
{
|
||||
float currentTime = CurrentTime;
|
||||
float latency = currentTime - time;
|
||||
|
||||
if (latency > maxAllowedLatency)
|
||||
{
|
||||
maxAllowedLatency = latency + SafetyLatencyMargin;
|
||||
latestLatencyBumpTime = currentTime;
|
||||
maxLatencyDetectedRecently = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
maxLatencyDetectedRecently = Mathf.Max(latency, maxLatencyDetectedRecently);
|
||||
|
||||
if (currentTime - latestLatencyBumpTime >= LatencyUpdatePeriod)
|
||||
{
|
||||
if (maxLatencyDetectedRecently < maxAllowedLatency - 2 * SafetyLatencyMargin)
|
||||
{
|
||||
maxAllowedLatency = maxLatencyDetectedRecently + SafetyLatencyMargin; // regular gameplay latency variation
|
||||
}
|
||||
latestLatencyBumpTime = currentTime;
|
||||
maxLatencyDetectedRecently = 0;
|
||||
}
|
||||
}
|
||||
|
||||
float occurrenceTime = time + INTERPOLATION_TIME + maxAllowedLatency;
|
||||
|
||||
// Cleaning any previous value change that would occur later than the newly received snapshot
|
||||
while (buffer.Last != null && buffer.Last.Value.IsSnapshotNewer(occurrenceTime))
|
||||
{
|
||||
buffer.RemoveLast();
|
||||
}
|
||||
|
||||
buffer.AddLast(new Snapshot(movementData, occurrenceTime));
|
||||
}
|
||||
|
||||
public void ClearBuffer() => buffer.Clear();
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (!gameObject.TryGetNitroxId(out NitroxId _objectId))
|
||||
{
|
||||
Log.Error($"Can't start a {nameof(MovementReplicator)} on {name} because it doesn't have an attached: {nameof(NitroxEntity)}");
|
||||
Destroy(this);
|
||||
return;
|
||||
}
|
||||
objectId = _objectId;
|
||||
|
||||
rigidbody = GetComponent<Rigidbody>();
|
||||
if (gameObject.TryGetComponent(out NitroxCyclops nitroxCyclops))
|
||||
{
|
||||
nitroxCyclops.SetReceiving();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (gameObject.TryGetComponent(out WorldForces worldForces))
|
||||
{
|
||||
worldForces.enabled = false;
|
||||
}
|
||||
rigidbody.isKinematic = false;
|
||||
}
|
||||
|
||||
MovementBroadcaster.RegisterReplicator(this);
|
||||
}
|
||||
|
||||
public void OnDestroy()
|
||||
{
|
||||
if (gameObject.TryGetComponent(out NitroxCyclops nitroxCyclops))
|
||||
{
|
||||
nitroxCyclops.SetBroadcasting();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (gameObject.TryGetComponent(out WorldForces worldForces))
|
||||
{
|
||||
worldForces.enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
MovementBroadcaster.UnregisterReplicator(this);
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
if (buffer.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float currentTime = CurrentTime;
|
||||
|
||||
// Sorting out expired nodes
|
||||
while (buffer.First != null && buffer.First.Value.IsExpired(currentTime))
|
||||
{
|
||||
buffer.RemoveFirst();
|
||||
}
|
||||
|
||||
LinkedListNode<Snapshot> firstNode = buffer.First;
|
||||
if (firstNode == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Current node is not useable yet
|
||||
if (firstNode.Value.IsSnapshotNewer(currentTime))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Purging the next nodes if they should have already happened (we still have an expiration margin for the first node so it's fine)
|
||||
while (firstNode.Next != null && !firstNode.Next.Value.IsSnapshotNewer(currentTime))
|
||||
{
|
||||
firstNode = firstNode.Next;
|
||||
buffer.RemoveFirst();
|
||||
}
|
||||
|
||||
LinkedListNode<Snapshot> nextNode = firstNode.Next;
|
||||
|
||||
// Current node is fine but there's no next node (waiting for it without dropping current)
|
||||
if (nextNode == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Interpolation
|
||||
|
||||
MovementData prevData = firstNode.Value.Data;
|
||||
MovementData nextData = nextNode.Value.Data;
|
||||
|
||||
float t = (currentTime - firstNode.Value.Time) / (nextNode.Value.Time - firstNode.Value.Time);
|
||||
|
||||
transform.position = Vector3.Lerp(prevData.Position.ToUnity(), nextData.Position.ToUnity(), t);
|
||||
|
||||
transform.rotation = Quaternion.Lerp(prevData.Rotation.ToUnity(), nextData.Rotation.ToUnity(), t);
|
||||
|
||||
ApplyNewMovementData(nextData);
|
||||
|
||||
// TODO: fix remote players being able to go through the object (ex: cyclops)
|
||||
}
|
||||
|
||||
public abstract void ApplyNewMovementData(MovementData newMovementData);
|
||||
|
||||
public record struct Snapshot(MovementData Data, float Time)
|
||||
{
|
||||
public bool IsSnapshotNewer(float currentTime) => currentTime < Time;
|
||||
|
||||
public bool IsExpired(float currentTime) => currentTime > Time + SNAPSHOT_EXPIRATION_TIME;
|
||||
}
|
||||
|
||||
public static MovementReplicator AddReplicatorToObject(GameObject gameObject)
|
||||
{
|
||||
if (gameObject.GetComponent<SeaMoth>())
|
||||
{
|
||||
return gameObject.AddComponent<SeamothMovementReplicator>();
|
||||
}
|
||||
if (gameObject.GetComponent<Exosuit>())
|
||||
{
|
||||
return gameObject.AddComponent<ExosuitMovementReplicator>();
|
||||
}
|
||||
if (gameObject.GetComponent<SubControl>())
|
||||
{
|
||||
return gameObject.AddComponent<CyclopsMovementReplicator>();
|
||||
}
|
||||
return gameObject.AddComponent<MovementReplicator>();
|
||||
}
|
||||
}
|
233
NitroxClient/MonoBehaviours/Multiplayer.cs
Normal file
233
NitroxClient/MonoBehaviours/Multiplayer.cs
Normal file
@@ -0,0 +1,233 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using NitroxClient.Communication;
|
||||
using NitroxClient.Communication.Abstract;
|
||||
using NitroxClient.Communication.MultiplayerSession;
|
||||
using NitroxClient.Communication.Packets.Processors.Abstract;
|
||||
using NitroxClient.GameLogic;
|
||||
using NitroxClient.GameLogic.Bases;
|
||||
using NitroxClient.GameLogic.ChatUI;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap;
|
||||
using NitroxClient.MonoBehaviours.Cyclops;
|
||||
using NitroxClient.MonoBehaviours.Discord;
|
||||
using NitroxClient.MonoBehaviours.Gui.MainMenu;
|
||||
using NitroxClient.MonoBehaviours.Gui.MainMenu.ServerJoin;
|
||||
using NitroxModel.Core;
|
||||
using NitroxModel.Packets;
|
||||
using NitroxModel.Packets.Processors.Abstract;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UWE;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours
|
||||
{
|
||||
public class Multiplayer : MonoBehaviour
|
||||
{
|
||||
public static Multiplayer Main;
|
||||
private readonly Dictionary<Type, PacketProcessor> packetProcessorCache = new();
|
||||
private IClient client;
|
||||
private IMultiplayerSession multiplayerSession;
|
||||
private PacketReceiver packetReceiver;
|
||||
private ThrottledPacketSender throttledPacketSender;
|
||||
private GameLogic.Terrain terrain;
|
||||
|
||||
public bool InitialSyncCompleted { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if multiplayer is loaded and client is connected to a server.
|
||||
/// </summary>
|
||||
public static bool Active => Main && Main.multiplayerSession.Client.IsConnected;
|
||||
|
||||
/// <summary>
|
||||
/// True if multiplayer is loaded and player has successfully joined a server.
|
||||
/// </summary>
|
||||
public static bool Joined => Main && Main.multiplayerSession.CurrentState.CurrentStage == MultiplayerSessionConnectionStage.SESSION_JOINED;
|
||||
|
||||
public void Awake()
|
||||
{
|
||||
NitroxServiceLocator.LifetimeScopeEnded += (_, _) => packetProcessorCache.Clear();
|
||||
client = NitroxServiceLocator.LocateService<IClient>();
|
||||
multiplayerSession = NitroxServiceLocator.LocateService<IMultiplayerSession>();
|
||||
packetReceiver = NitroxServiceLocator.LocateService<PacketReceiver>();
|
||||
throttledPacketSender = NitroxServiceLocator.LocateService<ThrottledPacketSender>();
|
||||
terrain = NitroxServiceLocator.LocateService<GameLogic.Terrain>();
|
||||
|
||||
Main = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
|
||||
Log.Info("Multiplayer client loaded…");
|
||||
Log.InGame(Language.main.Get("Nitrox_MultiplayerLoaded"));
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
client.PollEvents();
|
||||
|
||||
if (multiplayerSession.CurrentState.CurrentStage != MultiplayerSessionConnectionStage.DISCONNECTED)
|
||||
{
|
||||
ProcessPackets();
|
||||
throttledPacketSender.Update();
|
||||
|
||||
// Loading up shouldn't be bothered by entities spawning in the surroundings
|
||||
if (multiplayerSession.CurrentState.CurrentStage == MultiplayerSessionConnectionStage.SESSION_JOINED &&
|
||||
InitialSyncCompleted)
|
||||
{
|
||||
terrain.UpdateVisibility();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static event Action OnLoadingComplete;
|
||||
public static event Action OnBeforeMultiplayerStart;
|
||||
public static event Action OnAfterMultiplayerEnd;
|
||||
|
||||
public static void SubnauticaLoadingStarted()
|
||||
{
|
||||
OnBeforeMultiplayerStart?.Invoke();
|
||||
}
|
||||
|
||||
public static void SubnauticaLoadingCompleted()
|
||||
{
|
||||
if (Active)
|
||||
{
|
||||
Main.InitialSyncCompleted = false;
|
||||
Main.StartCoroutine(LoadAsync());
|
||||
}
|
||||
else
|
||||
{
|
||||
SetLoadingComplete();
|
||||
OnLoadingComplete?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerator LoadAsync()
|
||||
{
|
||||
WaitScreen.ManualWaitItem worldSettleItem = WaitScreen.Add(Language.main.Get("Nitrox_WorldSettling"));
|
||||
|
||||
yield return new WaitUntil(() => LargeWorldStreamer.main != null &&
|
||||
LargeWorldStreamer.main.land != null &&
|
||||
LargeWorldStreamer.main.IsReady() &&
|
||||
LargeWorldStreamer.main.IsWorldSettled());
|
||||
|
||||
WaitScreen.Remove(worldSettleItem);
|
||||
|
||||
WaitScreen.ManualWaitItem item = WaitScreen.Add(Language.main.Get("Nitrox_JoiningSession"));
|
||||
yield return Main.StartCoroutine(Main.StartSession());
|
||||
WaitScreen.Remove(item);
|
||||
|
||||
yield return new WaitUntil(() => Main.InitialSyncCompleted);
|
||||
|
||||
SetLoadingComplete();
|
||||
OnLoadingComplete?.Invoke();
|
||||
}
|
||||
|
||||
public void ProcessPackets()
|
||||
{
|
||||
static PacketProcessor ResolveProcessor(Packet packet, Dictionary<Type, PacketProcessor> processorCache)
|
||||
{
|
||||
Type packetType = packet.GetType();
|
||||
if (processorCache.TryGetValue(packetType, out PacketProcessor processor))
|
||||
{
|
||||
return processor;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Type packetProcessorType = typeof(ClientPacketProcessor<>).MakeGenericType(packetType);
|
||||
return processorCache[packetType] = (PacketProcessor)NitroxServiceLocator.LocateService(packetProcessorType);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, $"Failed to find packet processor for packet {packet}");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
packetReceiver.ConsumePackets(static (packet, processorCache) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
ResolveProcessor(packet, processorCache)?.ProcessPacket(packet, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, $"Error while processing packet {packet}");
|
||||
}
|
||||
}, packetProcessorCache);
|
||||
}
|
||||
|
||||
public IEnumerator StartSession()
|
||||
{
|
||||
yield return StartCoroutine(InitializeLocalPlayerState());
|
||||
multiplayerSession.JoinSession();
|
||||
InitMonoBehaviours();
|
||||
Utils.SetContinueMode(true);
|
||||
SceneManager.sceneLoaded += SceneManager_sceneLoaded;
|
||||
}
|
||||
|
||||
public void InitMonoBehaviours()
|
||||
{
|
||||
// Gameplay.
|
||||
gameObject.AddComponent<AnimationSender>();
|
||||
gameObject.AddComponent<PlayerMovementBroadcaster>();
|
||||
gameObject.AddComponent<PlayerDeathBroadcaster>();
|
||||
gameObject.AddComponent<PlayerStatsBroadcaster>();
|
||||
gameObject.AddComponent<EntityPositionBroadcaster>();
|
||||
gameObject.AddComponent<BuildingHandler>();
|
||||
gameObject.AddComponent<MovementBroadcaster>();
|
||||
VirtualCyclops.Initialize();
|
||||
}
|
||||
|
||||
public void StopCurrentSession()
|
||||
{
|
||||
SceneManager.sceneLoaded -= SceneManager_sceneLoaded;
|
||||
OnAfterMultiplayerEnd?.Invoke();
|
||||
}
|
||||
|
||||
private static void SetLoadingComplete()
|
||||
{
|
||||
WaitScreen.main.isWaiting = false;
|
||||
WaitScreen.main.stageProgress.Clear();
|
||||
FreezeTime.End(FreezeTime.Id.WaitScreen);
|
||||
WaitScreen.main.items.Clear();
|
||||
|
||||
PlayerManager remotePlayerManager = NitroxServiceLocator.LocateService<PlayerManager>();
|
||||
|
||||
LoadingScreenVersionText.DisableWarningText();
|
||||
DiscordClient.InitializeRPInGame(Main.multiplayerSession.AuthenticationContext.Username, remotePlayerManager.GetTotalPlayerCount(), Main.multiplayerSession.SessionPolicy.MaxConnections);
|
||||
CoroutineHost.StartCoroutine(NitroxServiceLocator.LocateService<PlayerChatManager>().LoadChatKeyHint());
|
||||
}
|
||||
|
||||
private IEnumerator InitializeLocalPlayerState()
|
||||
{
|
||||
ILocalNitroxPlayer localPlayer = NitroxServiceLocator.LocateService<ILocalNitroxPlayer>();
|
||||
IEnumerable<IColorSwapManager> colorSwapManagers = NitroxServiceLocator.LocateService<IEnumerable<IColorSwapManager>>();
|
||||
|
||||
// This is used to init the lazy GameObject in order to create a real default Body Prototype for other players
|
||||
GameObject body = localPlayer.BodyPrototype;
|
||||
Log.Info($"Init body prototype {body.name}");
|
||||
|
||||
ColorSwapAsyncOperation swapOperation = new ColorSwapAsyncOperation(localPlayer, colorSwapManagers).BeginColorSwap();
|
||||
yield return new WaitUntil(() => swapOperation.IsColorSwapComplete());
|
||||
swapOperation.ApplySwappedColors();
|
||||
|
||||
// UWE developers added noisy logging for non-whitelisted components during serialization.
|
||||
// We add NitroxEntiy in here to avoid a large amount of log spam.
|
||||
ProtobufSerializer.componentWhitelist.Add(nameof(NitroxEntity));
|
||||
}
|
||||
|
||||
private void SceneManager_sceneLoaded(Scene scene, LoadSceneMode loadMode)
|
||||
{
|
||||
if (scene.name == "XMenu")
|
||||
{
|
||||
// If we just disconnected from a multiplayer session, then we need to kill the connection here.
|
||||
// Maybe a better place for this, but here works in a pinch.
|
||||
JoinServerBackend.StopMultiplayerClient();
|
||||
SceneCleaner.Open();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
83
NitroxClient/MonoBehaviours/MultiplayerVehicleControl.cs
Normal file
83
NitroxClient/MonoBehaviours/MultiplayerVehicleControl.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
using System;
|
||||
using NitroxClient.GameLogic;
|
||||
using NitroxClient.Unity.Smoothing;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours
|
||||
{
|
||||
public abstract class MultiplayerVehicleControl : MonoBehaviour
|
||||
{
|
||||
private Rigidbody rigidbody;
|
||||
|
||||
protected readonly SmoothParameter SmoothYaw = new SmoothParameter();
|
||||
protected readonly SmoothParameter SmoothPitch = new SmoothParameter();
|
||||
protected readonly SmoothVector SmoothLeftArm = new SmoothVector();
|
||||
protected readonly SmoothVector SmoothRightArm = new SmoothVector();
|
||||
protected SmoothVector SmoothPosition;
|
||||
protected SmoothVector SmoothVelocity;
|
||||
protected SmoothRotation SmoothRotation;
|
||||
protected SmoothVector SmoothAngularVelocity;
|
||||
protected Action<float> WheelYawSetter;
|
||||
protected Action<float> WheelPitchSetter;
|
||||
|
||||
protected virtual void Awake()
|
||||
{
|
||||
rigidbody = gameObject.GetComponent<Rigidbody>();
|
||||
// For now, we assume the set position and rotation is equal to the server one.
|
||||
// Default velocities are probably empty, but set them anyway.
|
||||
SmoothPosition = new SmoothVector(gameObject.transform.position);
|
||||
SmoothVelocity = new SmoothVector(rigidbody.velocity);
|
||||
SmoothRotation = new SmoothRotation(gameObject.transform.rotation);
|
||||
SmoothAngularVelocity = new SmoothVector(rigidbody.angularVelocity);
|
||||
}
|
||||
|
||||
protected virtual void FixedUpdate()
|
||||
{
|
||||
SmoothYaw.FixedUpdate();
|
||||
SmoothPitch.FixedUpdate();
|
||||
|
||||
SmoothPosition.FixedUpdate();
|
||||
SmoothVelocity.FixedUpdate();
|
||||
rigidbody.velocity = MovementHelper.GetCorrectedVelocity(SmoothPosition.Current, SmoothVelocity.Current, gameObject, Time.fixedDeltaTime);
|
||||
SmoothRotation.FixedUpdate();
|
||||
SmoothAngularVelocity.FixedUpdate();
|
||||
rigidbody.angularVelocity = MovementHelper.GetCorrectedAngularVelocity(SmoothRotation.Current, SmoothAngularVelocity.Current, gameObject, Time.fixedDeltaTime);
|
||||
|
||||
WheelYawSetter(SmoothYaw.SmoothValue);
|
||||
WheelPitchSetter(SmoothPitch.SmoothValue);
|
||||
}
|
||||
|
||||
internal void SetPositionVelocityRotation(Vector3 remotePosition, Vector3 remoteVelocity, Quaternion remoteRotation, Vector3 remoteAngularVelocity)
|
||||
{
|
||||
gameObject.SetActive(true);
|
||||
SmoothPosition.Target = remotePosition;
|
||||
SmoothVelocity.Target = remoteVelocity;
|
||||
SmoothRotation.Target = remoteRotation;
|
||||
SmoothAngularVelocity.Target = remoteAngularVelocity;
|
||||
}
|
||||
|
||||
internal virtual void SetSteeringWheel(float yaw, float pitch)
|
||||
{
|
||||
SmoothYaw.Target = yaw;
|
||||
SmoothPitch.Target = pitch;
|
||||
}
|
||||
|
||||
internal virtual void SetArmPositions(Vector3 leftArmPosition, Vector3 rightArmPosition)
|
||||
{
|
||||
SmoothLeftArm.Target = leftArmPosition;
|
||||
SmoothRightArm.Target = rightArmPosition;
|
||||
}
|
||||
|
||||
internal virtual void Enter()
|
||||
{
|
||||
enabled = true;
|
||||
}
|
||||
|
||||
public virtual void Exit()
|
||||
{
|
||||
enabled = false;
|
||||
}
|
||||
|
||||
internal abstract void SetThrottle(bool isOn);
|
||||
}
|
||||
}
|
48
NitroxClient/MonoBehaviours/NitroxBootstrapper.cs
Normal file
48
NitroxClient/MonoBehaviours/NitroxBootstrapper.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using System.Diagnostics;
|
||||
using NitroxClient.MonoBehaviours.Discord;
|
||||
using NitroxClient.MonoBehaviours.Gui.MainMenu;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours;
|
||||
|
||||
public class NitroxBootstrapper : MonoBehaviour
|
||||
{
|
||||
internal static NitroxBootstrapper Instance;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
DontDestroyOnLoad(gameObject);
|
||||
Instance = this;
|
||||
gameObject.AddComponent<SceneCleanerPreserve>();
|
||||
gameObject.AddComponent<NitroxMainMenuModifications>();
|
||||
gameObject.AddComponent<DiscordClient>();
|
||||
|
||||
#if DEBUG
|
||||
EnableDeveloperFeatures();
|
||||
CreateDebugger();
|
||||
#endif
|
||||
|
||||
// This is very important, see Application_runInBackground_Patch.cs
|
||||
Application.runInBackground = true;
|
||||
Log.Info($"Unity run in background set to \"{Application.runInBackground}\"");
|
||||
// Also very important for similar reasons
|
||||
MiscSettings.pdaPause = false;
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
private static void EnableDeveloperFeatures()
|
||||
{
|
||||
Log.Info("Enabling Subnautica developer console");
|
||||
PlatformUtils.SetDevToolsEnabled(true);
|
||||
}
|
||||
|
||||
private void CreateDebugger()
|
||||
{
|
||||
Log.Info("Enabling Nitrox debugger");
|
||||
GameObject debugger = new();
|
||||
debugger.name = "Debug manager";
|
||||
debugger.AddComponent<NitroxDebugManager>();
|
||||
debugger.transform.SetParent(transform);
|
||||
}
|
||||
#endif
|
||||
}
|
183
NitroxClient/MonoBehaviours/NitroxDebugManager.cs
Normal file
183
NitroxClient/MonoBehaviours/NitroxDebugManager.cs
Normal file
@@ -0,0 +1,183 @@
|
||||
#if DEBUG
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using NitroxClient.Debuggers;
|
||||
using NitroxModel.Core;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class NitroxDebugManager : MonoBehaviour
|
||||
{
|
||||
private const KeyCode ENABLE_DEBUGGER_HOTKEY = KeyCode.F7;
|
||||
|
||||
private readonly HashSet<BaseDebugger> prevActiveDebuggers = [];
|
||||
private List<BaseDebugger> debuggers;
|
||||
|
||||
private bool showDebuggerList;
|
||||
private bool isDebugging;
|
||||
private Rect windowRect;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
debuggers = NitroxServiceLocator.LocateServicePreLifetime<IEnumerable<BaseDebugger>>().ToList();
|
||||
}
|
||||
|
||||
public static void ToggleCursor()
|
||||
{
|
||||
UWE.Utils.lockCursor = !UWE.Utils.lockCursor;
|
||||
}
|
||||
|
||||
public void OnGUI()
|
||||
{
|
||||
if (!isDebugging)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Main window to display all available debuggers.
|
||||
windowRect = GUILayout.Window(GUIUtility.GetControlID(FocusType.Keyboard), windowRect, DoWindow, "Nitrox debugging");
|
||||
|
||||
// Render debugger windows if they are enabled.
|
||||
foreach (BaseDebugger debugger in debuggers)
|
||||
{
|
||||
debugger.OnGUI();
|
||||
}
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
if (Input.GetKeyDown(ENABLE_DEBUGGER_HOTKEY))
|
||||
{
|
||||
ToggleDebugging();
|
||||
}
|
||||
|
||||
if (isDebugging)
|
||||
{
|
||||
if (Input.GetKeyDown(KeyCode.C) && Input.GetKey(KeyCode.LeftControl))
|
||||
{
|
||||
ToggleCursor();
|
||||
}
|
||||
|
||||
CheckDebuggerHotkeys();
|
||||
|
||||
foreach (BaseDebugger debugger in debuggers.Where(debugger => debugger.Enabled))
|
||||
{
|
||||
debugger.Update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ToggleDebugging()
|
||||
{
|
||||
isDebugging = !isDebugging;
|
||||
if (isDebugging)
|
||||
{
|
||||
UWE.Utils.PushLockCursor(false);
|
||||
ShowDebuggers();
|
||||
}
|
||||
else
|
||||
{
|
||||
UWE.Utils.PopLockCursor();
|
||||
HideDebuggers();
|
||||
foreach (BaseDebugger baseDebugger in debuggers)
|
||||
{
|
||||
baseDebugger.ResetWindowPosition();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DoWindow(int windowId)
|
||||
{
|
||||
using (new GUILayout.VerticalScope())
|
||||
{
|
||||
using (new GUILayout.HorizontalScope())
|
||||
{
|
||||
if (GUILayout.Button("Toggle cursor (CTRL+C)"))
|
||||
{
|
||||
ToggleCursor();
|
||||
}
|
||||
|
||||
if (GUILayout.Button("Show / Hide", GUILayout.Width(100)))
|
||||
{
|
||||
showDebuggerList = !showDebuggerList;
|
||||
windowRect = default;
|
||||
}
|
||||
}
|
||||
if (showDebuggerList)
|
||||
{
|
||||
foreach (BaseDebugger debugger in debuggers)
|
||||
{
|
||||
debugger.Enabled = GUILayout.Toggle(debugger.Enabled, $"{debugger.DebuggerName} debugger ({debugger.HotkeyString})");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckDebuggerHotkeys()
|
||||
{
|
||||
foreach (BaseDebugger debugger in debuggers)
|
||||
{
|
||||
if (Input.GetKeyDown(debugger.Hotkey) && Input.GetKey(KeyCode.LeftControl) == debugger.HotkeyControlRequired && Input.GetKey(KeyCode.LeftShift) == debugger.HotkeyShiftRequired && Input.GetKey(KeyCode.LeftAlt) == debugger.HotkeyAltRequired)
|
||||
{
|
||||
debugger.Enabled = !debugger.Enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HideDebuggers()
|
||||
{
|
||||
foreach (BaseDebugger debugger in GetComponents<BaseDebugger>())
|
||||
{
|
||||
if (debugger.Enabled)
|
||||
{
|
||||
prevActiveDebuggers.Add(debugger);
|
||||
}
|
||||
|
||||
debugger.Enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowDebuggers()
|
||||
{
|
||||
foreach (BaseDebugger debugger in prevActiveDebuggers)
|
||||
{
|
||||
debugger.Enabled = true;
|
||||
}
|
||||
|
||||
prevActiveDebuggers.Clear();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
SceneManager.sceneLoaded += SceneManager_sceneLoaded;
|
||||
SceneManager.sceneUnloaded += SceneManager_sceneUnloaded;
|
||||
SceneManager.activeSceneChanged += SceneManager_activeSceneChanged;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
SceneManager.sceneLoaded -= SceneManager_sceneLoaded;
|
||||
SceneManager.sceneUnloaded -= SceneManager_sceneUnloaded;
|
||||
SceneManager.activeSceneChanged -= SceneManager_activeSceneChanged;
|
||||
}
|
||||
|
||||
private static void SceneManager_sceneLoaded(Scene scene, LoadSceneMode loadMode)
|
||||
{
|
||||
Log.Debug($"Scene {scene.name} loaded as {loadMode}");
|
||||
}
|
||||
|
||||
private static void SceneManager_sceneUnloaded(Scene scene)
|
||||
{
|
||||
Log.Debug($"Scene {scene.name} unloaded.");
|
||||
}
|
||||
|
||||
private static void SceneManager_activeSceneChanged(Scene fromScene, Scene toScene)
|
||||
{
|
||||
Log.Debug($"Active scene changed from {fromScene.name} to {toScene.name}");
|
||||
}
|
||||
}
|
||||
#endif
|
174
NitroxClient/MonoBehaviours/NitroxEntity.cs
Normal file
174
NitroxClient/MonoBehaviours/NitroxEntity.cs
Normal file
@@ -0,0 +1,174 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.DataStructures.Util;
|
||||
using NitroxModel.Helper;
|
||||
using ProtoBuf;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours
|
||||
{
|
||||
[Serializable]
|
||||
[DataContract]
|
||||
[ProtoContract] // REQUIRED as the game serializes/deserializes phasing entities in batches when moving around the map.
|
||||
public class NitroxEntity : MonoBehaviour, IProtoTreeEventListener
|
||||
{
|
||||
private static readonly Dictionary<NitroxId, GameObject> gameObjectsById = new();
|
||||
|
||||
[DataMember(Order = 1)]
|
||||
[ProtoMember(1)]
|
||||
public NitroxId Id;
|
||||
|
||||
private NitroxEntity() // Default for Proto
|
||||
{
|
||||
}
|
||||
|
||||
public static IEnumerable<KeyValuePair<NitroxId, GameObject>> GetGameObjects()
|
||||
{
|
||||
return gameObjectsById;
|
||||
}
|
||||
|
||||
public static GameObject RequireObjectFrom(NitroxId id)
|
||||
{
|
||||
Optional<GameObject> gameObject = GetObjectFrom(id);
|
||||
Validate.IsPresent(gameObject, $"Game object required from id: {id}");
|
||||
return gameObject.Value;
|
||||
}
|
||||
|
||||
public static Optional<GameObject> GetObjectFrom(NitroxId id)
|
||||
{
|
||||
if (id == null)
|
||||
{
|
||||
return Optional.Empty;
|
||||
}
|
||||
|
||||
if (!gameObjectsById.TryGetValue(id, out GameObject gameObject))
|
||||
{
|
||||
return Optional.Empty;
|
||||
}
|
||||
|
||||
// Nullable incase game object is marked as destroyed
|
||||
return Optional.OfNullable(gameObject);
|
||||
}
|
||||
|
||||
public static Dictionary<NitroxId, GameObject> GetObjectsFrom(HashSet<NitroxId> ids)
|
||||
{
|
||||
return ids.Select(id => new KeyValuePair<NitroxId, GameObject>(id, gameObjectsById.GetOrDefault(id, null)))
|
||||
.Where(keyValue => keyValue.Value)
|
||||
.ToDictionary(kv => kv.Key, kv => kv.Value);
|
||||
}
|
||||
|
||||
public static bool TryGetObjectFrom(NitroxId id, out GameObject gameObject)
|
||||
{
|
||||
gameObject = null;
|
||||
return id != null && gameObjectsById.TryGetValue(id, out gameObject) && gameObject;
|
||||
}
|
||||
|
||||
public static bool TryGetComponentFrom<T>(NitroxId id, out T component)
|
||||
{
|
||||
component = default;
|
||||
return id != null && gameObjectsById.TryGetValue(id, out GameObject gameObject) && gameObject &&
|
||||
gameObject.TryGetComponent(out component);
|
||||
}
|
||||
|
||||
public static void SetNewId(GameObject gameObject, NitroxId id)
|
||||
{
|
||||
Validate.IsTrue(gameObject);
|
||||
Validate.NotNull(id);
|
||||
|
||||
if (gameObject.TryGetComponent(out NitroxEntity entity))
|
||||
{
|
||||
gameObjectsById.Remove(entity.Id);
|
||||
UniqueIdentifier.identifiers.Remove(entity.Id.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
entity = gameObject.AddComponent<NitroxEntity>();
|
||||
}
|
||||
entity.Id = id;
|
||||
gameObjectsById[id] = gameObject;
|
||||
|
||||
if (gameObject.TryGetComponent(out UniqueIdentifier uniqueIdentifier))
|
||||
{
|
||||
// To avoid unrequired error spams, we do the id setting manually
|
||||
// If the current UID was already registered, we unregister it
|
||||
if (!string.IsNullOrEmpty(uniqueIdentifier.id))
|
||||
{
|
||||
UniqueIdentifier.identifiers.Remove(uniqueIdentifier.id);
|
||||
}
|
||||
|
||||
uniqueIdentifier.id = id.ToString();
|
||||
UniqueIdentifier.identifiers[id.ToString()] = uniqueIdentifier;
|
||||
}
|
||||
}
|
||||
|
||||
public static NitroxId GenerateNewId(GameObject gameObject)
|
||||
{
|
||||
Validate.IsTrue(gameObject);
|
||||
|
||||
NitroxId id = new();
|
||||
SetNewId(gameObject, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
public static NitroxId GetIdOrGenerateNew(GameObject gameObject)
|
||||
{
|
||||
Validate.IsTrue(gameObject);
|
||||
|
||||
if (gameObject.TryGetComponent(out NitroxEntity entity))
|
||||
{
|
||||
return entity.Id;
|
||||
}
|
||||
|
||||
NitroxId id = new();
|
||||
SetNewId(gameObject, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
public static void RemoveFrom(GameObject gameObject)
|
||||
{
|
||||
if (gameObject.TryGetComponent(out NitroxEntity entity) && entity.Id != null)
|
||||
{
|
||||
gameObjectsById.Remove(entity.Id);
|
||||
DestroyImmediate(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the <see cref="NitroxEntity"/> from the global directory and set's its <see cref="Id"/> to null.
|
||||
/// </summary>
|
||||
public void Remove()
|
||||
{
|
||||
if (Id != null)
|
||||
{
|
||||
gameObjectsById.Remove(Id);
|
||||
Id = null;
|
||||
if (gameObject.TryGetComponent(out UniqueIdentifier uniqueIdentifier))
|
||||
{
|
||||
uniqueIdentifier.Unregister();
|
||||
uniqueIdentifier.id = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
// Just in case this object comes to life via serialization
|
||||
if (Id != null)
|
||||
{
|
||||
gameObjectsById[Id] = gameObject;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnProtoSerializeObjectTree(ProtobufSerializer _)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnProtoDeserializeObjectTree(ProtobufSerializer _)
|
||||
{
|
||||
gameObjectsById[Id] = gameObject;
|
||||
}
|
||||
}
|
||||
}
|
55
NitroxClient/MonoBehaviours/NitroxGeyser.cs
Normal file
55
NitroxClient/MonoBehaviours/NitroxGeyser.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using NitroxModel.DataStructures.GameLogic.Entities;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="Geyser"/> works with <see cref="MonoBehaviour.InvokeRepeating"/> which is based on <see cref="Time"/>.
|
||||
/// Therefore, to ensure client freeze (and other things modifying the local unity's time) don't disturb the precise geyser's erupt schedule,
|
||||
/// we manage it ourselves in a synced way.
|
||||
/// </summary>
|
||||
public class NitroxGeyser : MonoBehaviour
|
||||
{
|
||||
private Geyser geyser;
|
||||
|
||||
private float lastEruptTime;
|
||||
private float eruptInterval;
|
||||
|
||||
public void Initialize(GeyserWorldEntity geyserEntity, Geyser geyser)
|
||||
{
|
||||
if (!DayNightCycle.main)
|
||||
{
|
||||
Log.Error($"Can't initialize {nameof(NitroxGeyser)} without {nameof(DayNightCycle)} being initialized");
|
||||
Destroy(this);
|
||||
return;
|
||||
}
|
||||
this.geyser = geyser;
|
||||
|
||||
eruptInterval = geyser.eruptionInterval + geyserEntity.RandomIntervalVarianceMultiplier * geyser.eruptionIntervalVariance;
|
||||
float timePassed = DayNightCycle.main.timePassedAsFloat;
|
||||
float timeSinceLastErupt = (timePassed - geyserEntity.StartEruptTime) % eruptInterval;
|
||||
lastEruptTime = timePassed - timeSinceLastErupt;
|
||||
|
||||
geyser.CancelInvoke(nameof(Geyser.Erupt));
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
if (!DayNightCycle.main)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int eruptOccurrences = (int)((DayNightCycle.main.timePassedAsFloat - lastEruptTime) / eruptInterval);
|
||||
if (eruptOccurrences > 0)
|
||||
{
|
||||
lastEruptTime += eruptOccurrences * eruptInterval;
|
||||
if (geyser.erupting)
|
||||
{
|
||||
geyser.CancelInvoke(nameof(Geyser.EndErupt));
|
||||
geyser.EndErupt();
|
||||
}
|
||||
geyser.Erupt();
|
||||
}
|
||||
}
|
||||
}
|
30
NitroxClient/MonoBehaviours/OutOfCellEntity.cs
Normal file
30
NitroxClient/MonoBehaviours/OutOfCellEntity.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using NitroxClient.Communication.Abstract;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.Packets;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours;
|
||||
|
||||
/// <summary>
|
||||
/// Entities might move slightly out of the loaded zone, in which case the server thinks that they're in another cell
|
||||
/// (because the cell is only determined by the entity's position). Thus we need to be able to know when this entity is unloaded
|
||||
/// and broadcast this event so the server can switch the ownership from it.
|
||||
/// </summary>
|
||||
public class OutOfCellEntity : MonoBehaviour
|
||||
{
|
||||
private NitroxId entityId;
|
||||
|
||||
public void Init(NitroxId nitroxId)
|
||||
{
|
||||
if (entityId == null)
|
||||
{
|
||||
this.Resolve<IPacketSender>().Send(new PlayerSeeOutOfCellEntity(nitroxId));
|
||||
}
|
||||
entityId = nitroxId;
|
||||
}
|
||||
|
||||
public void OnDestroy()
|
||||
{
|
||||
this.Resolve<IPacketSender>().Send(new PlayerUnseeOutOfCellEntity(entityId));
|
||||
}
|
||||
}
|
71
NitroxClient/MonoBehaviours/Overrides/MultiplayerBench.cs
Normal file
71
NitroxClient/MonoBehaviours/Overrides/MultiplayerBench.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.Overrides
|
||||
{
|
||||
public class MultiplayerBench : Bench
|
||||
{
|
||||
private Side side;
|
||||
|
||||
public static MultiplayerBench FromBench(Bench origin, GameObject target, Side side, GameObject animatorRoot)
|
||||
{
|
||||
Animator animator = animatorRoot.GetComponent<Animator>();
|
||||
Transform playerTarget = animatorRoot.transform.Find("root/cine_loc/player_target");
|
||||
Transform playerOutTarget = animatorRoot.transform.Find("out_target");
|
||||
|
||||
MultiplayerBench bench = target.AddComponent<MultiplayerBench>();
|
||||
bench.frontObstacleCheck = origin.frontObstacleCheck;
|
||||
bench.backObstacleCheck = origin.backObstacleCheck;
|
||||
bench.frontAnimRotation = origin.frontAnimRotation;
|
||||
bench.backAnimRotation = origin.backAnimRotation;
|
||||
bench.checkDistance = origin.checkDistance;
|
||||
bench.handText = origin.handText;
|
||||
bench.triggerType = origin.triggerType;
|
||||
bench.volumeTriggerType = origin.volumeTriggerType;
|
||||
bench.standUpCinematicController = origin.standUpCinematicController;
|
||||
bench.cinematicController = origin.cinematicController;
|
||||
|
||||
bench.onCinematicStart = new CinematicModeEvent();
|
||||
bench.onCinematicEnd = new CinematicModeEvent();
|
||||
bench.side = side;
|
||||
|
||||
bench.animator = animator;
|
||||
bench.playerTarget = playerTarget;
|
||||
bench.cinematicController.animatedTransform = playerTarget;
|
||||
bench.cinematicController.animator = animator;
|
||||
bench.cinematicController.informGameObject = target;
|
||||
bench.standUpCinematicController.animatedTransform = playerTarget;
|
||||
bench.standUpCinematicController.endTransform = playerOutTarget;
|
||||
bench.standUpCinematicController.animator = animator;
|
||||
bench.standUpCinematicController.informGameObject = target;
|
||||
|
||||
return bench;
|
||||
}
|
||||
|
||||
public override void OnHandClick(GUIHand hand)
|
||||
{
|
||||
// Prevent users from sitting on a not fully-constructed bench
|
||||
if (gameObject.TryGetComponentInParent(out Constructable constructable, true) && !constructable.constructed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
standUpCinematicController.transform.localPosition = side switch
|
||||
{
|
||||
Side.LEFT => new Vector3(-0.75f, 0.082f, 0),
|
||||
Side.CENTER => new Vector3(0, 0.082f, 0),
|
||||
Side.RIGHT => new Vector3(0.75f, 0.082f, 0),
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
|
||||
base.OnHandClick(hand);
|
||||
}
|
||||
|
||||
public enum Side
|
||||
{
|
||||
LEFT,
|
||||
CENTER,
|
||||
RIGHT
|
||||
}
|
||||
}
|
||||
}
|
26
NitroxClient/MonoBehaviours/PlayerDeathBroadcaster.cs
Normal file
26
NitroxClient/MonoBehaviours/PlayerDeathBroadcaster.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using NitroxClient.GameLogic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours;
|
||||
|
||||
public class PlayerDeathBroadcaster : MonoBehaviour
|
||||
{
|
||||
private LocalPlayer localPlayer;
|
||||
|
||||
public void Awake()
|
||||
{
|
||||
localPlayer = this.Resolve<LocalPlayer>();
|
||||
|
||||
Player.main.playerDeathEvent.AddHandler(this, PlayerDeath);
|
||||
}
|
||||
|
||||
private void PlayerDeath(Player player)
|
||||
{
|
||||
localPlayer.BroadcastDeath(player.transform.position);
|
||||
}
|
||||
|
||||
public void OnDestroy()
|
||||
{
|
||||
Player.main.playerDeathEvent.RemoveHandler(this, PlayerDeath);
|
||||
}
|
||||
}
|
74
NitroxClient/MonoBehaviours/PlayerMovementBroadcaster.cs
Normal file
74
NitroxClient/MonoBehaviours/PlayerMovementBroadcaster.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using NitroxClient.Communication.Abstract;
|
||||
using NitroxClient.GameLogic;
|
||||
using NitroxModel.Packets;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours;
|
||||
|
||||
public class PlayerMovementBroadcaster : MonoBehaviour
|
||||
{
|
||||
private LocalPlayer localPlayer;
|
||||
|
||||
public void Awake()
|
||||
{
|
||||
localPlayer = this.Resolve<LocalPlayer>();
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
// Freecam does disable main camera control
|
||||
// But it's also disabled when driving the cyclops through a cyclops camera (content.activeSelf is only true when controlling through a cyclops camera)
|
||||
if (!MainCameraControl.main.isActiveAndEnabled &&
|
||||
!uGUI_CameraCyclops.main.content.activeSelf)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (BroadcastPlayerInCyclopsMovement())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Player.main.isPiloting)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 currentPosition = Player.main.transform.position;
|
||||
Vector3 playerVelocity = Player.main.playerController.velocity;
|
||||
|
||||
// IDEA: possibly only CameraRotation is of interest, because bodyrotation is extracted from that.
|
||||
Quaternion bodyRotation = MainCameraControl.main.viewModel.transform.rotation;
|
||||
Quaternion aimingRotation = Player.main.camRoot.GetAimingTransform().rotation;
|
||||
|
||||
SubRoot subRoot = Player.main.GetCurrentSub();
|
||||
|
||||
// If in a subroot the position will be relative to the subroot
|
||||
if (subRoot)
|
||||
{
|
||||
// Rotate relative player position relative to the subroot (else there are problems with respawning)
|
||||
Transform subRootTransform = subRoot.transform;
|
||||
Quaternion undoVehicleAngle = subRootTransform.rotation.GetInverse();
|
||||
currentPosition = currentPosition - subRootTransform.position;
|
||||
currentPosition = undoVehicleAngle * currentPosition;
|
||||
bodyRotation = undoVehicleAngle * bodyRotation;
|
||||
aimingRotation = undoVehicleAngle * aimingRotation;
|
||||
currentPosition = subRootTransform.TransformPoint(currentPosition);
|
||||
}
|
||||
|
||||
localPlayer.BroadcastLocation(currentPosition, playerVelocity, bodyRotation, aimingRotation);
|
||||
}
|
||||
|
||||
private bool BroadcastPlayerInCyclopsMovement()
|
||||
{
|
||||
if (!Player.main.isPiloting && Player.main.TryGetComponent(out CyclopsMotor cyclopsMotor) && cyclopsMotor.Pawn != null)
|
||||
{
|
||||
Transform pawnTransform = cyclopsMotor.Pawn.Handle.transform;
|
||||
PlayerInCyclopsMovement packet = new(this.Resolve<LocalPlayer>().PlayerId.Value, pawnTransform.localPosition.ToDto(), pawnTransform.localRotation.ToDto());
|
||||
this.Resolve<IPacketSender>().Send(packet);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
42
NitroxClient/MonoBehaviours/PlayerStatsBroadcaster.cs
Normal file
42
NitroxClient/MonoBehaviours/PlayerStatsBroadcaster.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using NitroxClient.GameLogic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours;
|
||||
|
||||
public class PlayerStatsBroadcaster : MonoBehaviour
|
||||
{
|
||||
private float time;
|
||||
private const float BROADCAST_INTERVAL = 3f;
|
||||
private LocalPlayer localPlayer;
|
||||
private Survival survival;
|
||||
|
||||
public void Awake()
|
||||
{
|
||||
localPlayer = this.Resolve<LocalPlayer>();
|
||||
survival = Player.main.AliveOrNull()?.GetComponent<Survival>();
|
||||
if (!survival)
|
||||
{
|
||||
Log.Error($"Couldn't find the {nameof(Survival)} instance on the main {nameof(Player)} instance. Destroying {nameof(PlayerStatsBroadcaster)}");
|
||||
Destroy(this);
|
||||
}
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
time += Time.deltaTime;
|
||||
|
||||
// Only do on a specific cadence to avoid hammering server
|
||||
if (time >= BROADCAST_INTERVAL)
|
||||
{
|
||||
time = 0;
|
||||
|
||||
float oxygen = Player.main.oxygenMgr.GetOxygenAvailable();
|
||||
float maxOxygen = Player.main.oxygenMgr.GetOxygenCapacity();
|
||||
float health = Player.main.liveMixin.health;
|
||||
float food = survival.food;
|
||||
float water = survival.water;
|
||||
float infectionAmount = Player.main.infectedMixin.GetInfectedAmount();
|
||||
localPlayer.BroadcastStats(oxygen, maxOxygen, health, food, water, infectionAmount);
|
||||
}
|
||||
}
|
||||
}
|
27
NitroxClient/MonoBehaviours/ReferenceHolder.cs
Normal file
27
NitroxClient/MonoBehaviours/ReferenceHolder.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours;
|
||||
|
||||
public class ReferenceHolder : MonoBehaviour
|
||||
{
|
||||
private readonly Dictionary<Type, object> references = [];
|
||||
|
||||
public bool TryGetReference<T>(out T outReference)
|
||||
{
|
||||
if (references.TryGetValue(typeof(T), out object value) && value is T reference)
|
||||
{
|
||||
outReference = reference;
|
||||
return true;
|
||||
}
|
||||
|
||||
outReference = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void AddReference<T>(T reference)
|
||||
{
|
||||
references[typeof(T)] = reference;
|
||||
}
|
||||
}
|
86
NitroxClient/MonoBehaviours/RemotelyControlled.cs
Normal file
86
NitroxClient/MonoBehaviours/RemotelyControlled.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using NitroxClient.GameLogic;
|
||||
using NitroxClient.Unity.Smoothing;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours;
|
||||
|
||||
public class RemotelyControlled : MonoBehaviour
|
||||
{
|
||||
private readonly SmoothVector smoothPosition = new SmoothVector();
|
||||
private readonly SmoothRotation smoothRotation = new SmoothRotation();
|
||||
|
||||
private SwimBehaviour swimBehaviour;
|
||||
private WalkBehaviour walkBehaviour;
|
||||
private Rigidbody rigidbody;
|
||||
|
||||
public void Awake()
|
||||
{
|
||||
swimBehaviour = gameObject.GetComponent<SwimBehaviour>();
|
||||
walkBehaviour = gameObject.GetComponent<WalkBehaviour>();
|
||||
rigidbody = gameObject.GetComponent<Rigidbody>();
|
||||
}
|
||||
|
||||
public void FixedUpdate()
|
||||
{
|
||||
if (swimBehaviour || walkBehaviour || !rigidbody)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
smoothPosition.FixedUpdate();
|
||||
smoothRotation.FixedUpdate();
|
||||
|
||||
rigidbody.isKinematic = false;
|
||||
rigidbody.velocity = MovementHelper.GetCorrectedVelocity(smoothPosition.Current, Vector3.zero, gameObject, EntityPositionBroadcaster.BROADCAST_INTERVAL);
|
||||
rigidbody.angularVelocity = MovementHelper.GetCorrectedAngularVelocity(smoothRotation.Current, Vector3.zero, gameObject, EntityPositionBroadcaster.BROADCAST_INTERVAL);
|
||||
}
|
||||
|
||||
public void UpdateOrientation(Vector3 position, Quaternion rotation)
|
||||
{
|
||||
TeleportIfTooFar(position, rotation);
|
||||
|
||||
if (swimBehaviour)
|
||||
{
|
||||
swimBehaviour.SwimTo(position, 3f);
|
||||
}
|
||||
|
||||
Transform selfTransform = transform;
|
||||
|
||||
// Entities can lose their swimBehavior (such as if they get killed). Keep these up-to-date incase that happens.
|
||||
smoothPosition.Current = selfTransform.position;
|
||||
smoothRotation.Current = selfTransform.rotation;
|
||||
smoothPosition.Target = position;
|
||||
smoothRotation.Target = rotation;
|
||||
}
|
||||
|
||||
public void UpdateKnownSplineUser(Vector3 currentPosition, Quaternion currentRotation, Vector3 destination, Vector3 destinationDirection, float velocity)
|
||||
{
|
||||
TeleportIfTooFar(currentPosition, currentRotation);
|
||||
|
||||
if (swimBehaviour)
|
||||
{
|
||||
// First lines of SwimBehaviour.SwimToInternal
|
||||
swimBehaviour.originalTargetPosition = destination;
|
||||
swimBehaviour.originalTargetDirection = destinationDirection;
|
||||
swimBehaviour.originalVelocity = velocity;
|
||||
// Only the useful part of the methods called in SwimBehaviour.SwimToInternal
|
||||
swimBehaviour.splineFollowing.GoTo(destination, destinationDirection, velocity);
|
||||
}
|
||||
|
||||
if (walkBehaviour)
|
||||
{
|
||||
walkBehaviour.GoToInternal(destination, destinationDirection, velocity);
|
||||
}
|
||||
}
|
||||
|
||||
private void TeleportIfTooFar(Vector3 position, Quaternion rotation)
|
||||
{
|
||||
Transform selfTransform = transform;
|
||||
|
||||
if ((selfTransform.position - position).sqrMagnitude > 25) // Optimized 5m distance test
|
||||
{
|
||||
selfTransform.position = position;
|
||||
selfTransform.rotation = rotation;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,91 @@
|
||||
using NitroxClient.GameLogic;
|
||||
using NitroxModel.Packets;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.Vehicles;
|
||||
|
||||
public class CyclopsMovementReplicator : VehicleMovementReplicator
|
||||
{
|
||||
protected static readonly int CYCLOPS_YAW = Animator.StringToHash("cyclops_yaw");
|
||||
protected static readonly int CYCLOPS_PITCH = Animator.StringToHash("cyclops_pitch");
|
||||
|
||||
private SubControl subControl;
|
||||
|
||||
private RemotePlayer drivingPlayer;
|
||||
private bool throttleApplied;
|
||||
private float steeringWheelYaw;
|
||||
|
||||
public void Awake()
|
||||
{
|
||||
subControl = GetComponent<SubControl>();
|
||||
}
|
||||
|
||||
public new void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (subControl.canAccel && throttleApplied)
|
||||
{
|
||||
// See SubControl.Update
|
||||
var topClamp = subControl.useThrottleIndex switch
|
||||
{
|
||||
1 => 0.66f,
|
||||
2 => 1f,
|
||||
_ => 0.33f,
|
||||
};
|
||||
subControl.engineRPMManager.AccelerateInput(topClamp);
|
||||
for (int i = 0; i < subControl.throttleHandlers.Length; i++)
|
||||
{
|
||||
subControl.throttleHandlers[i].OnSubAppliedThrottle();
|
||||
}
|
||||
}
|
||||
|
||||
if (Mathf.Abs(steeringWheelYaw) > 0.1f)
|
||||
{
|
||||
ShipSide shipSide = steeringWheelYaw > 0 ? ShipSide.Port : ShipSide.Starboard;
|
||||
for (int i = 0; i < subControl.turnHandlers.Length; i++)
|
||||
{
|
||||
subControl.turnHandlers[i].OnSubTurn(shipSide);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void ApplyNewMovementData(MovementData newMovementData)
|
||||
{
|
||||
if (newMovementData is not DrivenVehicleMovementData vehicleMovementData)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
steeringWheelYaw = vehicleMovementData.SteeringWheelYaw;
|
||||
float steeringWheelPitch = vehicleMovementData.SteeringWheelPitch;
|
||||
|
||||
// See SubControl.UpdateAnimation
|
||||
subControl.steeringWheelYaw = steeringWheelYaw;
|
||||
subControl.steeringWheelPitch = steeringWheelPitch;
|
||||
if (subControl.mainAnimator)
|
||||
{
|
||||
subControl.mainAnimator.SetFloat(VIEW_YAW, subControl.steeringWheelYaw);
|
||||
subControl.mainAnimator.SetFloat(VIEW_PITCH, subControl.steeringWheelPitch);
|
||||
|
||||
if (drivingPlayer != null)
|
||||
{
|
||||
drivingPlayer.AnimationController.SetFloat(CYCLOPS_YAW, subControl.steeringWheelYaw);
|
||||
drivingPlayer.AnimationController.SetFloat(CYCLOPS_PITCH, subControl.steeringWheelPitch);
|
||||
}
|
||||
}
|
||||
|
||||
throttleApplied = vehicleMovementData.ThrottleApplied;
|
||||
}
|
||||
|
||||
public override void Enter(RemotePlayer drivingPlayer)
|
||||
{
|
||||
this.drivingPlayer = drivingPlayer;
|
||||
}
|
||||
|
||||
public override void Exit()
|
||||
{
|
||||
drivingPlayer = null;
|
||||
throttleApplied = false;
|
||||
}
|
||||
}
|
@@ -0,0 +1,134 @@
|
||||
using FMOD.Studio;
|
||||
using NitroxClient.GameLogic;
|
||||
using NitroxClient.GameLogic.FMOD;
|
||||
using NitroxModel.GameLogic.FMOD;
|
||||
using NitroxModel.Packets;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.Vehicles;
|
||||
|
||||
public class ExosuitMovementReplicator : VehicleMovementReplicator
|
||||
{
|
||||
private Exosuit exosuit;
|
||||
|
||||
public Vector3 velocity;
|
||||
private float jetLoopingSoundDistance;
|
||||
private float thrustPower;
|
||||
private bool jetsActive;
|
||||
private float timeJetsActiveChanged;
|
||||
|
||||
public void Awake()
|
||||
{
|
||||
exosuit = GetComponent<Exosuit>();
|
||||
SetupSound();
|
||||
}
|
||||
|
||||
public new void Update()
|
||||
{
|
||||
Vector3 positionBefore = transform.position;
|
||||
base.Update();
|
||||
Vector3 positionAfter = transform.position;
|
||||
|
||||
velocity = (positionAfter - positionBefore) / Time.deltaTime;
|
||||
|
||||
|
||||
float volume = FMODSystem.CalculateVolume(transform.position, Player.main.transform.position, jetLoopingSoundDistance, 1f);
|
||||
EventInstance soundHandle = exosuit.loopingJetSound.playing ? exosuit.loopingJetSound.evt : exosuit.loopingJetSound.evtStop;
|
||||
if (soundHandle.hasHandle())
|
||||
{
|
||||
soundHandle.setVolume(volume);
|
||||
}
|
||||
|
||||
// See Exosuit.Update, thrust power simulation
|
||||
|
||||
if (jetsActive)
|
||||
{
|
||||
thrustPower = Mathf.Clamp01(thrustPower - Time.deltaTime * exosuit.thrustConsumption);
|
||||
exosuit.thrustIntensity += Time.deltaTime / exosuit.timeForFullVirbation;
|
||||
}
|
||||
else
|
||||
{
|
||||
float num = Time.deltaTime * exosuit.thrustConsumption * 0.7f;
|
||||
if (exosuit.onGround)
|
||||
{
|
||||
num = Time.deltaTime * exosuit.thrustConsumption * 4f;
|
||||
}
|
||||
thrustPower = Mathf.Clamp01(thrustPower + num);
|
||||
exosuit.thrustIntensity -= Time.deltaTime * 10f;
|
||||
}
|
||||
exosuit.thrustIntensity = Mathf.Clamp01(exosuit.thrustIntensity);
|
||||
|
||||
if (timeJetsActiveChanged + 0.3f <= Time.time)
|
||||
{
|
||||
if (jetsActive && thrustPower > 0f)
|
||||
{
|
||||
exosuit.loopingJetSound.Play();
|
||||
exosuit.fxcontrol.Play(0);
|
||||
exosuit.areFXPlaying = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
exosuit.loopingJetSound.Stop();
|
||||
exosuit.fxcontrol.Stop(0);
|
||||
exosuit.areFXPlaying = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void ApplyNewMovementData(MovementData newMovementData)
|
||||
{
|
||||
if (newMovementData is not DrivenVehicleMovementData vehicleMovementData)
|
||||
{
|
||||
return;
|
||||
}
|
||||
float steeringWheelYaw = vehicleMovementData.SteeringWheelYaw;
|
||||
float steeringWheelPitch = vehicleMovementData.SteeringWheelPitch;
|
||||
|
||||
// See Vehicle.Update (reverse operation for vehicle.steeringWheel... = ...)
|
||||
exosuit.steeringWheelYaw = steeringWheelPitch / 70f;
|
||||
exosuit.steeringWheelPitch = steeringWheelPitch / 45f;
|
||||
|
||||
if (exosuit.mainAnimator)
|
||||
{
|
||||
exosuit.mainAnimator.SetFloat(VIEW_YAW, steeringWheelYaw);
|
||||
exosuit.mainAnimator.SetFloat(VIEW_PITCH, steeringWheelPitch);
|
||||
}
|
||||
|
||||
// See Exosuit.jetsActive setter
|
||||
if (jetsActive != vehicleMovementData.ThrottleApplied)
|
||||
{
|
||||
jetsActive = vehicleMovementData.ThrottleApplied;
|
||||
timeJetsActiveChanged = Time.time;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupSound()
|
||||
{
|
||||
this.Resolve<FMODWhitelist>().TryGetSoundData(exosuit.loopingJetSound.asset.path, out SoundData jetSoundData);
|
||||
jetLoopingSoundDistance = jetSoundData.Radius;
|
||||
|
||||
if (FMODUWE.IsInvalidParameterId(exosuit.fmodIndexSpeed))
|
||||
{
|
||||
exosuit.fmodIndexSpeed = exosuit.ambienceSound.GetParameterIndex("speed");
|
||||
}
|
||||
if (FMODUWE.IsInvalidParameterId(exosuit.fmodIndexRotate))
|
||||
{
|
||||
exosuit.fmodIndexRotate = exosuit.ambienceSound.GetParameterIndex("rotate");
|
||||
}
|
||||
}
|
||||
|
||||
public override void Enter(RemotePlayer remotePlayer)
|
||||
{
|
||||
exosuit.SetIKEnabled(true);
|
||||
exosuit.thrustIntensity = 0;
|
||||
}
|
||||
|
||||
public override void Exit()
|
||||
{
|
||||
exosuit.SetIKEnabled(false);
|
||||
exosuit.loopingJetSound.Stop(STOP_MODE.ALLOWFADEOUT);
|
||||
exosuit.fxcontrol.Stop(0);
|
||||
|
||||
jetsActive = false;
|
||||
}
|
||||
}
|
@@ -0,0 +1,120 @@
|
||||
using FMOD.Studio;
|
||||
using NitroxClient.GameLogic;
|
||||
using NitroxModel.GameLogic.FMOD;
|
||||
using NitroxModel.Packets;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.Vehicles;
|
||||
|
||||
public class SeamothMovementReplicator : VehicleMovementReplicator
|
||||
{
|
||||
private SeaMoth seaMoth;
|
||||
|
||||
private FMOD_CustomLoopingEmitter rpmSound;
|
||||
private FMOD_CustomEmitter revSound;
|
||||
private FMOD_CustomEmitter enterSeamoth;
|
||||
|
||||
private float radiusRpmSound;
|
||||
private float radiusRevSound;
|
||||
private float radiusEnterSound;
|
||||
|
||||
private bool throttleApplied;
|
||||
|
||||
public void Awake()
|
||||
{
|
||||
seaMoth = GetComponent<SeaMoth>();
|
||||
SetupSound();
|
||||
}
|
||||
|
||||
public new void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (throttleApplied)
|
||||
{
|
||||
seaMoth.engineSound.AccelerateInput(1);
|
||||
}
|
||||
}
|
||||
|
||||
public override void ApplyNewMovementData(MovementData newMovementData)
|
||||
{
|
||||
if (newMovementData is not DrivenVehicleMovementData vehicleMovementData)
|
||||
{
|
||||
return;
|
||||
}
|
||||
float steeringWheelYaw = vehicleMovementData.SteeringWheelYaw;
|
||||
float steeringWheelPitch = vehicleMovementData.SteeringWheelPitch;
|
||||
|
||||
// See Vehicle.Update (reverse operation for vehicle.steeringWheel... = ...)
|
||||
seaMoth.steeringWheelYaw = steeringWheelYaw / 70f;
|
||||
seaMoth.steeringWheelPitch = steeringWheelPitch / 45f;
|
||||
|
||||
if (seaMoth.mainAnimator)
|
||||
{
|
||||
seaMoth.mainAnimator.SetFloat(VIEW_YAW, steeringWheelYaw);
|
||||
seaMoth.mainAnimator.SetFloat(VIEW_PITCH, steeringWheelPitch);
|
||||
}
|
||||
|
||||
// Adjusting volume for the engine Sound
|
||||
float distanceToPlayer = Vector3.Distance(Player.main.transform.position, transform.position);
|
||||
float volumeRpmSound = SoundHelper.CalculateVolume(distanceToPlayer, radiusRpmSound, 1f);
|
||||
float volumeRevSound = SoundHelper.CalculateVolume(distanceToPlayer, radiusRevSound, 1f);
|
||||
rpmSound.GetEventInstance().setVolume(volumeRpmSound);
|
||||
revSound.GetEventInstance().setVolume(volumeRevSound);
|
||||
|
||||
throttleApplied = vehicleMovementData.ThrottleApplied;
|
||||
}
|
||||
|
||||
private void SetupSound()
|
||||
{
|
||||
rpmSound = seaMoth.engineSound.engineRpmSFX;
|
||||
revSound = seaMoth.engineSound.engineRevUp;
|
||||
enterSeamoth = seaMoth.enterSeamoth;
|
||||
|
||||
rpmSound.followParent = true;
|
||||
revSound.followParent = true;
|
||||
|
||||
this.Resolve<FMODWhitelist>().IsWhitelisted(rpmSound.asset.path, out radiusRpmSound);
|
||||
this.Resolve<FMODWhitelist>().IsWhitelisted(revSound.asset.path, out radiusRevSound);
|
||||
this.Resolve<FMODWhitelist>().IsWhitelisted(seaMoth.enterSeamoth.asset.path, out radiusEnterSound);
|
||||
|
||||
rpmSound.GetEventInstance().setProperty(EVENT_PROPERTY.MINIMUM_DISTANCE, 1f);
|
||||
revSound.GetEventInstance().setProperty(EVENT_PROPERTY.MINIMUM_DISTANCE, 1f);
|
||||
enterSeamoth.GetEventInstance().setProperty(EVENT_PROPERTY.MINIMUM_DISTANCE, 1f);
|
||||
|
||||
rpmSound.GetEventInstance().setProperty(EVENT_PROPERTY.MAXIMUM_DISTANCE, radiusRpmSound);
|
||||
revSound.GetEventInstance().setProperty(EVENT_PROPERTY.MAXIMUM_DISTANCE, radiusRevSound);
|
||||
enterSeamoth.GetEventInstance().setProperty(EVENT_PROPERTY.MAXIMUM_DISTANCE, radiusEnterSound);
|
||||
|
||||
|
||||
if (FMODUWE.IsInvalidParameterId(seaMoth.fmodIndexSpeed))
|
||||
{
|
||||
seaMoth.fmodIndexSpeed = seaMoth.ambienceSound.GetParameterIndex("speed");
|
||||
}
|
||||
}
|
||||
|
||||
public override void Enter(RemotePlayer remotePlayer)
|
||||
{
|
||||
seaMoth.bubbles.Play();
|
||||
if (enterSeamoth)
|
||||
{
|
||||
// After first run, this sound will still be in "playing" mode so we need to release it by hand
|
||||
enterSeamoth.Stop();
|
||||
enterSeamoth.ReleaseEvent();
|
||||
enterSeamoth.CacheEventInstance();
|
||||
|
||||
float distanceToPlayer = Vector3.Distance(Player.main.transform.position, transform.position);
|
||||
float sound = SoundHelper.CalculateVolume(distanceToPlayer, radiusEnterSound, 1f);
|
||||
enterSeamoth.evt.setVolume(sound);
|
||||
|
||||
enterSeamoth.Play();
|
||||
}
|
||||
}
|
||||
|
||||
public override void Exit()
|
||||
{
|
||||
seaMoth.bubbles.Stop();
|
||||
|
||||
throttleApplied = false;
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
using NitroxClient.GameLogic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.Vehicles;
|
||||
|
||||
public abstract class VehicleMovementReplicator : MovementReplicator
|
||||
{
|
||||
protected static readonly int VIEW_YAW = Animator.StringToHash("view_yaw");
|
||||
protected static readonly int VIEW_PITCH = Animator.StringToHash("view_pitch");
|
||||
|
||||
public abstract void Enter(RemotePlayer remotePlayer);
|
||||
public abstract void Exit();
|
||||
}
|
147
NitroxClient/MonoBehaviours/Vehicles/WatchedEntry.cs
Normal file
147
NitroxClient/MonoBehaviours/Vehicles/WatchedEntry.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.Packets;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.MonoBehaviours.Vehicles;
|
||||
|
||||
public class WatchedEntry
|
||||
{
|
||||
/// <remarks>
|
||||
/// In unity position units. Refer to <see cref="ShouldBroadcastMovement"/> for use infos.
|
||||
/// </remarks>
|
||||
private const float MINIMAL_MOVEMENT_TRESHOLD = 0.05f;
|
||||
/// <remarks>
|
||||
/// In degrees (°). Refer to <see cref="ShouldBroadcastMovement"/> for use infos.
|
||||
/// </remarks>
|
||||
private const float MINIMAL_ROTATION_TRESHOLD = 0.05f;
|
||||
/// <remarks>
|
||||
/// In seconds. Refer to <see cref="ShouldBroadcastMovement"/> for use infos.
|
||||
/// </remarks>
|
||||
private const float MAX_TIME_WITHOUT_BROADCAST = 5f;
|
||||
/// <inheritdoc cref="MAX_TIME_WITHOUT_BROADCAST"/>
|
||||
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<Vehicle>();
|
||||
subControl = transform.GetComponent<SubControl>();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="MINIMAL_MOVEMENT_TRESHOLD"/> are ignored
|
||||
/// - rotation changes less than <see cref="MINIMAL_ROTATION_TRESHOLD"/> are ignored
|
||||
/// - every period of <see cref="MAX_TIME_WITHOUT_BROADCAST"/>, there's a <see cref="SAFETY_BROADCAST_WINDOW"/>
|
||||
/// during which movements packets are sent to avoid any packet drop's bad effect, regardless of <see cref="HasVehicleMoved"/>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="latestBroadcastTime"/> is not updated during the <see cref="SAFETY_BROADCAST_WINDOW"/> so we can recognize this window
|
||||
/// </remarks>
|
||||
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;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user