Files
Nitrox/NitroxClient/MonoBehaviours/Multiplayer.cs
2025-07-06 00:23:46 +02:00

234 lines
9.3 KiB
C#

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();
}
}
}
}