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 packetProcessorCache = new(); private IClient client; private IMultiplayerSession multiplayerSession; private PacketReceiver packetReceiver; private ThrottledPacketSender throttledPacketSender; private GameLogic.Terrain terrain; public bool InitialSyncCompleted { get; set; } /// /// True if multiplayer is loaded and client is connected to a server. /// public static bool Active => Main && Main.multiplayerSession.Client.IsConnected; /// /// True if multiplayer is loaded and player has successfully joined a server. /// public static bool Joined => Main && Main.multiplayerSession.CurrentState.CurrentStage == MultiplayerSessionConnectionStage.SESSION_JOINED; public void Awake() { NitroxServiceLocator.LifetimeScopeEnded += (_, _) => packetProcessorCache.Clear(); client = NitroxServiceLocator.LocateService(); multiplayerSession = NitroxServiceLocator.LocateService(); packetReceiver = NitroxServiceLocator.LocateService(); throttledPacketSender = NitroxServiceLocator.LocateService(); terrain = NitroxServiceLocator.LocateService(); 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 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(); gameObject.AddComponent(); gameObject.AddComponent(); gameObject.AddComponent(); gameObject.AddComponent(); gameObject.AddComponent(); gameObject.AddComponent(); 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(); LoadingScreenVersionText.DisableWarningText(); DiscordClient.InitializeRPInGame(Main.multiplayerSession.AuthenticationContext.Username, remotePlayerManager.GetTotalPlayerCount(), Main.multiplayerSession.SessionPolicy.MaxConnections); CoroutineHost.StartCoroutine(NitroxServiceLocator.LocateService().LoadChatKeyHint()); } private IEnumerator InitializeLocalPlayerState() { ILocalNitroxPlayer localPlayer = NitroxServiceLocator.LocateService(); IEnumerable colorSwapManagers = NitroxServiceLocator.LocateService>(); // 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(); } } } }