first commit

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

View File

@@ -0,0 +1,167 @@
using System.Reflection;
using Autofac;
using Autofac.Core;
using NitroxClient.Communication;
using NitroxClient.Communication.Abstract;
using NitroxClient.Communication.MultiplayerSession;
using NitroxClient.Communication.NetworkingLayer.LiteNetLib;
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.Debuggers;
using NitroxClient.GameLogic;
using NitroxClient.GameLogic.ChatUI;
using NitroxClient.GameLogic.FMOD;
using NitroxClient.GameLogic.HUD;
using NitroxClient.GameLogic.InitialSync.Abstract;
using NitroxClient.GameLogic.PlayerLogic;
using NitroxClient.GameLogic.PlayerLogic.PlayerModel;
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract;
using NitroxClient.GameLogic.PlayerLogic.PlayerPreferences;
using NitroxClient.GameLogic.Settings;
using NitroxClient.GameLogic.Spawning.Metadata;
using NitroxClient.GameLogic.Spawning.Metadata.Extractor.Abstract;
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxModel;
using NitroxModel.Core;
using NitroxModel.GameLogic.FMOD;
using NitroxModel.Helper;
using NitroxModel.Networking;
using NitroxModel_Subnautica.Helper;
namespace NitroxClient
{
public class ClientAutoFacRegistrar : IAutoFacRegistrar
{
private static readonly Assembly currentAssembly = Assembly.GetExecutingAssembly();
private readonly IModule[] modules;
public ClientAutoFacRegistrar(params IModule[] modules)
{
this.modules = modules;
}
public void RegisterDependencies(ContainerBuilder containerBuilder)
{
foreach (IModule module in modules)
{
containerBuilder.RegisterModule(module);
}
RegisterCoreDependencies(containerBuilder);
RegisterMetadataDependencies(containerBuilder);
RegisterPacketProcessors(containerBuilder);
RegisterColorSwapManagers(containerBuilder);
RegisterInitialSyncProcessors(containerBuilder);
}
private void RegisterCoreDependencies(ContainerBuilder containerBuilder)
{
#if DEBUG
containerBuilder.RegisterAssemblyTypes(currentAssembly)
.AssignableTo<BaseDebugger>()
.As<BaseDebugger>()
.AsImplementedInterfaces()
.AsSelf()
.SingleInstance();
#endif
containerBuilder.Register(c => new NitroxProtobufSerializer($"{nameof(NitroxModel)}.dll"));
containerBuilder.RegisterType<UnityPreferenceStateProvider>()
.As<IPreferenceStateProvider>()
.SingleInstance();
containerBuilder.RegisterType<PlayerPreferenceManager>().SingleInstance();
containerBuilder.RegisterType<MultiplayerSessionManager>()
.As<IMultiplayerSession>()
.As<IPacketSender>()
.InstancePerLifetimeScope();
containerBuilder.RegisterType<LiteNetLibClient>()
.As<IClient>()
.InstancePerLifetimeScope();
containerBuilder.RegisterType<LocalPlayer>()
.AsSelf() //Would like to deprecate this registration at some point and just work through an abstraction.
.As<ILocalNitroxPlayer>()
.InstancePerLifetimeScope();
containerBuilder.RegisterType<SubnauticaMap>()
.As<IMap>()
.InstancePerLifetimeScope();
containerBuilder.RegisterType<PlayerManager>().InstancePerLifetimeScope();
containerBuilder.RegisterType<PlayerModelManager>().InstancePerLifetimeScope();
containerBuilder.RegisterType<PlayerVitalsManager>().InstancePerLifetimeScope();
containerBuilder.RegisterType<PacketReceiver>().InstancePerLifetimeScope();
containerBuilder.RegisterType<Vehicles>().InstancePerLifetimeScope();
containerBuilder.RegisterType<AI>().InstancePerLifetimeScope();
containerBuilder.RegisterType<PlayerChatManager>().InstancePerLifetimeScope();
containerBuilder.RegisterType<SimulationOwnership>().InstancePerLifetimeScope();
containerBuilder.RegisterType<LiveMixinManager>().InstancePerLifetimeScope();
containerBuilder.RegisterType<Entities>().InstancePerLifetimeScope();
containerBuilder.RegisterType<MedkitFabricator>().InstancePerLifetimeScope();
containerBuilder.RegisterType<Items>().InstancePerLifetimeScope();
containerBuilder.RegisterType<EquipmentSlots>().InstancePerLifetimeScope();
containerBuilder.RegisterType<ItemContainers>().InstancePerLifetimeScope();
containerBuilder.RegisterType<Cyclops>().InstancePerLifetimeScope();
containerBuilder.RegisterType<Rockets>().InstancePerLifetimeScope();
containerBuilder.RegisterType<MobileVehicleBay>().InstancePerLifetimeScope();
containerBuilder.RegisterType<Interior>().InstancePerLifetimeScope();
containerBuilder.RegisterType<NitroxConsole>().InstancePerLifetimeScope();
containerBuilder.RegisterType<Terrain>().InstancePerLifetimeScope();
containerBuilder.RegisterType<ExosuitModuleEvent>().InstancePerLifetimeScope();
containerBuilder.RegisterType<SeamothModulesEvent>().InstancePerLifetimeScope();
containerBuilder.RegisterType<Fires>().InstancePerLifetimeScope();
containerBuilder.Register(_ => FMODWhitelist.Load(GameInfo.Subnautica)).InstancePerLifetimeScope();
containerBuilder.RegisterType<FMODSystem>().InstancePerLifetimeScope();
containerBuilder.RegisterType<NitroxSettingsManager>().InstancePerLifetimeScope();
containerBuilder.RegisterType<ThrottledPacketSender>().InstancePerLifetimeScope();
containerBuilder.RegisterType<PlayerCinematics>().InstancePerLifetimeScope();
containerBuilder.RegisterType<NitroxPDATabManager>().InstancePerLifetimeScope();
containerBuilder.RegisterType<TimeManager>().InstancePerLifetimeScope();
containerBuilder.RegisterType<BulletManager>().InstancePerLifetimeScope();
containerBuilder.RegisterType<NtpSyncer>().InstancePerLifetimeScope();
}
private void RegisterMetadataDependencies(ContainerBuilder containerBuilder)
{
containerBuilder.RegisterAssemblyTypes(currentAssembly)
.AssignableTo<IEntityMetadataExtractor>()
.As<IEntityMetadataExtractor>()
.AsSelf()
.SingleInstance();
containerBuilder.RegisterAssemblyTypes(currentAssembly)
.AssignableTo<IEntityMetadataProcessor>()
.As<IEntityMetadataProcessor>()
.AsSelf()
.SingleInstance();
containerBuilder.RegisterType<EntityMetadataManager>().InstancePerLifetimeScope();
}
private void RegisterPacketProcessors(ContainerBuilder containerBuilder)
{
containerBuilder
.RegisterAssemblyTypes(currentAssembly)
.AsClosedTypesOf(typeof(ClientPacketProcessor<>))
.InstancePerLifetimeScope();
}
private void RegisterColorSwapManagers(ContainerBuilder containerBuilder)
{
containerBuilder
.RegisterAssemblyTypes(currentAssembly)
.AssignableTo<IColorSwapManager>()
.As<IColorSwapManager>()
.SingleInstance();
}
private void RegisterInitialSyncProcessors(ContainerBuilder containerBuilder)
{
containerBuilder
.RegisterAssemblyTypes(currentAssembly)
.AssignableTo<IInitialSyncProcessor>()
.As<IInitialSyncProcessor>()
.InstancePerLifetimeScope();
}
}
}

View File

@@ -0,0 +1,19 @@
using System.Threading.Tasks;
using NitroxModel.Packets;
namespace NitroxClient.Communication.Abstract
{
/// <summary>
/// Abstracted IClient in order to give us options in the underlying protocol that we use to communicate with the server.
/// Ex: We may want to also roll a UDP client in the future to handle packets where we don't necessarily care
/// about transmission order or error recovery.
/// </summary>
public interface IClient
{
bool IsConnected { get; }
Task StartAsync(string ipAddress, int serverPort);
void Stop();
void PollEvents();
void Send(Packet packet);
}
}

View File

@@ -0,0 +1,21 @@
using System.Threading.Tasks;
using NitroxModel.MultiplayerSession;
using NitroxModel.Packets;
namespace NitroxClient.Communication.Abstract
{
public delegate void MultiplayerSessionConnectionStateChangedEventHandler(IMultiplayerSessionConnectionState newState);
public interface IMultiplayerSession : IPacketSender, IMultiplayerSessionState
{
IMultiplayerSessionConnectionState CurrentState { get; }
event MultiplayerSessionConnectionStateChangedEventHandler ConnectionStateChanged;
Task ConnectAsync(string ipAddress, int port);
void ProcessSessionPolicy(MultiplayerSessionPolicy policy);
void RequestSessionReservation(PlayerSettings playerSettings, AuthenticationContext authenticationContext);
void ProcessReservationResponsePacket(MultiplayerSessionReservation reservation);
void JoinSession();
void Disconnect();
}
}

View File

@@ -0,0 +1,8 @@
namespace NitroxClient.Communication.Abstract
{
public interface IMultiplayerSessionConnectionContext : IMultiplayerSessionState
{
void UpdateConnectionState(IMultiplayerSessionConnectionState sessionConnectionState);
void ClearSessionState();
}
}

View File

@@ -0,0 +1,14 @@
using System.Threading.Tasks;
using NitroxClient.Communication.MultiplayerSession;
namespace NitroxClient.Communication.Abstract
{
public interface IMultiplayerSessionConnectionState
{
MultiplayerSessionConnectionStage CurrentStage { get; }
Task NegotiateReservationAsync(IMultiplayerSessionConnectionContext sessionConnectionContext);
void JoinSession(IMultiplayerSessionConnectionContext sessionConnectionContext);
void Disconnect(IMultiplayerSessionConnectionContext sessionConnectionContext);
}
}

View File

@@ -0,0 +1,16 @@
using NitroxModel.MultiplayerSession;
using NitroxModel.Packets;
namespace NitroxClient.Communication.Abstract
{
public interface IMultiplayerSessionState
{
IClient Client { get; }
string IpAddress { get; }
int ServerPort { get; }
MultiplayerSessionPolicy SessionPolicy { get; }
PlayerSettings PlayerSettings { get; }
AuthenticationContext AuthenticationContext { get; }
MultiplayerSessionReservation Reservation { get; }
}
}

View File

@@ -0,0 +1,13 @@
using NitroxModel.Packets;
namespace NitroxClient.Communication.Abstract;
public interface IPacketSender
{
/// <summary>
/// Sends the packet. Returns true if packet was not suppressed.
/// </summary>
/// <param name="packet">The packet to send.</param>
/// <returns>True if not suppressed and actually sent.</returns>
bool Send<T>(T packet) where T : Packet;
}

View File

@@ -0,0 +1,15 @@
using System;
namespace NitroxClient.Communication.Exceptions
{
public class ClientConnectionFailedException : Exception
{
public ClientConnectionFailedException(string message) : base(message)
{
}
public ClientConnectionFailedException(string message, Exception innerException) : base(message, innerException)
{
}
}
}

View File

@@ -0,0 +1,148 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using LiteNetLib;
using LiteNetLib.Utils;
using NitroxModel.Constants;
namespace NitroxClient.Communication;
public static class LANBroadcastClient
{
private static event Action<IPEndPoint> serverFound;
public static event Action<IPEndPoint> ServerFound
{
add
{
serverFound += value;
// Trigger event for servers already found.
foreach (IPEndPoint server in DiscoveredServers)
{
value?.Invoke(server);
}
}
remove => serverFound -= value;
}
private static Task<IEnumerable<IPEndPoint>> lastTask;
public static ConcurrentQueue<IPEndPoint> DiscoveredServers = [];
public static async Task<IEnumerable<IPEndPoint>> SearchAsync(bool force = false, CancellationToken cancellationToken = default)
{
if (!force && lastTask != null)
{
DiscoveredServers = [];
foreach (IPEndPoint ipEndPoint in await lastTask)
{
DiscoveredServers.Enqueue(ipEndPoint);
}
return DiscoveredServers;
}
return await (lastTask = SearchInternalAsync(cancellationToken));
}
private static async Task<IEnumerable<IPEndPoint>> SearchInternalAsync(CancellationToken cancellationToken = default)
{
static void ReceivedResponse(IPEndPoint remoteEndPoint, NetPacketReader reader, UnconnectedMessageType messageType)
{
if (messageType != UnconnectedMessageType.Broadcast)
{
return;
}
string responseString = reader.GetString();
if (responseString != LANDiscoveryConstants.BROADCAST_RESPONSE_STRING)
{
return;
}
int serverPort = reader.GetInt();
IPEndPoint serverEndPoint = new(remoteEndPoint.Address, serverPort);
if (DiscoveredServers.Contains(serverEndPoint))
{
return;
}
Log.Debug($"Found LAN server at {serverEndPoint}.");
DiscoveredServers.Enqueue(serverEndPoint);
OnServerFound(serverEndPoint);
}
cancellationToken = cancellationToken == default ? new CancellationTokenSource(TimeSpan.FromMinutes(1)).Token : cancellationToken;
EventBasedNetListener listener = new();
NetManager client = new(listener)
{
AutoRecycle = true,
BroadcastReceiveEnabled = true,
UnconnectedMessagesEnabled = true
};
// Try start client on an available, predefined, port
foreach (int port in LANDiscoveryConstants.BROADCAST_PORTS)
{
if (client.Start(port))
{
break;
}
}
if (!client.IsRunning)
{
Log.Warn("Failed to start LAN discover client: none of the defined ports are available");
return [];
}
Log.Info("Searching for LAN servers...");
listener.NetworkReceiveUnconnectedEvent += ReceivedResponse;
Task broadcastTask = Task.Run(async () =>
{
while (!cancellationToken.IsCancellationRequested)
{
NetDataWriter writer = new();
writer.Put(LANDiscoveryConstants.BROADCAST_REQUEST_STRING);
foreach (int port in LANDiscoveryConstants.BROADCAST_PORTS)
{
client.SendBroadcast(writer, port);
}
try
{
await Task.Delay(5000, cancellationToken);
}
catch (TaskCanceledException)
{
// ignore
}
}
}, cancellationToken);
Task receiveTask = Task.Run(async () =>
{
while (!cancellationToken.IsCancellationRequested)
{
client.PollEvents();
try
{
await Task.Delay(100, cancellationToken);
}
catch (TaskCanceledException)
{
// ignore
}
}
}, cancellationToken);
await Task.WhenAll(broadcastTask, receiveTask);
// Cleanup
listener.ClearNetworkReceiveUnconnectedEvent();
client.Stop();
listener.NetworkReceiveUnconnectedEvent -= ReceivedResponse;
return DiscoveredServers;
}
private static void OnServerFound(IPEndPoint obj)
{
serverFound?.Invoke(obj);
}
}

View File

@@ -0,0 +1,87 @@
using System;
using System.Threading.Tasks;
using NitroxClient.Communication.Abstract;
using NitroxModel.Helper;
using NitroxModel.MultiplayerSession;
using NitroxModel.Packets;
namespace NitroxClient.Communication.MultiplayerSession.ConnectionState
{
public class AwaitingReservationCredentials : ConnectionNegotiatingState
{
public override MultiplayerSessionConnectionStage CurrentStage => MultiplayerSessionConnectionStage.AWAITING_RESERVATION_CREDENTIALS;
public override Task NegotiateReservationAsync(IMultiplayerSessionConnectionContext sessionConnectionContext)
{
try
{
ValidateState(sessionConnectionContext);
string reservationCorrelationId = Guid.NewGuid().ToString();
RequestSessionReservation(sessionConnectionContext, reservationCorrelationId);
AwaitSessionReservation(sessionConnectionContext, reservationCorrelationId);
}
catch (Exception)
{
Disconnect(sessionConnectionContext);
throw;
}
return Task.CompletedTask;
}
private void RequestSessionReservation(IMultiplayerSessionConnectionContext sessionConnectionContext, string reservationCorrelationId)
{
IClient client = sessionConnectionContext.Client;
PlayerSettings playerSettings = sessionConnectionContext.PlayerSettings;
AuthenticationContext authenticationContext = sessionConnectionContext.AuthenticationContext;
MultiplayerSessionReservationRequest requestPacket = new(reservationCorrelationId, playerSettings, authenticationContext);
client.Send(requestPacket);
}
private void AwaitSessionReservation(IMultiplayerSessionConnectionContext sessionConnectionContext, string reservationCorrelationId)
{
AwaitingSessionReservation nextState = new AwaitingSessionReservation(reservationCorrelationId);
sessionConnectionContext.UpdateConnectionState(nextState);
}
private static void ValidateState(IMultiplayerSessionConnectionContext sessionConnectionContext)
{
ClientIsConnected(sessionConnectionContext);
PlayerSettingsIsNotNull(sessionConnectionContext);
AuthenticationContextIsNotNull(sessionConnectionContext);
}
private static void ClientIsConnected(IMultiplayerSessionConnectionContext sessionConnectionContext)
{
if (!sessionConnectionContext.Client.IsConnected)
{
throw new InvalidOperationException("The client is not connected.");
}
}
private static void PlayerSettingsIsNotNull(IMultiplayerSessionConnectionContext sessionConnectionContext)
{
try
{
Validate.NotNull(sessionConnectionContext.PlayerSettings);
}
catch (ArgumentNullException ex)
{
throw new InvalidOperationException("The context does not contain player settings.", ex);
}
}
private static void AuthenticationContextIsNotNull(IMultiplayerSessionConnectionContext sessionConnectionContext)
{
try
{
Validate.NotNull(sessionConnectionContext.AuthenticationContext);
}
catch (ArgumentNullException ex)
{
throw new InvalidOperationException("The context does not contain an authentication context.", ex);
}
}
}
}

View File

@@ -0,0 +1,74 @@
using System;
using System.Threading.Tasks;
using NitroxClient.Communication.Abstract;
using NitroxModel.Helper;
using NitroxModel.MultiplayerSession;
using NitroxModel.Packets.Exceptions;
namespace NitroxClient.Communication.MultiplayerSession.ConnectionState
{
public class AwaitingSessionReservation : ConnectionNegotiatingState
{
private readonly string reservationCorrelationId;
public AwaitingSessionReservation(string reservationCorrelationId)
{
Validate.NotNull(reservationCorrelationId);
this.reservationCorrelationId = reservationCorrelationId;
}
public override MultiplayerSessionConnectionStage CurrentStage => MultiplayerSessionConnectionStage.AWAITING_SESSION_RESERVATION;
public override Task NegotiateReservationAsync(IMultiplayerSessionConnectionContext sessionConnectionContext)
{
try
{
ValidateState(sessionConnectionContext);
HandleReservation(sessionConnectionContext);
}
catch (Exception)
{
Disconnect(sessionConnectionContext);
throw;
}
return Task.CompletedTask;
}
private static void HandleReservation(IMultiplayerSessionConnectionContext sessionConnectionContext)
{
IMultiplayerSessionConnectionState nextState = sessionConnectionContext.Reservation.ReservationState switch
{
MultiplayerSessionReservationState.RESERVED => new SessionReserved(),
_ => new SessionReservationRejected(),
};
sessionConnectionContext.UpdateConnectionState(nextState);
}
private void ValidateState(IMultiplayerSessionConnectionContext sessionConnectionContext)
{
ReservationIsNotNull(sessionConnectionContext);
ReservationPacketIsCorrelated(sessionConnectionContext);
}
private static void ReservationIsNotNull(IMultiplayerSessionConnectionContext sessionConnectionContext)
{
try
{
Validate.NotNull(sessionConnectionContext.Reservation);
}
catch (ArgumentNullException ex)
{
throw new InvalidOperationException("The context does not have a reservation.", ex);
}
}
private void ReservationPacketIsCorrelated(IMultiplayerSessionConnectionContext sessionConnectionContext)
{
if (!reservationCorrelationId.Equals(sessionConnectionContext.Reservation.CorrelationId))
{
throw new UncorrelatedPacketException(sessionConnectionContext.Reservation, reservationCorrelationId);
}
}
}
}

View File

@@ -0,0 +1,22 @@
using System.Threading.Tasks;
using NitroxClient.Communication.Abstract;
namespace NitroxClient.Communication.MultiplayerSession.ConnectionState
{
public abstract class CommunicatingState : IMultiplayerSessionConnectionState
{
public abstract MultiplayerSessionConnectionStage CurrentStage { get; }
public abstract Task NegotiateReservationAsync(IMultiplayerSessionConnectionContext sessionConnectionContext);
public abstract void JoinSession(IMultiplayerSessionConnectionContext sessionConnectionContext);
public virtual void Disconnect(IMultiplayerSessionConnectionContext sessionConnectionContext)
{
sessionConnectionContext.ClearSessionState();
sessionConnectionContext.Client.Stop();
Disconnected newConnectionState = new Disconnected();
sessionConnectionContext.UpdateConnectionState(newConnectionState);
}
}
}

View File

@@ -0,0 +1,14 @@
using System;
using System.Threading.Tasks;
using NitroxClient.Communication.Abstract;
namespace NitroxClient.Communication.MultiplayerSession.ConnectionState
{
public abstract class ConnectionNegotiatedState : CommunicatingState
{
public override Task NegotiateReservationAsync(IMultiplayerSessionConnectionContext sessionConnectionContext)
{
throw new InvalidOperationException("Unable to negotiate a session connection in the current state.");
}
}
}

View File

@@ -0,0 +1,13 @@
using System;
using NitroxClient.Communication.Abstract;
namespace NitroxClient.Communication.MultiplayerSession.ConnectionState
{
public abstract class ConnectionNegotiatingState : CommunicatingState
{
public override void JoinSession(IMultiplayerSessionConnectionContext sessionConnectionContext)
{
throw new InvalidOperationException("Cannot join a session until a reservation has been negotiated with the server.");
}
}
}

View File

@@ -0,0 +1,85 @@
using System;
using System.Threading.Tasks;
using NitroxClient.Communication.Abstract;
using NitroxClient.Communication.Exceptions;
using NitroxModel.Helper;
using NitroxModel.Packets;
namespace NitroxClient.Communication.MultiplayerSession.ConnectionState
{
public class Disconnected : IMultiplayerSessionConnectionState
{
public MultiplayerSessionConnectionStage CurrentStage => MultiplayerSessionConnectionStage.DISCONNECTED;
public async Task NegotiateReservationAsync(IMultiplayerSessionConnectionContext sessionConnectionContext)
{
ValidateState(sessionConnectionContext);
IClient client = sessionConnectionContext.Client;
string ipAddress = sessionConnectionContext.IpAddress;
int port = sessionConnectionContext.ServerPort;
await StartClientAsync(ipAddress, client, port);
EstablishSessionPolicy(sessionConnectionContext, client);
}
private void ValidateState(IMultiplayerSessionConnectionContext sessionConnectionContext)
{
ValidateClient(sessionConnectionContext);
try
{
Validate.NotNull(sessionConnectionContext.IpAddress);
}
catch (ArgumentNullException ex)
{
throw new InvalidOperationException("The context is missing an IP address.", ex);
}
}
private static void ValidateClient(IMultiplayerSessionConnectionContext sessionConnectionContext)
{
try
{
Validate.NotNull(sessionConnectionContext.Client);
}
catch (ArgumentNullException ex)
{
throw new InvalidOperationException("The client must be set on the connection context before trying to negotiate a session reservation.", ex);
}
}
private static async Task StartClientAsync(string ipAddress, IClient client, int port)
{
if (!client.IsConnected)
{
await client.StartAsync(ipAddress, port);
if (!client.IsConnected)
{
throw new ClientConnectionFailedException("The client failed to connect without providing a reason why.");
}
}
}
private static void EstablishSessionPolicy(IMultiplayerSessionConnectionContext sessionConnectionContext, IClient client)
{
string policyRequestCorrelationId = Guid.NewGuid().ToString();
MultiplayerSessionPolicyRequest requestPacket = new MultiplayerSessionPolicyRequest(policyRequestCorrelationId);
client.Send(requestPacket);
EstablishingSessionPolicy nextState = new EstablishingSessionPolicy(policyRequestCorrelationId);
sessionConnectionContext.UpdateConnectionState(nextState);
}
public void JoinSession(IMultiplayerSessionConnectionContext sessionConnectionContext)
{
throw new InvalidOperationException("Cannot join a session until a reservation has been negotiated with the server.");
}
public void Disconnect(IMultiplayerSessionConnectionContext sessionConnectionContext)
{
throw new InvalidOperationException("Not connected to a multiplayer server.");
}
}
}

View File

@@ -0,0 +1,68 @@
using System;
using System.Threading.Tasks;
using NitroxClient.Communication.Abstract;
using NitroxModel.Helper;
using NitroxModel.Packets.Exceptions;
namespace NitroxClient.Communication.MultiplayerSession.ConnectionState
{
public class EstablishingSessionPolicy : ConnectionNegotiatingState
{
private readonly string policyRequestCorrelationId;
public EstablishingSessionPolicy(string policyRequestCorrelationId)
{
Validate.NotNull(policyRequestCorrelationId);
this.policyRequestCorrelationId = policyRequestCorrelationId;
}
public override MultiplayerSessionConnectionStage CurrentStage => MultiplayerSessionConnectionStage.ESTABLISHING_SERVER_POLICY;
public override Task NegotiateReservationAsync(IMultiplayerSessionConnectionContext sessionConnectionContext)
{
try
{
ValidateState(sessionConnectionContext);
AwaitReservationCredentials(sessionConnectionContext);
}
catch (Exception)
{
Disconnect(sessionConnectionContext);
throw;
}
return Task.CompletedTask;
}
private void ValidateState(IMultiplayerSessionConnectionContext sessionConnectionContext)
{
SessionPolicyIsNotNull(sessionConnectionContext);
SessionPolicyPacketCorrelation(sessionConnectionContext);
}
private static void SessionPolicyIsNotNull(IMultiplayerSessionConnectionContext sessionConnectionContext)
{
try
{
Validate.NotNull(sessionConnectionContext.SessionPolicy);
}
catch (ArgumentNullException ex)
{
throw new InvalidOperationException("The context is missing a session policy.", ex);
}
}
private void SessionPolicyPacketCorrelation(IMultiplayerSessionConnectionContext sessionConnectionContext)
{
if (!policyRequestCorrelationId.Equals(sessionConnectionContext.SessionPolicy.CorrelationId))
{
throw new UncorrelatedPacketException(sessionConnectionContext.SessionPolicy, policyRequestCorrelationId);
}
}
private void AwaitReservationCredentials(IMultiplayerSessionConnectionContext sessionConnectionContext)
{
AwaitingReservationCredentials nextState = new AwaitingReservationCredentials();
sessionConnectionContext.UpdateConnectionState(nextState);
}
}
}

View File

@@ -0,0 +1,15 @@
using System;
using NitroxClient.Communication.Abstract;
namespace NitroxClient.Communication.MultiplayerSession.ConnectionState
{
public class SessionJoined : ConnectionNegotiatedState
{
public override MultiplayerSessionConnectionStage CurrentStage => MultiplayerSessionConnectionStage.SESSION_JOINED;
public override void JoinSession(IMultiplayerSessionConnectionContext sessionConnectionContext)
{
throw new InvalidOperationException("The session is already in progress.");
}
}
}

View File

@@ -0,0 +1,15 @@
using System;
using NitroxClient.Communication.Abstract;
namespace NitroxClient.Communication.MultiplayerSession.ConnectionState
{
public class SessionReservationRejected : ConnectionNegotiatedState
{
public override MultiplayerSessionConnectionStage CurrentStage => MultiplayerSessionConnectionStage.SESSION_RESERVATION_REJECTED;
public override void JoinSession(IMultiplayerSessionConnectionContext sessionConnectionContext)
{
throw new InvalidOperationException("The session has rejected the reserveration request.");
}
}
}

View File

@@ -0,0 +1,51 @@
using System;
using NitroxClient.Communication.Abstract;
using NitroxModel.Packets;
namespace NitroxClient.Communication.MultiplayerSession.ConnectionState
{
public class SessionReserved : ConnectionNegotiatedState
{
public override MultiplayerSessionConnectionStage CurrentStage => MultiplayerSessionConnectionStage.SESSION_RESERVED;
public override void JoinSession(IMultiplayerSessionConnectionContext sessionConnectionContext)
{
try
{
ValidateState(sessionConnectionContext);
EnterMultiplayerSession(sessionConnectionContext);
ChangeState(sessionConnectionContext);
}
catch (Exception)
{
Disconnect(sessionConnectionContext);
throw;
}
}
private static void ValidateState(IMultiplayerSessionConnectionContext sessionConnectionContext)
{
if (!sessionConnectionContext.Client.IsConnected)
{
throw new InvalidOperationException("The client is not connected.");
}
}
private void EnterMultiplayerSession(IMultiplayerSessionConnectionContext sessionConnectionContext)
{
IClient client = sessionConnectionContext.Client;
MultiplayerSessionReservation reservation = sessionConnectionContext.Reservation;
string correlationId = reservation.CorrelationId;
string reservationKey = reservation.ReservationKey;
PlayerJoiningMultiplayerSession packet = new PlayerJoiningMultiplayerSession(correlationId, reservationKey);
client.Send(packet);
}
private void ChangeState(IMultiplayerSessionConnectionContext sessionConnectionContext)
{
SessionJoined nextState = new SessionJoined();
sessionConnectionContext.UpdateConnectionState(nextState);
}
}
}

View File

@@ -0,0 +1,13 @@
namespace NitroxClient.Communication.MultiplayerSession
{
public enum MultiplayerSessionConnectionStage
{
DISCONNECTED,
ESTABLISHING_SERVER_POLICY,
AWAITING_RESERVATION_CREDENTIALS,
AWAITING_SESSION_RESERVATION,
SESSION_RESERVED,
SESSION_RESERVATION_REJECTED,
SESSION_JOINED
}
}

View File

@@ -0,0 +1,163 @@
using System;
using System.Threading.Tasks;
using NitroxClient.Communication.Abstract;
using NitroxClient.Communication.MultiplayerSession.ConnectionState;
using NitroxClient.GameLogic;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
using NitroxModel.MultiplayerSession;
using NitroxModel.Packets;
using NitroxModel.Serialization;
namespace NitroxClient.Communication.MultiplayerSession
{
public class MultiplayerSessionManager : IMultiplayerSession, IMultiplayerSessionConnectionContext
{
private static readonly Task initSerializerTask;
static MultiplayerSessionManager()
{
initSerializerTask = Task.Run(Packet.InitSerializer);
}
public IClient Client { get; }
public string IpAddress { get; private set; }
public int ServerPort { get; private set; }
public MultiplayerSessionPolicy SessionPolicy { get; private set; }
public PlayerSettings PlayerSettings { get; private set; }
public AuthenticationContext AuthenticationContext { get; private set; }
public IMultiplayerSessionConnectionState CurrentState { get; private set; }
public MultiplayerSessionReservation Reservation { get; private set; }
public MultiplayerSessionManager(IClient client)
{
Log.Info("Initializing MultiplayerSessionManager...");
Client = client;
CurrentState = new Disconnected();
}
// Testing entry point
internal MultiplayerSessionManager(IClient client, IMultiplayerSessionConnectionState initialState)
{
Client = client;
CurrentState = initialState;
}
public event MultiplayerSessionConnectionStateChangedEventHandler ConnectionStateChanged;
public async Task ConnectAsync(string ipAddress, int port)
{
IpAddress = ipAddress;
ServerPort = port;
await initSerializerTask;
await CurrentState.NegotiateReservationAsync(this);
}
public void ProcessSessionPolicy(MultiplayerSessionPolicy policy)
{
SessionPolicy = policy;
NitroxConsole.DisableConsole = SessionPolicy.DisableConsole;
Version localVersion = NitroxEnvironment.Version;
NitroxVersion nitroxVersion = new(localVersion.Major, localVersion.Minor);
switch (nitroxVersion.CompareTo(SessionPolicy.NitroxVersionAllowed))
{
case -1:
Log.Error($"Client is out of date. Server: {SessionPolicy.NitroxVersionAllowed}, Client: {localVersion}");
Log.InGame(Language.main.Get("Nitrox_OutOfDateClient")
.Replace("{serverVersion}", SessionPolicy.NitroxVersionAllowed.ToString())
.Replace("{localVersion}", localVersion.ToString()));
CurrentState.Disconnect(this);
return;
case 1:
Log.Error($"Server is out of date. Server: {SessionPolicy.NitroxVersionAllowed}, Client: {localVersion}");
Log.InGame(Language.main.Get("Nitrox_OutOfDateServer")
.Replace("{serverVersion}", SessionPolicy.NitroxVersionAllowed.ToString())
.Replace("{localVersion}", localVersion.ToString()));
CurrentState.Disconnect(this);
return;
}
CurrentState.NegotiateReservationAsync(this);
}
public void RequestSessionReservation(PlayerSettings playerSettings, AuthenticationContext authenticationContext)
{
// If a reservation has already been sent (in which case the client is enqueued in the join queue)
if (CurrentState.CurrentStage == MultiplayerSessionConnectionStage.AWAITING_SESSION_RESERVATION)
{
Log.Info("Waiting in join queue…");
Log.InGame(Language.main.Get("Nitrox_Waiting"));
return;
}
PlayerSettings = playerSettings;
AuthenticationContext = authenticationContext;
CurrentState.NegotiateReservationAsync(this);
}
public void ProcessReservationResponsePacket(MultiplayerSessionReservation reservation)
{
if (reservation.ReservationState == MultiplayerSessionReservationState.ENQUEUED_IN_JOIN_QUEUE)
{
Log.Info("Waiting in join queue…");
Log.InGame(Language.main.Get("Nitrox_Waiting"));
return;
}
Reservation = reservation;
CurrentState.NegotiateReservationAsync(this);
}
public void JoinSession()
{
CurrentState.JoinSession(this);
}
public void Disconnect()
{
if (CurrentState.CurrentStage != MultiplayerSessionConnectionStage.DISCONNECTED)
{
CurrentState.Disconnect(this);
}
}
public bool Send<T>(T packet) where T : Packet
{
if (!PacketSuppressor<T>.IsSuppressed)
{
Client.Send(packet);
return true;
}
return false;
}
public void UpdateConnectionState(IMultiplayerSessionConnectionState sessionConnectionState)
{
Validate.NotNull(sessionConnectionState);
string fromStage = CurrentState == null ? "null" : CurrentState.CurrentStage.ToString();
string username = AuthenticationContext == null ? "" : AuthenticationContext.Username;
Log.Debug($"Updating session stage from '{fromStage}' to '{sessionConnectionState.CurrentStage}' for '{username}'");
CurrentState = sessionConnectionState;
// Last connection state changed will not have any handlers
ConnectionStateChanged?.Invoke(CurrentState);
if (sessionConnectionState.CurrentStage == MultiplayerSessionConnectionStage.SESSION_RESERVED)
{
Log.PlayerName = username;
}
}
public void ClearSessionState()
{
IpAddress = null;
ServerPort = ServerList.DEFAULT_PORT;
SessionPolicy = null;
PlayerSettings = null;
AuthenticationContext = null;
Reservation = null;
}
}
}

View File

@@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace NitroxClient.Communication.NetworkingLayer.LiteNetLib;
public sealed class ClockSyncProcedure(LiteNetLibClient liteNetLibClient) : IDisposable
{
private readonly LiteNetLibClient liteNetLibClient = liteNetLibClient;
private readonly int previousPingInterval = liteNetLibClient.PingInterval;
private readonly List<long> deltas = [];
public static ClockSyncProcedure Start(LiteNetLibClient liteNetLibClient, int procedureDuration)
{
ClockSyncProcedure clockSyncProcedure = new(liteNetLibClient);
liteNetLibClient.PingInterval = (int)TimeSpan.FromSeconds(procedureDuration).TotalMilliseconds;
liteNetLibClient.LatencyUpdateCallback += clockSyncProcedure.LatencyUpdate;
liteNetLibClient.ForceUpdate();
return clockSyncProcedure;
}
public void LatencyUpdate(long remoteTimeDelta)
{
deltas.Add(remoteTimeDelta);
liteNetLibClient.ForceUpdate();
}
/// <summary>
/// Get an average of remote delta times gathered until now without the ones which are not in the standard deviation range.
/// </summary>
public bool TryGetSafeAverageRTD(out long average)
{
if (deltas.Count == 0)
{
Log.Error($"[{nameof(ClockSyncProcedure)}] No delta received !");
average = 0;
return false; // abnormal situation
}
double avg = deltas.Average();
average = (long)avg; // Need to assign from another variable since you can't give a ref to the below lambda
long standardDeviation = (long)Math.Sqrt(deltas.Average(v => Math.Pow(v - avg, 2)));
List<long> validValues = [];
foreach (long delta in deltas)
{
if (Math.Abs(delta - average) <= standardDeviation)
{
validValues.Add(delta);
}
}
if (validValues.Count == 0)
{
Log.Warn($"[{nameof(ClockSyncProcedure)}] No valid value out of {deltas.Count} deltas. Using regular average without filtering.");
return true; // value is not really meaningful ...
}
average = (long)validValues.Average();
return true;
}
public void Dispose()
{
liteNetLibClient.LatencyUpdateCallback -= LatencyUpdate;
liteNetLibClient.PingInterval = previousPingInterval;
}
}

View File

@@ -0,0 +1,146 @@
using System;
using System.Buffers;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using LiteNetLib;
using LiteNetLib.Utils;
using NitroxClient.Communication.Abstract;
using NitroxClient.Debuggers;
using NitroxClient.MonoBehaviours;
using NitroxClient.MonoBehaviours.Gui.Modals;
using NitroxModel.Networking;
using NitroxModel.Packets;
namespace NitroxClient.Communication.NetworkingLayer.LiteNetLib;
public class LiteNetLibClient : IClient
{
private readonly NetManager client;
private readonly AutoResetEvent connectedEvent = new(false);
private readonly NetDataWriter dataWriter = new();
private readonly INetworkDebugger networkDebugger;
private readonly PacketReceiver packetReceiver;
private readonly FieldInfo manualModeFieldInfo = typeof(NetManager).GetField("_manualMode", BindingFlags.Instance | BindingFlags.NonPublic);
public bool IsConnected { get; private set; }
public int PingInterval
{
get => client.PingInterval;
set => client.PingInterval = value;
}
public Action<long> LatencyUpdateCallback;
public LiteNetLibClient(PacketReceiver packetReceiver, INetworkDebugger networkDebugger = null)
{
this.packetReceiver = packetReceiver;
this.networkDebugger = networkDebugger;
EventBasedNetListener listener = new();
listener.PeerConnectedEvent += Connected;
listener.PeerDisconnectedEvent += Disconnected;
listener.NetworkReceiveEvent += ReceivedNetworkData;
listener.NetworkLatencyUpdateEvent += (peer, _) =>
{
LatencyUpdateCallback?.Invoke(peer.RemoteTimeDelta);
};
client = new NetManager(listener)
{
UpdateTime = 15,
ChannelsCount = (byte)typeof(Packet.UdpChannelId).GetEnumValues().Length,
#if DEBUG
DisconnectTimeout = 300000 //Disables Timeout (for 5 min) for debug purpose (like if you jump though the server code)
#endif
};
}
public async Task StartAsync(string ipAddress, int serverPort)
{
Log.Info("Initializing LiteNetLibClient...");
// ConfigureAwait(false) is needed because Unity uses a custom "UnitySynchronizationContext". Which makes async/await work like Unity coroutines.
// Because this Task.Run is async-over-sync this would otherwise blocks the main thread as it wants to, without ConfigureAwait(false), continue on the same thread (i.e. main thread).
await Task.Run(() =>
{
client.Start();
client.Connect(ipAddress, serverPort, "nitrox");
}).ConfigureAwait(false);
connectedEvent.WaitOne(2000);
connectedEvent.Reset();
}
public void Send(Packet packet)
{
byte[] packetData = packet.Serialize();
dataWriter.Reset();
dataWriter.Put(packetData.Length);
dataWriter.Put(packetData);
networkDebugger?.PacketSent(packet, dataWriter.Length);
client.SendToAll(dataWriter, (byte)packet.UdpChannel, NitroxDeliveryMethod.ToLiteNetLib(packet.DeliveryMethod));
}
public void Stop()
{
IsConnected = false;
client.Stop();
}
/// <summary>
/// This should be called <b>once</b> each game tick
/// </summary>
public void PollEvents() => client.PollEvents();
private void ReceivedNetworkData(NetPeer peer, NetDataReader reader, byte channel, DeliveryMethod deliveryMethod)
{
int packetDataLength = reader.GetInt();
byte[] packetData = ArrayPool<byte>.Shared.Rent(packetDataLength);
try
{
reader.GetBytes(packetData, packetDataLength);
Packet packet = Packet.Deserialize(packetData);
packetReceiver.Add(packet);
networkDebugger?.PacketReceived(packet, packetDataLength);
}
finally
{
ArrayPool<byte>.Shared.Return(packetData, true);
}
}
private void Connected(NetPeer peer)
{
// IsConnected must happen before Set() so that its state is noticed WHEN we unblock the thread (cf. connectedEvent.WaitOne(...))
IsConnected = true;
connectedEvent.Set();
Log.Info("Connected to server");
}
private void Disconnected(NetPeer peer, DisconnectInfo disconnectInfo)
{
// Check must happen before IsConnected is set to false, so that it doesn't send an exception when we aren't even ingame
if (Multiplayer.Active)
{
Modal.Get<LostConnectionModal>()?.Show();
}
IsConnected = false;
Log.Info("Disconnected from server");
}
internal void ForceUpdate()
{
int pingInterval = PingInterval;
// Set PingInterval to 0 so another ping is sent immediately
PingInterval = 0;
// ManualUpdate requires the client to have _manualMode set to true so we temporarily do so
manualModeFieldInfo.SetValue(client, true);
client.ManualUpdate(0);
manualModeFieldInfo.SetValue(client, false);
// We set it back to its high value so another ping isn't sent while we're waiting for the previous one
PingInterval = pingInterval;
}
}

View File

@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using NitroxModel.Packets;
namespace NitroxClient.Communication;
public class PacketReceiver
{
private readonly Queue<Packet> receivedPackets = new(16);
private readonly object receivedPacketsLock = new();
public void Add(Packet packet)
{
lock (receivedPacketsLock)
{
receivedPackets.Enqueue(packet);
}
}
public Packet GetNextPacket()
{
lock (receivedPacketsLock)
{
return receivedPackets.Count == 0 ? null : receivedPackets.Dequeue();
}
}
/// <summary>
/// Applies an operation on each packet waiting to be processed and removes it from the queue.
/// </summary>
public void ConsumePackets<TExtra>(Action<Packet, TExtra> consumer, TExtra extraParameter)
{
Packet packet = GetNextPacket();
while (packet != null)
{
consumer(packet, extraParameter);
packet = GetNextPacket();
}
}
}

View File

@@ -0,0 +1,66 @@
using System;
using NitroxModel.Packets;
namespace NitroxClient.Communication;
/// <summary>
/// Suppresses the given packet type from being sent. Disables the suppression when disposed.
/// </summary>
/// <typeparam name="T">The packet type to suppress.</typeparam>
public readonly struct PacketSuppressor<T> : IDisposable where T : Packet
{
private static bool isSuppressed;
public static bool IsSuppressed => isSuppressed;
private static readonly PacketSuppressor<T> instance = new();
public static PacketSuppressor<T> Suppress()
{
isSuppressed = true;
return instance;
}
public void Dispose()
{
isSuppressed = false;
}
}
/// <inheritdoc cref="PacketSuppressor{T}"/>
/// <typeparam name="T1">First packet type to suppress.</typeparam>
/// <typeparam name="T2">Second packet type to suppress.</typeparam>
/// <typeparam name="T3">Third packet type to suppress.</typeparam>
/// <typeparam name="T4">Fourth packet type to suppress.</typeparam>
/// <typeparam name="T5">Fifth packet type to suppress.</typeparam>
public readonly struct PacketSuppressor<T1, T2, T3, T4, T5> : IDisposable
where T1 : Packet
where T2 : Packet
where T3 : Packet
where T4 : Packet
where T5 : Packet
{
private static readonly PacketSuppressor<T1> instance1 = new();
private static readonly PacketSuppressor<T2> instance2 = new();
private static readonly PacketSuppressor<T3> instance3 = new();
private static readonly PacketSuppressor<T4> instance4 = new();
private static readonly PacketSuppressor<T5> instance5 = new();
public static PacketSuppressor<T1, T2, T3, T4, T5> Suppress()
{
PacketSuppressor<T1>.Suppress();
PacketSuppressor<T2>.Suppress();
PacketSuppressor<T3>.Suppress();
PacketSuppressor<T4>.Suppress();
PacketSuppressor<T5>.Suppress();
return new PacketSuppressor<T1, T2, T3, T4, T5>();
}
public void Dispose()
{
instance1.Dispose();
instance2.Dispose();
instance3.Dispose();
instance4.Dispose();
instance5.Dispose();
}
}

View File

@@ -0,0 +1,15 @@
using NitroxModel.Packets;
using NitroxModel.Packets.Processors.Abstract;
namespace NitroxClient.Communication.Packets.Processors.Abstract
{
public abstract class ClientPacketProcessor<T> : PacketProcessor where T : Packet
{
public override void ProcessPacket(Packet packet, IProcessorContext context)
{
Process((T)packet);
}
public abstract void Process(T packet);
}
}

View File

@@ -0,0 +1,19 @@
using NitroxClient.GameLogic;
using NitroxModel.Packets;
namespace NitroxClient.Communication.Packets.Processors.Abstract;
public class KeepInventoryChangedProcessor : ClientPacketProcessor<KeepInventoryChanged>
{
private readonly LocalPlayer localPlayer;
public KeepInventoryChangedProcessor(LocalPlayer localPlayer)
{
this.localPlayer = localPlayer;
}
public override void Process(KeepInventoryChanged packet)
{
localPlayer.KeepInventoryOnDeath = packet.KeepInventoryOnDeath;
}
}

View File

@@ -0,0 +1,13 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxModel.Packets;
namespace NitroxClient.Communication.Packets.Processors;
public class AggressiveWhenSeeTargetChangedProcessor : ClientPacketProcessor<AggressiveWhenSeeTargetChanged>
{
public override void Process(AggressiveWhenSeeTargetChanged packet)
{
AI.AggressiveWhenSeeTargetChanged(packet.CreatureId, packet.TargetId, packet.Locked, packet.AggressionAmount);
}
}

View File

@@ -0,0 +1,27 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxClient.MonoBehaviours;
using NitroxModel.DataStructures.Util;
using NitroxModel.Packets;
namespace NitroxClient.Communication.Packets.Processors
{
public class AnimationProcessor : ClientPacketProcessor<AnimationChangeEvent>
{
private readonly PlayerManager remotePlayerManager;
public AnimationProcessor(PlayerManager remotePlayerManager)
{
this.remotePlayerManager = remotePlayerManager;
}
public override void Process(AnimationChangeEvent animEvent)
{
Optional<RemotePlayer> opPlayer = remotePlayerManager.Find(animEvent.PlayerId);
if (opPlayer.HasValue)
{
opPlayer.Value.UpdateAnimationAndCollider((AnimChangeType)animEvent.Type, (AnimChangeState)animEvent.State);
}
}
}
}

View File

@@ -0,0 +1,13 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxModel.Packets;
namespace NitroxClient.Communication.Packets.Processors;
public class AttackCyclopsTargetChangedProcessor : ClientPacketProcessor<AttackCyclopsTargetChanged>
{
public override void Process(AttackCyclopsTargetChanged packet)
{
AI.AttackCyclopsTargetChanged(packet.CreatureId, packet.TargetId, packet.AggressiveToNoiseAmount);
}
}

View File

@@ -0,0 +1,26 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxModel.Packets;
namespace NitroxClient.Communication.Packets.Processors;
public class AuroraAndTimeUpdateProcessor : ClientPacketProcessor<AuroraAndTimeUpdate>
{
private readonly TimeManager timeManager;
public AuroraAndTimeUpdateProcessor(TimeManager timeManager)
{
this.timeManager = timeManager;
}
public override void Process(AuroraAndTimeUpdate packet)
{
timeManager.ProcessUpdate(packet.TimeData.TimePacket);
StoryManager.UpdateAuroraData(packet.TimeData.AuroraEventData);
timeManager.AuroraRealExplosionTime = packet.TimeData.AuroraEventData.AuroraRealExplosionTime;
if (packet.Restore)
{
StoryManager.RestoreAurora();
}
}
}

View File

@@ -0,0 +1,12 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxModel.Packets;
namespace NitroxClient.Communication.Packets.Processors
{
class BedEnterProcessor : ClientPacketProcessor<BedEnter>
{
public override void Process(BedEnter packet)
{
}
}
}

View File

@@ -0,0 +1,31 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic.Bases;
using NitroxModel.Packets;
namespace NitroxClient.Communication.Packets.Processors;
public abstract class BuildProcessor<T> : ClientPacketProcessor<T> where T : Packet
{
public override void Process(T packet)
{
BuildingHandler.Main.BuildQueue.Enqueue(packet);
}
}
public class PlaceGhostProcessor : BuildProcessor<PlaceGhost> { }
public class PlaceModuleProcessor : BuildProcessor<PlaceModule> { }
public class ModifyConstructedAmountProcessor : BuildProcessor<ModifyConstructedAmount> { }
public class PlaceBaseProcessor : BuildProcessor<PlaceBase> { }
public class UpdateBaseProcessor : BuildProcessor<UpdateBase> { }
public class BaseDeconstructedProcessor : BuildProcessor<BaseDeconstructed> { }
public class PieceDeconstructedProcessor : BuildProcessor<PieceDeconstructed> { }
public class WaterParkDeconstructedProcessor : BuildProcessor<WaterParkDeconstructed> { }
public class LargeWaterParkDeconstructedProcessor : BuildProcessor<LargeWaterParkDeconstructed> { }

View File

@@ -0,0 +1,31 @@
using System.Collections.Generic;
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic.Bases;
using NitroxClient.GameLogic.Settings;
using NitroxModel.DataStructures;
using NitroxModel.Packets;
namespace NitroxClient.Communication.Packets.Processors;
public class BuildingDesyncWarningProcessor : ClientPacketProcessor<BuildingDesyncWarning>
{
public override void Process(BuildingDesyncWarning packet)
{
if (!BuildingHandler.Main)
{
return;
}
foreach (KeyValuePair<NitroxId, int> operation in packet.Operations)
{
OperationTracker tracker = BuildingHandler.Main.EnsureTracker(operation.Key);
tracker.LastOperationId = operation.Value;
tracker.FailedOperations++;
}
if (NitroxPrefs.SafeBuildingLog.Value)
{
Log.InGame(Language.main.Get("Nitrox_BuildingDesyncDetected"));
}
}
}

View File

@@ -0,0 +1,179 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxClient.GameLogic.Bases;
using NitroxClient.GameLogic.Spawning.Bases;
using NitroxClient.GameLogic.Spawning.Metadata;
using NitroxClient.MonoBehaviours;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.GameLogic.Entities.Bases;
using NitroxModel.Packets;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
namespace NitroxClient.Communication.Packets.Processors;
public class BuildingResyncProcessor : ClientPacketProcessor<BuildingResync>
{
private readonly Entities entities;
private readonly EntityMetadataManager entityMetadataManager;
public BuildingResyncProcessor(Entities entities, EntityMetadataManager entityMetadataManager)
{
this.entities = entities;
this.entityMetadataManager = entityMetadataManager;
}
public override void Process(BuildingResync packet)
{
if (!BuildingHandler.Main)
{
return;
}
BuildingHandler.Main.StartCoroutine(ResyncBuildingEntities(packet.BuildEntities, packet.ModuleEntities));
}
public IEnumerator ResyncBuildingEntities(Dictionary<BuildEntity, int> buildEntities, Dictionary<ModuleEntity, int> moduleEntities)
{
Stopwatch stopwatch = Stopwatch.StartNew();
BuildingHandler.Main.StartResync(buildEntities);
yield return UpdateEntities<Base, BuildEntity>(buildEntities.Keys.ToList(), OverwriteBase, IsInCloseProximity).OnYieldError(exception => Log.Error(exception, $"Encountered an exception while resyncing BuildEntities"));
BuildingHandler.Main.StartResync(moduleEntities);
yield return UpdateEntities<Constructable, ModuleEntity>(moduleEntities.Keys.ToList(), OverwriteModule, IsInCloseProximity).OnYieldError(exception => Log.Error(exception, $"Encountered an exception while resyncing ModuleEntities"));
BuildingHandler.Main.StopResync();
stopwatch.Stop();
int totalEntities = buildEntities.Count + moduleEntities.Count;
Log.InGame(Language.main.Get("Nitrox_FinishedResyncRequest").Replace("{TIME}", stopwatch.ElapsedMilliseconds.ToString()).Replace("{COUNT}", totalEntities.ToString()));
}
private bool IsInCloseProximity<C>(WorldEntity entity, C componentInWorld) where C : Component
{
return Vector3.Distance(entity.Transform.Position.ToUnity(), componentInWorld.transform.position) < 0.001f;
}
/// <summary>
/// Tries to overwrite components of the provided type found in GlobalRoot's hierarchy by the provided list of entities to update.
/// If no component is found to be corresponding to a provided entity, the entity will be spawned independently.
/// Other components of the provided type which weren't updated shall be destroyed.
/// </summary>
/// <remarks>
/// The provided list is modified by the function. Make sure it's not used somewhere else.
/// </remarks>
/// <typeparam name="C">The Unity component to be looked for</typeparam>
/// <typeparam name="E">The GlobalRootEntity type which will be updated</typeparam>
/// <param name="overwrite">A function to overwrite a given component by a given entity</param>
/// <param name="correspondingPredicate">
/// Predicate to determine if an entity can overwrite the GameObject of the provided component.
/// </param>
public IEnumerator UpdateEntities<C,E>(List<E> entitiesToUpdate, Func<C, E, IEnumerator> overwrite, Func<E, C, bool> correspondingPredicate) where C : Component where E : GlobalRootEntity
{
List<C> unmarkedComponents = new();
Dictionary<NitroxId, E> entitiesToUpdateById = entitiesToUpdate.ToDictionary(e => e.Id);
foreach (Transform childTransform in LargeWorldStreamer.main.globalRoot.transform)
{
if (childTransform.TryGetComponent(out C component))
{
if (component.TryGetNitroxId(out NitroxId id) && entitiesToUpdateById.TryGetValue(id, out E correspondingEntity))
{
yield return overwrite(component, correspondingEntity).OnYieldError(Log.Error);
entitiesToUpdate.Remove(correspondingEntity);
continue;
}
unmarkedComponents.Add(component);
}
}
for (int i = entitiesToUpdate.Count - 1; i >= 0; i--)
{
E entity = entitiesToUpdate[i];
C associatedComponent = unmarkedComponents.Find(c =>
correspondingPredicate(entity, c));
yield return overwrite(associatedComponent, entity).OnYieldError(Log.Error);
unmarkedComponents.Remove(associatedComponent);
entitiesToUpdate.RemoveAt(i);
}
for (int i = unmarkedComponents.Count - 1; i >= 0; i--)
{
Log.Info($"[{typeof(E)} RESYNC] Destroyed GameObject {unmarkedComponents[i].gameObject}");
GameObject.Destroy(unmarkedComponents[i].gameObject);
}
foreach (E entity in entitiesToUpdate)
{
Log.Info($"[{typeof(E)} RESYNC] spawning entity {entity.Id}");
yield return entities.SpawnEntityAsync(entity).OnYieldError(Log.Error);
}
}
public IEnumerator OverwriteBase(Base @base, BuildEntity buildEntity)
{
Log.Info($"[Base RESYNC] Overwriting base with id {buildEntity.Id}");
ClearBaseChildren(@base);
// Frame to let all children be deleted properly
yield return Yielders.WaitForEndOfFrame;
yield return BuildEntitySpawner.SetupBase(buildEntity, @base, entities);
yield return MoonpoolManager.RestoreMoonpools(buildEntity.ChildEntities.OfType<MoonpoolEntity>(), @base);
yield return entities.SpawnBatchAsync(buildEntity.ChildEntities.OfType<PlayerWorldEntity>().ToList<Entity>(), false, false);
foreach (Entity childEntity in buildEntity.ChildEntities)
{
switch (childEntity)
{
case MapRoomEntity mapRoomEntity:
yield return InteriorPieceEntitySpawner.RestoreMapRoom(@base, mapRoomEntity);
break;
case BaseLeakEntity baseLeakEntity:
yield return entities.SpawnEntityAsync(baseLeakEntity, true);
break;
}
}
}
public IEnumerator OverwriteModule(Constructable constructable, ModuleEntity moduleEntity)
{
Log.Info($"[Module RESYNC] Overwriting module with id {moduleEntity.Id}");
ModuleEntitySpawner.ApplyModuleData(moduleEntity, constructable.gameObject);
entityMetadataManager.ApplyMetadata(constructable.gameObject, moduleEntity.Metadata);
yield break;
}
/// <summary>
/// Destroys manually ghosts, modules, interior pieces and vehicles of a base
/// </summary>
/// <remarks>
/// This is the destructive way of clearing the base, if the base isn't modified consequently, IBaseModuleGeometry under the base cells may start spamming errors.
/// </remarks>
public static void ClearBaseChildren(Base @base)
{
for (int i = @base.transform.childCount - 1; i >= 0; i--)
{
Transform child = @base.transform.GetChild(i);
if (child.GetComponent<IBaseModule>().AliveOrNull() || child.GetComponent<Constructable>())
{
UnityEngine.Object.Destroy(child.gameObject);
}
}
foreach (VehicleDockingBay vehicleDockingBay in @base.GetComponentsInChildren<VehicleDockingBay>(true))
{
if (vehicleDockingBay.dockedVehicle)
{
UnityEngine.Object.Destroy(vehicleDockingBay.dockedVehicle.gameObject);
vehicleDockingBay.SetVehicleUndocked();
}
}
}
}

View File

@@ -0,0 +1,82 @@
using System;
using System.Linq;
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxClient.GameLogic.ChatUI;
using NitroxClient.GameLogic.Settings;
using NitroxModel.DataStructures.Unity;
using NitroxModel.DataStructures.Util;
using NitroxModel.Packets;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
namespace NitroxClient.Communication.Packets.Processors
{
class ChatMessageProcessor : ClientPacketProcessor<ChatMessage>
{
private readonly PlayerManager remotePlayerManager;
private readonly LocalPlayer localPlayer;
private readonly PlayerChatManager playerChatManager;
private readonly Color32 serverMessageColor = new Color32(0x8c, 0x00, 0xFF, 0xFF);
public ChatMessageProcessor(PlayerManager remotePlayerManager, LocalPlayer localPlayer, PlayerChatManager playerChatManager)
{
this.remotePlayerManager = remotePlayerManager;
this.localPlayer = localPlayer;
this.playerChatManager = playerChatManager;
}
public override void Process(ChatMessage message)
{
if (message.PlayerId != ChatMessage.SERVER_ID)
{
LogClientMessage(message);
}
else
{
LogServerMessage(message);
}
}
private void LogClientMessage(ChatMessage message)
{
// The message can come from either the local player or other players
string playerName;
NitroxColor color;
if (localPlayer.PlayerId == message.PlayerId)
{
playerName = localPlayer.PlayerName;
color = localPlayer.PlayerSettings.PlayerColor;
}
else
{
Optional<RemotePlayer> remotePlayer = remotePlayerManager.Find(message.PlayerId);
if (!remotePlayer.HasValue)
{
string playerTableFormatted = string.Join("\n", remotePlayerManager.GetAll().Select(ply => $"Name: '{ply.PlayerName}', Id: {ply.PlayerId}"));
Log.Error($"Tried to add chat message for remote player that could not be found with id '${message.PlayerId}' and message: '{message.Text}'.\nAll remote players right now:\n{playerTableFormatted}");
throw new Exception($"Tried to add chat message for remote player that could not be found with id '${message.PlayerId}' and message: '{message.Text}'.\nAll remote players right now:\n{playerTableFormatted}");
}
playerName = remotePlayer.Value.PlayerName;
color = remotePlayer.Value.PlayerSettings.PlayerColor;
}
playerChatManager.AddMessage(playerName, message.Text, color.ToUnity());
if (!NitroxPrefs.SilenceChat.Value)
{
playerChatManager.ShowChat();
}
}
private void LogServerMessage(ChatMessage message)
{
playerChatManager.AddMessage("Server", message.Text, serverMessageColor);
if (!NitroxPrefs.SilenceChat.Value)
{
playerChatManager.ShowChat();
}
}
}
}

View File

@@ -0,0 +1,20 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxModel.Packets;
namespace NitroxClient.Communication.Packets.Processors;
public class CreatureActionProcessor : ClientPacketProcessor<CreatureActionChanged>
{
private readonly AI ai;
public CreatureActionProcessor(AI ai)
{
this.ai = ai;
}
public override void Process(CreatureActionChanged packet)
{
ai.CreatureActionChanged(packet.CreatureId, packet.CreatureActionType);
}
}

View File

@@ -0,0 +1,13 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxModel.Packets;
namespace NitroxClient.Communication.Packets.Processors;
public class CreaturePoopPerformedProcessor : ClientPacketProcessor<CreaturePoopPerformed>
{
public override void Process(CreaturePoopPerformed packet)
{
AI.CreaturePoopPerformed(packet.CreatureId);
}
}

View File

@@ -0,0 +1,31 @@
using NitroxClient.Communication.Abstract;
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.MonoBehaviours;
using NitroxClient.Unity.Helper;
using NitroxModel_Subnautica.Packets;
using UnityEngine;
namespace NitroxClient.Communication.Packets.Processors
{
public class CyclopsDamagePointHealthChangedProcessor : ClientPacketProcessor<CyclopsDamagePointRepaired>
{
private readonly IPacketSender packetSender;
public CyclopsDamagePointHealthChangedProcessor(IPacketSender packetSender)
{
this.packetSender = packetSender;
}
public override void Process(CyclopsDamagePointRepaired packet)
{
GameObject gameObject = NitroxEntity.RequireObjectFrom(packet.Id);
SubRoot cyclops = gameObject.RequireComponent<SubRoot>();
using (PacketSuppressor<CyclopsDamage>.Suppress())
using (PacketSuppressor<CyclopsDamagePointRepaired>.Suppress())
{
cyclops.damageManager.damagePoints[packet.DamagePointIndex].liveMixin.AddHealth(packet.RepairAmount);
}
}
}
}

View File

@@ -0,0 +1,202 @@
using System.Collections.Generic;
using System.Linq;
using NitroxClient.Communication.Abstract;
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxClient.MonoBehaviours;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures;
using NitroxModel.Packets;
using NitroxModel_Subnautica.DataStructures.GameLogic;
using NitroxModel_Subnautica.Packets;
using UnityEngine;
namespace NitroxClient.Communication.Packets.Processors
{
/// <summary>
/// Add/remove <see cref="CyclopsDamagePoint"/>s and <see cref="Fire"/>s to match the <see cref="CyclopsDamage"/> packet received
/// </summary>
public class CyclopsDamageProcessor : ClientPacketProcessor<CyclopsDamage>
{
private readonly IPacketSender packetSender;
private readonly Fires fires;
public CyclopsDamageProcessor(IPacketSender packetSender, Fires fires)
{
this.packetSender = packetSender;
this.fires = fires;
}
public override void Process(CyclopsDamage packet)
{
SubRoot subRoot = NitroxEntity.RequireObjectFrom(packet.Id).GetComponent<SubRoot>();
using (PacketSuppressor<CyclopsDamagePointRepaired>.Suppress())
{
SetActiveDamagePoints(subRoot, packet.DamagePointIndexes);
}
using (PacketSuppressor<FireDoused>.Suppress())
{
SetActiveRoomFires(subRoot, packet.RoomFires);
}
LiveMixin subHealth = subRoot.gameObject.RequireComponent<LiveMixin>();
float oldHPPercent = subRoot.oldHPPercent;
// Client side noises. Not necessary for keeping the health synced
if (subHealth.GetHealthFraction() < 0.5f && oldHPPercent >= 0.5f)
{
subRoot.voiceNotificationManager.PlayVoiceNotification(subRoot.hullLowNotification, true, false);
}
else if (subHealth.GetHealthFraction() < 0.25f && oldHPPercent >= 0.25f)
{
subRoot.voiceNotificationManager.PlayVoiceNotification(subRoot.hullCriticalNotification, true, false);
}
using (PacketSuppressor<CyclopsDamage>.Suppress())
{
// Not necessary, but used by above code whenever damage is done
subRoot.oldHPPercent = subHealth.GetHealthFraction();
// Apply the actual health changes
subRoot.gameObject.RequireComponent<LiveMixin>().health = packet.SubHealth;
subRoot.gameObject.RequireComponentInChildren<CyclopsExternalDamageManager>().subLiveMixin.health = packet.DamageManagerHealth;
subRoot.gameObject.RequireComponent<SubFire>().liveMixin.health = packet.SubFireHealth;
}
}
/// <summary>
/// Add/remove <see cref="CyclopsDamagePoint"/>s until it matches the <paramref name="damagePointIndexes"/> array passed. Can trigger <see cref="CyclopsDamagePointRepaired"/> packets
/// </summary>
private void SetActiveDamagePoints(SubRoot cyclops, int[] damagePointIndexes)
{
CyclopsExternalDamageManager damageManager = cyclops.gameObject.RequireComponentInChildren<CyclopsExternalDamageManager>();
List<CyclopsDamagePoint> unusedDamagePoints = damageManager.unusedDamagePoints;
// CyclopsExternalDamageManager.damagePoints is an unchanged list. It will never have items added/removed from it. Since packet.DamagePointIndexes is also an array
// generated in an ordered manner, we can match them without worrying about unordered items.
if (damagePointIndexes != null && damagePointIndexes.Length > 0)
{
int packetDamagePointsIndex = 0;
for (int damagePointsIndex = 0; damagePointsIndex < damageManager.damagePoints.Length; damagePointsIndex++)
{
// Loop over all of the packet.DamagePointIndexes as long as there's more to match
if (packetDamagePointsIndex < damagePointIndexes.Length
&& damagePointIndexes[packetDamagePointsIndex] == damagePointsIndex)
{
if (!damageManager.damagePoints[damagePointsIndex].gameObject.activeSelf)
{
// Copied from CyclopsExternalDamageManager.CreatePoint(), except without the random index pick.
damageManager.damagePoints[damagePointsIndex].gameObject.SetActive(true);
damageManager.damagePoints[damagePointsIndex].RestoreHealth();
GameObject prefabGo = damageManager.fxPrefabs[UnityEngine.Random.Range(0, damageManager.fxPrefabs.Length - 1)];
damageManager.damagePoints[damagePointsIndex].SpawnFx(prefabGo);
unusedDamagePoints.Remove(damageManager.damagePoints[damagePointsIndex]);
}
packetDamagePointsIndex++;
}
else
{
// If it's active, but not in the list, it must have been repaired.
if (damageManager.damagePoints[damagePointsIndex].gameObject.activeSelf)
{
RepairDamagePoint(cyclops, damagePointsIndex, 999);
}
}
}
// Looks like the list came in unordered. I've uttered "That shouldn't happen" enough to do sanity checks for what should be impossible.
if (packetDamagePointsIndex < damagePointIndexes.Length)
{
Log.Error($"[CyclopsDamageProcessor packet.DamagePointIds did not fully iterate! Id: {damagePointIndexes[packetDamagePointsIndex]} had no matching Id in damageManager.damagePoints, or the order is incorrect!]");
}
}
else
{
// None should be active.
for (int i = 0; i < damageManager.damagePoints.Length; i++)
{
if (damageManager.damagePoints[i].gameObject.activeSelf)
{
RepairDamagePoint(cyclops, i, 999);
}
}
}
// unusedDamagePoints is checked against damagePoints to determine if there's enough damage points. Failing to set the new list
// of unusedDamagePoints will cause random DamagePoints to appear.
damageManager.unusedDamagePoints = unusedDamagePoints;
// Visual update only to show the water leaking through the window and various hull points based on missing health.
damageManager.ToggleLeakPointsBasedOnDamage();
}
/// <summary>
/// Add/remove fires until it matches the <paramref name="roomFires"/> array. Can trigger <see cref="FireDoused"/> packets
/// </summary>
private void SetActiveRoomFires(SubRoot subRoot, CyclopsFireData[] roomFires)
{
SubFire subFire = subRoot.gameObject.RequireComponent<SubFire>();
Dictionary<CyclopsRooms, SubFire.RoomFire> roomFiresDict = subFire.roomFires;
if (!subRoot.TryGetIdOrWarn(out NitroxId subRootId))
{
return;
}
if (roomFires != null && roomFires.Length > 0)
{
// Removing and adding fires will happen in the same loop
foreach (KeyValuePair<CyclopsRooms, SubFire.RoomFire> keyValuePair in roomFiresDict)
{
for (int nodeIndex = 0; nodeIndex < keyValuePair.Value.spawnNodes.Length; nodeIndex++)
{
CyclopsFireData fireNode = roomFires.SingleOrDefault(x => x.Room == keyValuePair.Key && x.NodeIndex == nodeIndex);
// If there's a matching node index, add a fire if there isn't one already. Otherwise remove a fire if there is one
if (fireNode == null)
{
if (keyValuePair.Value.spawnNodes[nodeIndex].childCount > 0)
{
keyValuePair.Value.spawnNodes[nodeIndex].GetComponentInChildren<Fire>().Douse(10000);
}
}
else
{
if (keyValuePair.Value.spawnNodes[nodeIndex].childCount < 1)
{
fires.Create(new CyclopsFireData(fireNode.FireId, subRootId, fireNode.Room, fireNode.NodeIndex));
}
}
}
}
}
// Clear out the fires, there should be none active
else
{
foreach (KeyValuePair<CyclopsRooms, SubFire.RoomFire> keyValuePair in roomFiresDict)
{
foreach (Transform spawnNode in keyValuePair.Value.spawnNodes)
{
if (spawnNode.childCount > 0)
{
spawnNode.GetComponentInChildren<Fire>().Douse(10000);
}
}
}
}
}
/// <summary>
/// Set the health of a <see cref="CyclopsDamagePoint"/>. This can trigger sending <see cref="CyclopsDamagePointRepaired"/> packets
/// </summary>
/// <param name="repairAmount">The max health of the point is 1. 999 is passed to trigger a full repair of the <see cref="CyclopsDamagePoint"/></param>
private void RepairDamagePoint(SubRoot subRoot, int damagePointIndex, float repairAmount)
{
subRoot.damageManager.damagePoints[damagePointIndex].liveMixin.AddHealth(repairAmount);
}
}
}

View File

@@ -0,0 +1,24 @@
using NitroxClient.Communication.Abstract;
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxModel_Subnautica.Packets;
namespace NitroxClient.Communication.Packets.Processors
{
class CyclopsDecoyLaunchProcessor : ClientPacketProcessor<CyclopsDecoyLaunch>
{
private readonly IPacketSender packetSender;
private readonly Cyclops cyclops;
public CyclopsDecoyLaunchProcessor(IPacketSender packetSender, Cyclops cyclops)
{
this.packetSender = packetSender;
this.cyclops = cyclops;
}
public override void Process(CyclopsDecoyLaunch decoyLaunchPacket)
{
cyclops.LaunchDecoy(decoyLaunchPacket.Id);
}
}
}

View File

@@ -0,0 +1,24 @@
using NitroxClient.Communication.Abstract;
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxModel_Subnautica.Packets;
namespace NitroxClient.Communication.Packets.Processors
{
public class CyclopsFireCreatedProcessor : ClientPacketProcessor<CyclopsFireCreated>
{
private readonly IPacketSender packetSender;
private readonly Fires fires;
public CyclopsFireCreatedProcessor(IPacketSender packetSender, Fires fires)
{
this.packetSender = packetSender;
this.fires = fires;
}
public override void Process(CyclopsFireCreated packet)
{
fires.Create(packet.FireCreatedData);
}
}
}

View File

@@ -0,0 +1,24 @@
using NitroxClient.Communication.Abstract;
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxModel_Subnautica.Packets;
namespace NitroxClient.Communication.Packets.Processors
{
public class CyclopsFireSuppressionProcessor : ClientPacketProcessor<CyclopsFireSuppression>
{
private readonly IPacketSender packetSender;
private readonly Cyclops cyclops;
public CyclopsFireSuppressionProcessor(IPacketSender packetSender, Cyclops cyclops)
{
this.packetSender = packetSender;
this.cyclops = cyclops;
}
public override void Process(CyclopsFireSuppression fireSuppressionPacket)
{
cyclops.StartFireSuppression(fireSuppressionPacket.Id);
}
}
}

View File

@@ -0,0 +1,19 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxModel.DataStructures.Unity;
using NitroxModel.Packets;
using UnityEngine;
namespace NitroxClient.Communication.Packets.Processors
{
class DebugStartMapProcessor : ClientPacketProcessor<DebugStartMapPacket>
{
public override void Process(DebugStartMapPacket packet)
{
foreach (NitroxVector3 position in packet.StartPositions)
{
GameObject prim = GameObject.CreatePrimitive(PrimitiveType.Cube);
prim.transform.position = new Vector3(position.X, position.Y + 10, position.Z);
}
}
}
}

View File

@@ -0,0 +1,43 @@
using NitroxClient.Communication.Abstract;
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic.Helper;
using NitroxClient.MonoBehaviours;
using NitroxModel.Packets;
using UnityEngine;
using static NitroxClient.GameLogic.Helper.TransientLocalObjectManager;
namespace NitroxClient.Communication.Packets.Processors
{
public class DeconstructionBeginProcessor : ClientPacketProcessor<DeconstructionBegin>
{
private readonly IPacketSender packetSender;
public DeconstructionBeginProcessor(IPacketSender packetSender)
{
this.packetSender = packetSender;
}
public override void Process(DeconstructionBegin packet)
{
Log.Info($"Received deconstruction packet for id: {packet.Id}");
GameObject deconstructing = NitroxEntity.RequireObjectFrom(packet.Id);
Constructable constructable = deconstructing.GetComponent<Constructable>();
BaseDeconstructable baseDeconstructable = deconstructing.GetComponent<BaseDeconstructable>();
using (PacketSuppressor<DeconstructionBegin>.Suppress())
{
if (baseDeconstructable != null)
{
TransientLocalObjectManager.Add(TransientObjectType.LATEST_DECONSTRUCTED_BASE_PIECE_GUID, packet.Id);
baseDeconstructable.Deconstruct();
}
else if (constructable != null)
{
constructable.SetState(false, false);
}
}
}
}
}

View File

@@ -0,0 +1,36 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxClient.GameLogic.HUD;
using NitroxModel.DataStructures.Util;
using NitroxModel.Packets;
namespace NitroxClient.Communication.Packets.Processors
{
class DisconnectProcessor : ClientPacketProcessor<Disconnect>
{
private readonly PlayerManager remotePlayerManager;
private readonly PlayerVitalsManager vitalsManager;
public DisconnectProcessor(PlayerManager remotePlayerManager, PlayerVitalsManager vitalsManager)
{
this.remotePlayerManager = remotePlayerManager;
this.vitalsManager = vitalsManager;
}
public override void Process(Disconnect disconnect)
{
// TODO: don't remove right away... maybe grey out and start
// a coroutine to finally remove.
vitalsManager.RemoveForPlayer(disconnect.PlayerId);
Optional<RemotePlayer> remotePlayer = remotePlayerManager.Find(disconnect.PlayerId);
if (remotePlayer.HasValue)
{
remotePlayer.Value.PlayerDisconnectEvent.Trigger(remotePlayer.Value);
remotePlayerManager.RemovePlayer(disconnect.PlayerId);
Log.Info($"{remotePlayer.Value.PlayerName} disconnected");
Log.InGame(Language.main.Get("Nitrox_PlayerDisconnected").Replace("{PLAYER}", remotePlayer.Value.PlayerName));
}
}
}
}

View File

@@ -0,0 +1,13 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.MonoBehaviours.Discord;
using NitroxModel.Packets;
namespace NitroxClient.Communication.Packets.Processors;
public class DiscordRequestIPProcessor : ClientPacketProcessor<DiscordRequestIP>
{
public override void Process(DiscordRequestIP packet)
{
DiscordClient.UpdateIpPort(packet.IpPort);
}
}

View File

@@ -0,0 +1,20 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxModel.Packets;
namespace NitroxClient.Communication.Packets.Processors;
public class DropSimulationOwnershipProcessor : ClientPacketProcessor<DropSimulationOwnership>
{
private readonly SimulationOwnership simulationOwnershipManager;
public DropSimulationOwnershipProcessor(SimulationOwnership simulationOwnershipManager)
{
this.simulationOwnershipManager = simulationOwnershipManager;
}
public override void Process(DropSimulationOwnership packet)
{
simulationOwnershipManager.DropSimulationFrom(packet.EntityId);
}
}

View File

@@ -0,0 +1,95 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxClient.GameLogic.PlayerLogic;
using NitroxClient.MonoBehaviours;
using NitroxModel.Packets;
using UnityEngine;
namespace NitroxClient.Communication.Packets.Processors;
public class EntityDestroyedProcessor : ClientPacketProcessor<EntityDestroyed>
{
public const DamageType DAMAGE_TYPE_RUN_ORIGINAL = (DamageType)100;
private readonly Entities entities;
public EntityDestroyedProcessor(Entities entities)
{
this.entities = entities;
}
public override void Process(EntityDestroyed packet)
{
entities.RemoveEntity(packet.Id);
if (!NitroxEntity.TryGetObjectFrom(packet.Id, out GameObject gameObject))
{
entities.MarkForDeletion(packet.Id);
Log.Warn($"[{nameof(EntityDestroyedProcessor)}] Could not find entity with id: {packet.Id} to destroy.");
return;
}
using (PacketSuppressor<EntityDestroyed>.Suppress())
{
// This type of check could get out of control if there are many types with custom destroy logic. If we get a few more, move to separate processors.
if (gameObject.TryGetComponent(out Vehicle vehicle))
{
DestroyVehicle(vehicle);
}
else if (gameObject.TryGetComponent(out SubRoot subRoot))
{
DestroySubroot(subRoot);
}
else
{
Entities.DestroyObject(gameObject);
}
}
}
private void DestroySubroot(SubRoot subRoot)
{
DamageInfo damageInfo = new() { type = DAMAGE_TYPE_RUN_ORIGINAL };
if (subRoot.live.health > 0f)
{
// oldHPPercent must be in the interval [0; 0.25[ because else, SubRoot.OnTakeDamage will end up in the wrong else condition
subRoot.oldHPPercent = 0f;
subRoot.live.health = 0f;
subRoot.live.NotifyAllAttachedDamageReceivers(damageInfo);
subRoot.live.Kill();
}
// We use a specific DamageType so that the Prefix on this method will accept this call
subRoot.OnTakeDamage(damageInfo);
}
private void DestroyVehicle(Vehicle vehicle)
{
if (vehicle.GetPilotingMode()) //Check Local Object Have Player inside
{
vehicle.OnPilotModeEnd();
if (!Player.main.ToNormalMode(true))
{
Player.main.ToNormalMode(false);
Player.main.transform.parent = null;
}
}
foreach (RemotePlayerIdentifier identifier in vehicle.GetComponentsInChildren<RemotePlayerIdentifier>(true))
{
identifier.RemotePlayer.ResetStates();
}
if (vehicle.gameObject)
{
if (vehicle.destructionEffect)
{
GameObject gameObject = Object.Instantiate(vehicle.destructionEffect);
gameObject.transform.position = vehicle.transform.position;
gameObject.transform.rotation = vehicle.transform.rotation;
}
Object.Destroy(vehicle.gameObject);
}
}
}

View File

@@ -0,0 +1,34 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic.Spawning.Metadata;
using NitroxClient.GameLogic.Spawning.Metadata.Processor.Abstract;
using NitroxClient.MonoBehaviours;
using NitroxModel.DataStructures.Util;
using NitroxModel.Helper;
using NitroxModel.Packets;
using UnityEngine;
namespace NitroxClient.Communication.Packets.Processors;
public class EntityMetadataUpdateProcessor : ClientPacketProcessor<EntityMetadataUpdate>
{
private readonly EntityMetadataManager entityMetadataManager;
public EntityMetadataUpdateProcessor(EntityMetadataManager entityMetadataManager)
{
this.entityMetadataManager = entityMetadataManager;
}
public override void Process(EntityMetadataUpdate update)
{
if (!NitroxEntity.TryGetObjectFrom(update.Id, out GameObject gameObject))
{
entityMetadataManager.RegisterNewerMetadata(update.Id, update.NewValue);
return;
}
Optional<IEntityMetadataProcessor> metadataProcessor = entityMetadataManager.FromMetaData(update.NewValue);
Validate.IsTrue(metadataProcessor.HasValue, $"No processor found for EntityMetadata of type {update.NewValue.GetType()}");
metadataProcessor.Value.ProcessMetadata(gameObject, update.NewValue);
}
}

View File

@@ -0,0 +1,99 @@
using System;
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxClient.GameLogic.Helper;
using NitroxClient.MonoBehaviours;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.Util;
using NitroxModel.Packets;
using UnityEngine;
namespace NitroxClient.Communication.Packets.Processors;
public class EntityReparentedProcessor : ClientPacketProcessor<EntityReparented>
{
private readonly Entities entities;
public EntityReparentedProcessor(Entities entities)
{
this.entities = entities;
}
public override void Process(EntityReparented packet)
{
Optional<GameObject> entity = NitroxEntity.GetObjectFrom(packet.Id);
if (!entity.HasValue)
{
// In some cases, the affected entity may be pending spawning or out of range.
// we only require the parent (in this case, the visible entity is undergoing
// some change that must be shown, and if not is an error).
return;
}
GameObject newParent = NitroxEntity.RequireObjectFrom(packet.NewParentId);
if (entity.Value.TryGetComponent(out Pickupable pickupable))
{
WaterParkItem waterParkItem = pickupable.GetComponent<WaterParkItem>();
// If the entity is being parented to a WaterPark
if (newParent.TryGetComponent(out WaterPark waterPark))
{
// If the entity is already in a WaterPark
if (waterParkItem.currentWaterPark)
{
waterParkItem.SetWaterPark(waterPark);
return;
}
pickupable.SetVisible(false);
pickupable.Activate(false);
waterPark.AddItem(pickupable);
// The reparenting is automatic here so we don't need to continue
return;
}
// If the entity was parented to a WaterPark but is picked up by someone
else if (waterParkItem)
{
pickupable.Deactivate();
waterParkItem.SetWaterPark(null);
}
}
using (PacketSuppressor<EntityReparented>.Suppress())
{
Type entityType = entities.RequireEntityType(packet.Id);
// Move this to a resolver if there ends up being a lot of custom reparenting logic
if (entityType == typeof(InventoryItemEntity))
{
InventoryItemReparented(entity.Value, newParent);
}
else
{
PerformDefaultReparenting(entity.Value, newParent);
}
}
}
private void InventoryItemReparented(GameObject entity, GameObject newParent)
{
Optional<ItemsContainer> opContainer = InventoryContainerHelper.TryGetContainerByOwner(newParent);
if (!opContainer.HasValue)
{
Log.Error($"Could not find container field on GameObject {newParent.GetFullHierarchyPath()}");
return;
}
Pickupable pickupable = entity.RequireComponent<Pickupable>();
ItemsContainer container = opContainer.Value;
container.UnsafeAdd(new InventoryItem(pickupable));
}
private void PerformDefaultReparenting(GameObject entity, GameObject newParent)
{
entity.transform.SetParent(newParent.transform, false);
}
}

View File

@@ -0,0 +1,46 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxClient.MonoBehaviours;
using NitroxModel.Packets;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
using static NitroxModel.Packets.EntityTransformUpdates;
namespace NitroxClient.Communication.Packets.Processors;
public class EntityTransformUpdatesProcessor : ClientPacketProcessor<EntityTransformUpdates>
{
private readonly SimulationOwnership simulationOwnership;
public EntityTransformUpdatesProcessor(SimulationOwnership simulationOwnership)
{
this.simulationOwnership = simulationOwnership;
}
public override void Process(EntityTransformUpdates packet)
{
foreach (EntityTransformUpdate update in packet.Updates)
{
// We will cancel any position update attempt at one of our locked entities
if (!NitroxEntity.TryGetObjectFrom(update.Id, out GameObject gameObject) ||
simulationOwnership.HasAnyLockType(update.Id))
{
continue;
}
RemotelyControlled remotelyControlled = gameObject.EnsureComponent<RemotelyControlled>();
Vector3 position = update.Position.ToUnity();
Quaternion rotation = update.Rotation.ToUnity();
if (update is SplineTransformUpdate splineUpdate)
{
remotelyControlled.UpdateKnownSplineUser(position, rotation, splineUpdate.DestinationPosition.ToUnity(), splineUpdate.DestinationDirection.ToUnity(), splineUpdate.Velocity);
}
else
{
remotelyControlled.UpdateOrientation(position, rotation);
}
}
}
}

View File

@@ -0,0 +1,39 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxClient.MonoBehaviours;
using NitroxModel.DataStructures.Util;
using NitroxModel.Packets;
using UnityEngine;
namespace NitroxClient.Communication.Packets.Processors
{
public class EscapePodChangedProcessor : ClientPacketProcessor<EscapePodChanged>
{
private readonly PlayerManager remotePlayerManager;
public EscapePodChangedProcessor(PlayerManager remotePlayerManager)
{
this.remotePlayerManager = remotePlayerManager;
}
public override void Process(EscapePodChanged packet)
{
Optional<RemotePlayer> remotePlayer = remotePlayerManager.Find(packet.PlayerId);
if (remotePlayer.HasValue)
{
EscapePod escapePod = null;
if (packet.EscapePodId.HasValue)
{
GameObject sub = NitroxEntity.RequireObjectFrom(packet.EscapePodId.Value);
escapePod = sub.GetComponent<EscapePod>();
}
remotePlayer.Value.SetEscapePod(escapePod);
}
}
}
}

View File

@@ -0,0 +1,36 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxClient.MonoBehaviours;
using NitroxModel_Subnautica.DataStructures;
using NitroxModel_Subnautica.Packets;
using UnityEngine;
namespace NitroxClient.Communication.Packets.Processors;
public class ExosuitArmActionProcessor : ClientPacketProcessor<ExosuitArmActionPacket>
{
public override void Process(ExosuitArmActionPacket packet)
{
if (!NitroxEntity.TryGetObjectFrom(packet.ArmId, out GameObject gameObject))
{
Log.Error("Could not find exosuit arm");
return;
}
switch (packet.TechType)
{
case TechType.ExosuitClawArmModule:
ExosuitModuleEvent.UseClaw(gameObject.GetComponent<ExosuitClawArm>(), packet.ArmAction);
break;
case TechType.ExosuitDrillArmModule:
ExosuitModuleEvent.UseDrill(gameObject.GetComponent<ExosuitDrillArm>(), packet.ArmAction);
break;
case TechType.ExosuitGrapplingArmModule:
ExosuitModuleEvent.UseGrappling(gameObject.GetComponent<ExosuitGrapplingArm>(), packet.ArmAction, packet.OpVector?.ToUnity());
break;
default:
Log.Error($"Got an arm tech that is not handled: {packet.TechType} with action: {packet.ArmAction} for id {packet.ArmId}");
break;
}
}
}

View File

@@ -0,0 +1,28 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.MonoBehaviours;
using NitroxModel_Subnautica.DataStructures;
using NitroxModel.GameLogic.FMOD;
using NitroxModel.Packets;
namespace NitroxClient.Communication.Packets.Processors;
public class FMODAssetProcessor : ClientPacketProcessor<FMODAssetPacket>
{
private readonly FMODWhitelist fmodWhitelist;
public FMODAssetProcessor(FMODWhitelist fmodWhitelist)
{
this.fmodWhitelist = fmodWhitelist;
}
public override void Process(FMODAssetPacket packet)
{
if (!fmodWhitelist.TryGetSoundData(packet.AssetPath, out SoundData soundData))
{
Log.ErrorOnce($"[{nameof(FMODAssetProcessor)}] Whitelist has no item for {packet.AssetPath}.");
return;
}
FMODEmitterController.PlayEventOneShot(packet.AssetPath, soundData.Radius, packet.Position.ToUnity(), packet.Volume);
}
}

View File

@@ -0,0 +1,37 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.MonoBehaviours;
using NitroxModel.Packets;
using UnityEngine;
namespace NitroxClient.Communication.Packets.Processors;
public class FMODCustomEmitterProcessor : ClientPacketProcessor<FMODCustomEmitterPacket>
{
public override void Process(FMODCustomEmitterPacket packet)
{
if (!NitroxEntity.TryGetObjectFrom(packet.Id, out GameObject emitterControllerEntity))
{
Log.ErrorOnce($"[{nameof(FMODCustomEmitterProcessor)}] Couldn't find entity {packet.Id}");
return;
}
if (!emitterControllerEntity.TryGetComponent(out FMODEmitterController fmodEmitterController))
{
fmodEmitterController = emitterControllerEntity.AddComponent<FMODEmitterController>();
fmodEmitterController.LateRegisterEmitter();
}
using (PacketSuppressor<FMODCustomEmitterPacket>.Suppress())
using (PacketSuppressor<FMODCustomLoopingEmitterPacket>.Suppress())
{
if (packet.Play)
{
fmodEmitterController.PlayCustomEmitter(packet.AssetPath);
}
else
{
fmodEmitterController.StopCustomEmitter(packet.AssetPath);
}
}
}
}

View File

@@ -0,0 +1,26 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.MonoBehaviours;
using NitroxModel.Packets;
using UnityEngine;
namespace NitroxClient.Communication.Packets.Processors;
public class FMODCustomLoopingEmitterProcessor : ClientPacketProcessor<FMODCustomLoopingEmitterPacket>
{
public override void Process(FMODCustomLoopingEmitterPacket packet)
{
if (!NitroxEntity.TryGetObjectFrom(packet.Id, out GameObject emitterControllerObject))
{
Log.ErrorOnce($"[{nameof(FMODCustomLoopingEmitterProcessor)}] Couldn't find entity {packet.Id}");
return;
}
if (!emitterControllerObject.TryGetComponent(out FMODEmitterController fmodEmitterController))
{
fmodEmitterController = emitterControllerObject.AddComponent<FMODEmitterController>();
fmodEmitterController.LateRegisterEmitter();
}
fmodEmitterController.PlayCustomLoopingEmitter(packet.AssetPath);
}
}

View File

@@ -0,0 +1,33 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.MonoBehaviours;
using NitroxModel.Packets;
using UnityEngine;
namespace NitroxClient.Communication.Packets.Processors;
public class FMODEventInstanceProcessor : ClientPacketProcessor<FMODEventInstancePacket>
{
public override void Process(FMODEventInstancePacket packet)
{
if (!NitroxEntity.TryGetObjectFrom(packet.Id, out GameObject emitterControllerObject))
{
Log.ErrorOnce($"[{nameof(FMODEventInstanceProcessor)}] Couldn't find entity {packet.Id}");
return;
}
if (!emitterControllerObject.TryGetComponent(out FMODEmitterController fmodEmitterController))
{
fmodEmitterController = emitterControllerObject.AddComponent<FMODEmitterController>();
fmodEmitterController.LateRegisterEmitter();
}
if (packet.Play)
{
fmodEmitterController.PlayEventInstance(packet.AssetPath, packet.Volume);
}
else
{
fmodEmitterController.StopEventInstance(packet.AssetPath);
}
}
}

View File

@@ -0,0 +1,36 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.MonoBehaviours;
using NitroxModel.Packets;
using UnityEngine;
namespace NitroxClient.Communication.Packets.Processors;
public class FMODStudioEventEmitterProcessor : ClientPacketProcessor<FMODStudioEmitterPacket>
{
public override void Process(FMODStudioEmitterPacket packet)
{
if (!NitroxEntity.TryGetObjectFrom(packet.Id, out GameObject emitterControllerObject))
{
Log.ErrorOnce($"[{nameof(FMODStudioEventEmitterProcessor)}] Couldn't find entity {packet.Id}");
return;
}
if (!emitterControllerObject.TryGetComponent(out FMODEmitterController fmodEmitterController))
{
fmodEmitterController = emitterControllerObject.AddComponent<FMODEmitterController>();
fmodEmitterController.LateRegisterEmitter();
}
using (PacketSuppressor<FMODStudioEmitterPacket>.Suppress())
{
if (packet.Play)
{
fmodEmitterController.PlayStudioEmitter(packet.AssetPath);
}
else
{
fmodEmitterController.StopStudioEmitter(packet.AssetPath, packet.AllowFadeout);
}
}
}
}

View File

@@ -0,0 +1,21 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxModel.Packets;
namespace NitroxClient.Communication.Packets.Processors;
public class FastCheatChangedProcessor : ClientPacketProcessor<FastCheatChanged>
{
public override void Process(FastCheatChanged packet)
{
switch (packet.Cheat)
{
case FastCheatChanged.FastCheat.HATCH:
NoCostConsoleCommand.main.fastHatchCheat = packet.Value;
break;
case FastCheatChanged.FastCheat.GROW:
NoCostConsoleCommand.main.fastGrowCheat = packet.Value;
break;
}
}
}

View File

@@ -0,0 +1,33 @@
using NitroxClient.Communication.Abstract;
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.MonoBehaviours;
using NitroxClient.Unity.Helper;
using NitroxModel.Packets;
using UnityEngine;
namespace NitroxClient.Communication.Packets.Processors
{
public class FireDousedProcessor : ClientPacketProcessor<FireDoused>
{
private readonly IPacketSender packetSender;
public FireDousedProcessor(IPacketSender packetSender)
{
this.packetSender = packetSender;
}
/// <summary>
/// Finds and executes <see cref="Fire.Douse(float)"/>. If the fire is extinguished, it will pass a large float to trigger the private
/// <see cref="Fire.Extinguish()"/> method.
/// </summary>
public override void Process(FireDoused packet)
{
GameObject fireGameObject = NitroxEntity.RequireObjectFrom(packet.Id);
using (PacketSuppressor<FireDoused>.Suppress())
{
fireGameObject.RequireComponent<Fire>().Douse(packet.DouseAmount);
}
}
}
}

View File

@@ -0,0 +1,57 @@
using System;
using FMOD;
using FMOD.Studio;
using FMODUnity;
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxClient.GameLogic.FMOD;
using NitroxModel.DataStructures.Util;
using NitroxModel.GameLogic.FMOD;
using NitroxModel.Packets;
namespace NitroxClient.Communication.Packets.Processors;
public class FootstepPacketProcessor : ClientPacketProcessor<FootstepPacket>
{
private readonly PlayerManager remotePlayerManager;
private readonly Lazy<FootstepSounds> localFootstepSounds = new(() => Player.mainObject.GetComponent<FootstepSounds>());
private PARAMETER_ID fmodIndexSpeed = FMODUWE.invalidParameterId;
private readonly float footstepAudioRadius; // To modify this value, modify the last value in the SoundWhitelist_Subnautica.csv file
private const float FOOTSTEP_AUDIO_MAX_VOLUME = 0.5f;
public FootstepPacketProcessor(PlayerManager remotePlayerManager, FMODWhitelist whitelist)
{
this.remotePlayerManager = remotePlayerManager;
whitelist.TryGetSoundData("event:/player/footstep_precursor_base", out SoundData soundData);
footstepAudioRadius = soundData.Radius;
}
public override void Process(FootstepPacket packet)
{
Optional<RemotePlayer> player = remotePlayerManager.Find(packet.PlayerID);
if (player.HasValue)
{
FMODAsset asset = packet.AssetIndex switch
{
FootstepPacket.StepSounds.PRECURSOR => localFootstepSounds.Value.precursorInteriorSound,
FootstepPacket.StepSounds.METAL => localFootstepSounds.Value.metalSound,
FootstepPacket.StepSounds.LAND => localFootstepSounds.Value.landSound,
_ => null
};
EventInstance evt = FMODUWE.GetEvent(asset);
if (evt.isValid())
{
if (FMODUWE.IsInvalidParameterId(fmodIndexSpeed))
{
fmodIndexSpeed = FMODUWE.GetEventInstanceParameterIndex(evt, "speed");
}
ATTRIBUTES_3D attributes = player.Value.Body.To3DAttributes();
evt.set3DAttributes(attributes);
evt.setParameterValueByIndex(fmodIndexSpeed, player.Value.AnimationController.Velocity.magnitude);
evt.setVolume(FMODSystem.CalculateVolume(Player.mainObject.transform.position, player.Value.Body.transform.position, footstepAudioRadius, FOOTSTEP_AUDIO_MAX_VOLUME));
evt.start();
evt.release();
}
}
}
}

View File

@@ -0,0 +1,36 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxModel.Packets;
namespace NitroxClient.Communication.Packets.Processors;
public class GameModeChangedProcessor : ClientPacketProcessor<GameModeChanged>
{
private readonly LocalPlayer localPlayer;
private readonly PlayerManager playerManager;
public GameModeChangedProcessor(LocalPlayer localPlayer, PlayerManager playerManager)
{
this.localPlayer = localPlayer;
this.playerManager = playerManager;
}
public override void Process(GameModeChanged packet)
{
if (packet.AllPlayers || packet.PlayerId == localPlayer.PlayerId)
{
GameModeUtils.SetGameMode((GameModeOption)(int)packet.GameMode, GameModeOption.None);
}
if (packet.AllPlayers)
{
foreach (RemotePlayer remotePlayer in playerManager.GetAll())
{
remotePlayer.SetGameMode(packet.GameMode);
}
}
else if (playerManager.TryFind(packet.PlayerId, out RemotePlayer remotePlayer))
{
remotePlayer.SetGameMode(packet.GameMode);
}
}
}

View File

@@ -0,0 +1,118 @@
using System;
using System.Collections;
using System.Collections.Generic;
using NitroxClient.Communication.Abstract;
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic.InitialSync.Abstract;
using NitroxClient.MonoBehaviours;
using NitroxModel.Packets;
namespace NitroxClient.Communication.Packets.Processors
{
public class InitialPlayerSyncProcessor : ClientPacketProcessor<InitialPlayerSync>
{
private readonly IPacketSender packetSender;
private readonly HashSet<IInitialSyncProcessor> processors;
private readonly HashSet<Type> alreadyRan = new();
private InitialPlayerSync packet;
private WaitScreen.ManualWaitItem loadingMultiplayerWaitItem;
private WaitScreen.ManualWaitItem subWaitScreenItem;
private int cumulativeProcessorsRan;
private int processorsRanLastCycle;
public InitialPlayerSyncProcessor(IPacketSender packetSender, IEnumerable<IInitialSyncProcessor> processors)
{
this.packetSender = packetSender;
this.processors = processors.ToSet();
}
public override void Process(InitialPlayerSync packet)
{
this.packet = packet;
loadingMultiplayerWaitItem = WaitScreen.Add(Language.main.Get("Nitrox_SyncingWorld"));
cumulativeProcessorsRan = 0;
Multiplayer.Main.StartCoroutine(ProcessInitialSyncPacket(this, null));
}
private IEnumerator ProcessInitialSyncPacket(object sender, EventArgs eventArgs)
{
bool moreProcessorsToRun;
do
{
yield return Multiplayer.Main.StartCoroutine(RunPendingProcessors());
moreProcessorsToRun = alreadyRan.Count < processors.Count;
if (moreProcessorsToRun && processorsRanLastCycle == 0)
{
throw new Exception($"Detected circular dependencies in initial packet sync between: {GetRemainingProcessorsText()}");
}
} while (moreProcessorsToRun);
WaitScreen.Remove(loadingMultiplayerWaitItem);
Multiplayer.Main.InitialSyncCompleted = true;
// When the player finishes loading, we can take back his invincibility
Player.main.liveMixin.invincible = false;
Player.main.UnfreezeStats();
packetSender.Send(new PlayerSyncFinished());
}
private IEnumerator RunPendingProcessors()
{
processorsRanLastCycle = 0;
foreach (IInitialSyncProcessor processor in processors)
{
if (IsWaitingToRun(processor.GetType()) && HasDependenciesSatisfied(processor))
{
loadingMultiplayerWaitItem.SetProgress(cumulativeProcessorsRan, processors.Count);
alreadyRan.Add(processor.GetType());
processorsRanLastCycle++;
cumulativeProcessorsRan++;
Log.Info($"Running {processor.GetType()}");
subWaitScreenItem = WaitScreen.Add($"Running {processor.GetType().Name}");
yield return Multiplayer.Main.StartCoroutine(processor.Process(packet, subWaitScreenItem));
WaitScreen.Remove(subWaitScreenItem);
}
}
}
private bool HasDependenciesSatisfied(IInitialSyncProcessor processor)
{
foreach (Type dependentType in processor.DependentProcessors)
{
if (IsWaitingToRun(dependentType))
{
return false;
}
}
return true;
}
private bool IsWaitingToRun(Type processor)
{
return alreadyRan.Contains(processor) == false;
}
private string GetRemainingProcessorsText()
{
string remaining = "";
foreach (IInitialSyncProcessor processor in processors)
{
if (IsWaitingToRun(processor.GetType()))
{
remaining += $" {processor.GetType()}";
}
}
return remaining;
}
}
}

View File

@@ -0,0 +1,25 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxClient.MonoBehaviours;
using NitroxModel.DataStructures.Util;
using NitroxModel.Packets;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
namespace NitroxClient.Communication.Packets.Processors
{
class ItemPositionProcessor : ClientPacketProcessor<ItemPosition>
{
private const float ITEM_TRANSFORM_SMOOTH_PERIOD = 0.25f;
public override void Process(ItemPosition drop)
{
Optional<GameObject> opItem = NitroxEntity.GetObjectFrom(drop.Id);
if (opItem.HasValue)
{
MovementHelper.MoveRotateGameObject(opItem.Value, drop.Position.ToUnity(), drop.Rotation.ToUnity(), ITEM_TRANSFORM_SMOOTH_PERIOD);
}
}
}
}

View File

@@ -0,0 +1,37 @@
using System;
using NitroxClient.Communication.Abstract;
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxModel.Packets;
using NitroxModel_Subnautica.DataStructures;
namespace NitroxClient.Communication.Packets.Processors;
public class KnownTechEntryProcessorAdd : ClientPacketProcessor<KnownTechEntryAdd>
{
private readonly IPacketSender packetSender;
public KnownTechEntryProcessorAdd(IPacketSender packetSender)
{
this.packetSender = packetSender;
}
public override void Process(KnownTechEntryAdd packet)
{
using (PacketSuppressor<KnownTechEntryAdd>.Suppress())
{
switch (packet.Category)
{
case KnownTechEntryAdd.EntryCategory.KNOWN:
KnownTech.Add(packet.TechType.ToUnity(), packet.Verbose);
break;
case KnownTechEntryAdd.EntryCategory.ANALYZED:
KnownTech.Analyze(packet.TechType.ToUnity(), packet.Verbose);
break;
default:
string categoryName = Enum.GetName(typeof(KnownTechEntryAdd.EntryCategory), packet.Category);
Log.Error($"Received an unknown category type for {nameof(KnownTechEntryAdd)} packet: {categoryName}");
break;
}
}
}
}

View File

@@ -0,0 +1,17 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.MonoBehaviours;
using NitroxModel.Packets;
using NitroxModel_Subnautica.DataStructures;
namespace NitroxClient.Communication.Packets.Processors;
public class LeakRepairedProcessor : ClientPacketProcessor<LeakRepaired>
{
public override void Process(LeakRepaired packet)
{
if (NitroxEntity.TryGetComponentFrom(packet.BaseId, out BaseLeakManager baseLeakManager))
{
baseLeakManager.HealLeakToMax(packet.RelativeCell.ToUnity());
}
}
}

View File

@@ -0,0 +1,36 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic.FMOD;
using NitroxClient.MonoBehaviours;
using NitroxClient.Unity.Helper;
using NitroxModel.Packets;
using UnityEngine;
namespace NitroxClient.Communication.Packets.Processors;
public class MedicalCabinetClickedProcessor : ClientPacketProcessor<MedicalCabinetClicked>
{
public override void Process(MedicalCabinetClicked packet)
{
GameObject gameObject = NitroxEntity.RequireObjectFrom(packet.Id);
MedicalCabinet cabinet = gameObject.RequireComponent<MedicalCabinet>();
bool medkitPickedUp = !packet.HasMedKit && cabinet.hasMedKit;
bool doorChangedState = cabinet.doorOpen != packet.DoorOpen;
cabinet.hasMedKit = packet.HasMedKit;
cabinet.timeSpawnMedKit = packet.NextSpawnTime;
using (PacketSuppressor<FMODCustomEmitterPacket>.Suppress())
using (FMODSystem.SuppressSubnauticaSounds())
{
if (doorChangedState)
{
cabinet.Invoke(nameof(MedicalCabinet.ToggleDoorState), 0f);
}
else if (medkitPickedUp)
{
cabinet.Invoke(nameof(MedicalCabinet.ToggleDoorState), 2f);
}
}
}
}

View File

@@ -0,0 +1,22 @@
using NitroxClient.Communication.Abstract;
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxModel.Packets;
namespace NitroxClient.Communication.Packets.Processors
{
public class MultiplayerSessionPolicyProcessor : ClientPacketProcessor<MultiplayerSessionPolicy>
{
private readonly IMultiplayerSession multiplayerSession;
public MultiplayerSessionPolicyProcessor(IMultiplayerSession multiplayerSession)
{
this.multiplayerSession = multiplayerSession;
}
public override void Process(MultiplayerSessionPolicy packet)
{
Log.Info("Processing session policy information.");
multiplayerSession.ProcessSessionPolicy(packet);
}
}
}

View File

@@ -0,0 +1,21 @@
using NitroxClient.Communication.Abstract;
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxModel.Packets;
namespace NitroxClient.Communication.Packets.Processors
{
public class MultiplayerSessionReservationProcessor : ClientPacketProcessor<MultiplayerSessionReservation>
{
private readonly IMultiplayerSession multiplayerSession;
public MultiplayerSessionReservationProcessor(IMultiplayerSession multiplayerSession)
{
this.multiplayerSession = multiplayerSession;
}
public override void Process(MultiplayerSessionReservation packet)
{
multiplayerSession.ProcessReservationResponsePacket(packet);
}
}
}

View File

@@ -0,0 +1,30 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxModel.DataStructures.Util;
using NitroxModel.Packets;
namespace NitroxClient.Communication.Packets.Processors;
public class MutePlayerProcessor : ClientPacketProcessor<MutePlayer>
{
private readonly PlayerManager playerManager;
public delegate void PlayerMuted(ushort playerId, bool muted);
public PlayerMuted OnPlayerMuted;
public MutePlayerProcessor(PlayerManager playerManager)
{
this.playerManager = playerManager;
}
public override void Process(MutePlayer packet)
{
// We only need to notice if that's another player than local player
Optional<RemotePlayer> player = playerManager.Find(packet.PlayerId);
if (player.HasValue)
{
player.Value.PlayerContext.IsMuted = packet.Muted;
}
OnPlayerMuted(packet.PlayerId, packet.Muted);
}
}

View File

@@ -0,0 +1,30 @@
using NitroxClient.Communication.Abstract;
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.MonoBehaviours;
using NitroxClient.Unity.Helper;
using NitroxModel.Packets;
using UnityEngine;
namespace NitroxClient.Communication.Packets.Processors
{
public class OpenableStateChangedProcessor : ClientPacketProcessor<OpenableStateChanged>
{
private readonly IPacketSender packetSender;
public OpenableStateChangedProcessor(IPacketSender packetSender)
{
this.packetSender = packetSender;
}
public override void Process(OpenableStateChanged packet)
{
GameObject gameObject = NitroxEntity.RequireObjectFrom(packet.Id);
Openable openable = gameObject.RequireComponent<Openable>();
using (PacketSuppressor<OpenableStateChanged>.Suppress())
{
openable.PlayOpenAnimation(packet.IsOpen, packet.Duration);
}
}
}
}

View File

@@ -0,0 +1,23 @@
using NitroxClient.Communication.Abstract;
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxModel.Packets;
namespace NitroxClient.Communication.Packets.Processors;
public class PDAEncyclopediaEntryAddProcessor : ClientPacketProcessor<PDAEncyclopediaEntryAdd>
{
private readonly IPacketSender packetSender;
public PDAEncyclopediaEntryAddProcessor(IPacketSender packetSender)
{
this.packetSender = packetSender;
}
public override void Process(PDAEncyclopediaEntryAdd packet)
{
using (PacketSuppressor<PDAEncyclopediaEntryAdd>.Suppress())
{
PDAEncyclopedia.Add(packet.Key, packet.Verbose);
}
}
}

View File

@@ -0,0 +1,24 @@
using NitroxClient.Communication.Abstract;
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxModel.Packets;
namespace NitroxClient.Communication.Packets.Processors
{
public class PDALogEntryAddProcessor : ClientPacketProcessor<PDALogEntryAdd>
{
private readonly IPacketSender packetSender;
public PDALogEntryAddProcessor(IPacketSender packetSender)
{
this.packetSender = packetSender;
}
public override void Process(PDALogEntryAdd packet)
{
using (PacketSuppressor<PDALogEntryAdd>.Suppress())
{
PDALog.Add(packet.Key);
}
}
}
}

View File

@@ -0,0 +1,37 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxModel;
using NitroxModel.Packets;
using NitroxModel_Subnautica.DataStructures;
namespace NitroxClient.Communication.Packets.Processors;
public class PDAScanFinishedProcessor : ClientPacketProcessor<PDAScanFinished>
{
public override void Process(PDAScanFinished packet)
{
if (packet.Id != null)
{
StoryManager.ScanCompleted(packet.Id, packet.Destroy);
}
if (packet.WasAlreadyResearched)
{
return;
}
TechType packetTechType = packet.TechType.ToUnity();
if (packet.FullyResearched)
{
PDAScanner.partial.RemoveAllFast(packetTechType, static (item, techType) => item.techType == techType);
PDAScanner.complete.Add(packetTechType);
return;
}
if (PDAScanner.GetPartialEntryByKey(packetTechType, out PDAScanner.Entry entry))
{
entry.unlocked = packet.UnlockedAmount;
}
else
{
PDAScanner.Add(packetTechType, packet.UnlockedAmount);
}
}
}

View File

@@ -0,0 +1,25 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.Packets;
namespace NitroxClient.Communication.Packets.Processors;
public class PermsChangedProcessor : ClientPacketProcessor<PermsChanged>
{
private LocalPlayer localPlayer;
public delegate void PermissionsChanged(Perms perms);
public PermissionsChanged OnPermissionsChanged;
public PermsChangedProcessor(LocalPlayer localPlayer)
{
this.localPlayer = localPlayer;
}
public override void Process(PermsChanged packet)
{
localPlayer.Permissions = packet.NewPerms;
OnPermissionsChanged(packet.NewPerms);
}
}

View File

@@ -0,0 +1,26 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxModel;
using NitroxModel.Packets;
using Story;
namespace NitroxClient.Communication.Packets.Processors;
public class PlaySunbeamEventProcessor : ClientPacketProcessor<PlaySunbeamEvent>
{
public override void Process(PlaySunbeamEvent packet)
{
// TODO: Look into compound goals and OnUnlock goals to bring back the necessary ones
int beginIndex = PlaySunbeamEvent.SunbeamGoals.GetIndex(packet.EventKey);
if (beginIndex == -1)
{
Log.Error($"Couldn't find the corresponding sunbeam event in {nameof(PlaySunbeamEvent.SunbeamGoals)} for key {packet.EventKey}");
return;
}
for (int i = beginIndex; i < PlaySunbeamEvent.SunbeamGoals.Length; i++)
{
StoryGoalManager.main.completedGoals.Remove(PlaySunbeamEvent.SunbeamGoals[i]);
}
// Same execution as for StoryGoalCustomEventHandler commands
StoryGoalManager.main.OnGoalComplete(packet.EventKey);
}
}

View File

@@ -0,0 +1,46 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxClient.MonoBehaviours;
using NitroxClient.MonoBehaviours.CinematicController;
using NitroxModel.DataStructures.Util;
using NitroxModel.Helper;
using NitroxModel.Packets;
using UnityEngine;
namespace NitroxClient.Communication.Packets.Processors;
public class PlayerCinematicControllerCallProcessor : ClientPacketProcessor<PlayerCinematicControllerCall>
{
private readonly PlayerManager playerManager;
public PlayerCinematicControllerCallProcessor(PlayerManager playerManager)
{
this.playerManager = playerManager;
}
public override void Process(PlayerCinematicControllerCall packet)
{
if (!NitroxEntity.TryGetObjectFrom(packet.ControllerID, out GameObject entity))
{
return; // Entity can be not spawned yet bc async.
}
if (!entity.TryGetComponent(out MultiplayerCinematicReference reference))
{
Log.Warn($"Couldn't find {nameof(MultiplayerCinematicReference)} on {entity.name}:{packet.ControllerID}");
return;
}
Optional<RemotePlayer> opPlayer = playerManager.Find(packet.PlayerId);
Validate.IsPresent(opPlayer);
if (packet.StartPlaying)
{
reference.CallStartCinematicMode(packet.Key, packet.ControllerNameHash, opPlayer.Value);
}
else
{
reference.CallCinematicModeEnd(packet.Key, packet.ControllerNameHash, opPlayer.Value);
}
}
}

View File

@@ -0,0 +1,26 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxModel.Helper;
using NitroxModel.Packets;
namespace NitroxClient.Communication.Packets.Processors;
public class PlayerDeathProcessor : ClientPacketProcessor<PlayerDeathEvent>
{
private readonly PlayerManager playerManager;
public PlayerDeathProcessor(PlayerManager playerManager)
{
this.playerManager = playerManager;
}
public override void Process(PlayerDeathEvent playerDeath)
{
RemotePlayer player = Validate.IsPresent(playerManager.Find(playerDeath.PlayerId));
Log.Debug($"{player.PlayerName} died");
Log.InGame(Language.main.Get("Nitrox_PlayerDied").Replace("{PLAYER}", player.PlayerName));
player.PlayerDeathEvent.Trigger(player);
// TODO: Add any death related triggers (i.e. scoreboard updates, rewards, etc.)
}
}

View File

@@ -0,0 +1,119 @@
using System;
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxClient.MonoBehaviours;
using NitroxModel.DataStructures.Util;
using NitroxModel.Helper;
using NitroxModel.Packets;
using UnityEngine;
namespace NitroxClient.Communication.Packets.Processors;
public class PlayerHeldItemChangedProcessor : ClientPacketProcessor<PlayerHeldItemChanged>
{
private int defaultLayer;
private int viewModelLayer;
private readonly PlayerManager playerManager;
public PlayerHeldItemChangedProcessor(PlayerManager playerManager)
{
this.playerManager = playerManager;
if (NitroxEnvironment.IsNormal)
{
SetupLayers();
}
}
private void SetupLayers()
{
defaultLayer = LayerMask.NameToLayer("Default");
viewModelLayer = LayerMask.NameToLayer("Viewmodel");
}
public override void Process(PlayerHeldItemChanged packet)
{
Optional<RemotePlayer> opPlayer = playerManager.Find(packet.PlayerId);
Validate.IsPresent(opPlayer);
if (!NitroxEntity.TryGetObjectFrom(packet.ItemId, out GameObject item))
{
return; // Item can be not spawned yet bc async.
}
Pickupable pickupable = item.GetComponent<Pickupable>();
Validate.IsTrue(pickupable);
Validate.NotNull(pickupable.inventoryItem);
ItemsContainer inventory = opPlayer.Value.Inventory;
PlayerTool tool = item.GetComponent<PlayerTool>();
// Copied from QuickSlots
switch (packet.Type)
{
case PlayerHeldItemChanged.ChangeType.DRAW_AS_TOOL:
Validate.IsTrue(tool);
ModelPlug.PlugIntoSocket(tool, opPlayer.Value.ItemAttachPoint);
Utils.SetLayerRecursively(item, viewModelLayer);
foreach (Animator componentsInChild in tool.GetComponentsInChildren<Animator>())
{
componentsInChild.cullingMode = AnimatorCullingMode.AlwaysAnimate;
}
if (tool.mainCollider)
{
tool.mainCollider.enabled = false;
}
tool.GetComponent<Rigidbody>().isKinematic = true;
item.SetActive(true);
tool.SetHandIKTargetsEnabled(true);
SafeAnimator.SetBool(opPlayer.Value.ArmsController.GetComponent<Animator>(), $"holding_{tool.animToolName}", true);
opPlayer.Value.AnimationController["using_tool_first"] = packet.IsFirstTime == null;
if (item.TryGetComponent(out FPModel fpModelDraw)) //FPModel needs to be updated
{
fpModelDraw.OnEquip(null, null);
}
break;
case PlayerHeldItemChanged.ChangeType.HOLSTER_AS_TOOL:
Validate.IsTrue(tool);
item.SetActive(false);
Utils.SetLayerRecursively(item, defaultLayer);
if (tool.mainCollider)
{
tool.mainCollider.enabled = true;
}
tool.GetComponent<Rigidbody>().isKinematic = false;
pickupable.inventoryItem.item.Reparent(inventory.tr);
foreach (Animator componentsInChild in tool.GetComponentsInChildren<Animator>())
{
componentsInChild.cullingMode = AnimatorCullingMode.CullUpdateTransforms;
}
SafeAnimator.SetBool(opPlayer.Value.ArmsController.GetComponent<Animator>(), $"holding_{tool.animToolName}", false);
opPlayer.Value.AnimationController["using_tool_first"] = false;
if (item.TryGetComponent(out FPModel fpModelHolster)) //FPModel needs to be updated
{
fpModelHolster.OnUnequip(null, null);
}
break;
case PlayerHeldItemChanged.ChangeType.DRAW_AS_ITEM:
pickupable.inventoryItem.item.Reparent(opPlayer.Value.ItemAttachPoint);
pickupable.inventoryItem.item.SetVisible(true);
Utils.SetLayerRecursively(pickupable.inventoryItem.item.gameObject, viewModelLayer);
break;
case PlayerHeldItemChanged.ChangeType.HOLSTER_AS_ITEM:
pickupable.inventoryItem.item.Reparent(inventory.tr);
pickupable.inventoryItem.item.SetVisible(false);
Utils.SetLayerRecursively(pickupable.inventoryItem.item.gameObject, defaultLayer);
break;
default:
throw new ArgumentOutOfRangeException(nameof(packet.Type));
}
}
}

View File

@@ -0,0 +1,24 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxModel.Packets;
using NitroxModel_Subnautica.DataStructures;
namespace NitroxClient.Communication.Packets.Processors;
public class PlayerInCyclopsMovementProcessor : ClientPacketProcessor<PlayerInCyclopsMovement>
{
private readonly PlayerManager remotePlayerManager;
public PlayerInCyclopsMovementProcessor(PlayerManager remotePlayerManager)
{
this.remotePlayerManager = remotePlayerManager;
}
public override void Process(PlayerInCyclopsMovement movement)
{
if (remotePlayerManager.TryFind(movement.PlayerId, out RemotePlayer remotePlayer) && remotePlayer.Pawn != null)
{
remotePlayer.UpdatePositionInCyclops(movement.LocalPosition.ToUnity(), movement.LocalRotation.ToUnity());
}
}
}

View File

@@ -0,0 +1,33 @@
using System.Collections;
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxModel.Packets;
using UWE;
namespace NitroxClient.Communication.Packets.Processors;
public class PlayerJoinedMultiplayerSessionProcessor : ClientPacketProcessor<PlayerJoinedMultiplayerSession>
{
private readonly PlayerManager playerManager;
private readonly Entities entities;
public PlayerJoinedMultiplayerSessionProcessor(PlayerManager playerManager, Entities entities)
{
this.playerManager = playerManager;
this.entities = entities;
}
public override void Process(PlayerJoinedMultiplayerSession packet)
{
CoroutineHost.StartCoroutine(SpawnRemotePlayer(packet));
}
private IEnumerator SpawnRemotePlayer(PlayerJoinedMultiplayerSession packet)
{
playerManager.Create(packet.PlayerContext);
yield return entities.SpawnEntityAsync(packet.PlayerWorldEntity, true, true);
Log.Info($"{packet.PlayerContext.PlayerName} joined the game");
Log.InGame(Language.main.Get("Nitrox_PlayerJoined").Replace("{PLAYER}", packet.PlayerContext.PlayerName));
}
}

View File

@@ -0,0 +1,30 @@
using NitroxClient.Communication.Abstract;
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.MonoBehaviours.Gui.Modals;
using NitroxModel.Packets;
namespace NitroxClient.Communication.Packets.Processors
{
public class UserKickedProcessor : ClientPacketProcessor<PlayerKicked>
{
private readonly IMultiplayerSession session;
public UserKickedProcessor(IMultiplayerSession session)
{
this.session = session;
}
public override void Process(PlayerKicked packet)
{
string message = Language.main.Get("Nitrox_PlayerKicked");
if (!string.IsNullOrEmpty(packet.Reason))
{
message += $"\n {packet.Reason}";
}
session.Disconnect();
Modal.Get<KickedModal>()?.Show(message);
}
}
}

View File

@@ -0,0 +1,27 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxModel.Packets;
using NitroxModel_Subnautica.DataStructures;
namespace NitroxClient.Communication.Packets.Processors;
public class PlayerMovementProcessor : ClientPacketProcessor<PlayerMovement>
{
private readonly PlayerManager remotePlayerManager;
public PlayerMovementProcessor(PlayerManager remotePlayerManager)
{
this.remotePlayerManager = remotePlayerManager;
}
public override void Process(PlayerMovement movement)
{
if (remotePlayerManager.TryFind(movement.PlayerId, out RemotePlayer remotePlayer))
{
remotePlayer.UpdatePosition(movement.Position.ToUnity(),
movement.Velocity.ToUnity(),
movement.BodyRotation.ToUnity(),
movement.AimingRotation.ToUnity());
}
}
}

View File

@@ -0,0 +1,29 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxClient.MonoBehaviours.Gui.HUD;
using NitroxModel.Packets;
namespace NitroxClient.Communication.Packets.Processors;
public class PlayerStatsProcessor : ClientPacketProcessor<PlayerStats>
{
private readonly PlayerManager playerManager;
public PlayerStatsProcessor(PlayerManager playerManager)
{
this.playerManager = playerManager;
}
public override void Process(PlayerStats playerStats)
{
if (playerManager.TryFind(playerStats.PlayerId, out RemotePlayer remotePlayer))
{
RemotePlayerVitals vitals = remotePlayer.vitals;
vitals.SetOxygen(playerStats.Oxygen, playerStats.MaxOxygen);
vitals.SetHealth(playerStats.Health);
vitals.SetFood(playerStats.Food);
vitals.SetWater(playerStats.Water);
remotePlayer.UpdateHealthAndInfection(playerStats.Health, playerStats.InfectionAmount);
}
}
}

View File

@@ -0,0 +1,43 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.MonoBehaviours;
using NitroxClient.Unity.Helper;
using NitroxModel.Packets;
using NitroxModel_Subnautica.DataStructures;
using UWE;
using Terrain = NitroxClient.GameLogic.Terrain;
namespace NitroxClient.Communication.Packets.Processors;
public class PlayerTeleportedProcessor : ClientPacketProcessor<PlayerTeleported>
{
public override void Process(PlayerTeleported packet)
{
Player.main.OnPlayerPositionCheat();
Vehicle currentVehicle = Player.main.currentMountedVehicle;
if (currentVehicle)
{
currentVehicle.TeleportVehicle(packet.DestinationTo.ToUnity(), currentVehicle.transform.rotation);
Player.main.WaitForTeleportation();
return;
}
Player.main.SetPosition(packet.DestinationTo.ToUnity());
if (packet.SubRootID.HasValue && NitroxEntity.TryGetComponentFrom(packet.SubRootID.Value, out SubRoot subRoot))
{
Player.main.SetCurrentSub(subRoot, true);
return;
}
// Freeze the player while it's loading its new position
Player.main.cinematicModeActive = true;
Player.main.WaitForTeleportation();
CoroutineHost.StartCoroutine(Terrain.WaitForWorldLoad().OnYieldError(e =>
{
Player.main.cinematicModeActive = false;
Log.Warn($"Something wrong happened while waiting for the terrain to load.\n{e}");
}));
}
}

View File

@@ -0,0 +1,15 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxModel.Packets;
namespace NitroxClient.Communication.Packets.Processors;
public class PvPAttackProcessor : ClientPacketProcessor<PvPAttack>
{
public override void Process(PvPAttack packet)
{
if (Player.main && Player.main.liveMixin)
{
Player.main.liveMixin.TakeDamage(packet.Damage);
}
}
}

View File

@@ -0,0 +1,14 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxModel.Packets;
using Story;
namespace NitroxClient.Communication.Packets.Processors
{
public class RadioPlayPendingMessageProcessor : ClientPacketProcessor<RadioPlayPendingMessage>
{
public override void Process(RadioPlayPendingMessage packet)
{
StoryGoalManager.main.ExecutePendingRadioMessage();
}
}
}

View File

@@ -0,0 +1,13 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxModel.Packets;
namespace NitroxClient.Communication.Packets.Processors;
public class RangedAttackLastTargetUpdateProcessor : ClientPacketProcessor<RangedAttackLastTargetUpdate>
{
public override void Process(RangedAttackLastTargetUpdate packet)
{
AI.RangedAttackLastTargetUpdate(packet.CreatureId, packet.TargetId, packet.AttackTypeIndex, packet.State);
}
}

View File

@@ -0,0 +1,71 @@
using System.Collections;
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxClient.MonoBehaviours;
using NitroxModel.DataStructures;
using NitroxModel.Packets;
using NitroxModel_Subnautica.DataStructures;
using UWE;
namespace NitroxClient.Communication.Packets.Processors;
public class RemoveCreatureCorpseProcessor : ClientPacketProcessor<RemoveCreatureCorpse>
{
private readonly Entities entities;
private readonly LiveMixinManager liveMixinManager;
private readonly SimulationOwnership simulationOwnership;
public RemoveCreatureCorpseProcessor(Entities entities, LiveMixinManager liveMixinManager, SimulationOwnership simulationOwnership)
{
this.entities = entities;
this.liveMixinManager = liveMixinManager;
this.simulationOwnership = simulationOwnership;
}
public override void Process(RemoveCreatureCorpse packet)
{
entities.RemoveEntity(packet.CreatureId);
if (!NitroxEntity.TryGetComponentFrom(packet.CreatureId, out CreatureDeath creatureDeath))
{
entities.MarkForDeletion(packet.CreatureId);
Log.Warn($"[{nameof(RemoveCreatureCorpseProcessor)}] Could not find entity with id: {packet.CreatureId} to remove corpse from.");
return;
}
creatureDeath.transform.localPosition = packet.DeathPosition.ToUnity();
creatureDeath.transform.localRotation = packet.DeathRotation.ToUnity();
CoroutineHost.StartCoroutine(SimplerOnKillAsync(creatureDeath, packet.CreatureId));
}
/// <summary>
/// Calls only some parts from <see cref="CreatureDeath.OnKillAsync"/> to avoid sending packets from it
/// or already synced behaviour (like spawning another respawner from the remote clients)
/// </summary>
public IEnumerator SimplerOnKillAsync(CreatureDeath creatureDeath, NitroxId creatureId)
{
// Ensure we don't broadcast anything from this kill event
simulationOwnership.StopSimulatingEntity(creatureId);
// Remove the position broadcasting stuff from it
EntityPositionBroadcaster.RemoveEntityMovementControl(creatureDeath.gameObject, creatureId);
// Receiving this packet means the creature is dead
liveMixinManager.SyncRemoteHealth(creatureDeath.liveMixin, 0);
// To avoid SpawnRespawner to be called
creatureDeath.respawn = false;
creatureDeath.hasSpawnedRespawner = true;
// To avoid the cooked data section
bool lastDamageWasHeat = creatureDeath.lastDamageWasHeat;
creatureDeath.lastDamageWasHeat = false;
using (PacketSuppressor<EntityMetadataUpdate>.Suppress())
{
yield return creatureDeath.OnKillAsync();
}
// Restore the field in case it was useful
creatureDeath.lastDamageWasHeat = lastDamageWasHeat;
}
}

View File

@@ -0,0 +1,20 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxModel_Subnautica.Packets;
namespace NitroxClient.Communication.Packets.Processors;
public class RocketLaunchProcessor : ClientPacketProcessor<RocketLaunch>
{
private readonly Rockets rockets;
public RocketLaunchProcessor(Rockets rockets)
{
this.rockets = rockets;
}
public override void Process(RocketLaunch rocketLaunch)
{
rockets.RocketLaunch(rocketLaunch.RocketId);
}
}

View File

@@ -0,0 +1,35 @@
using System.Linq;
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxModel.Packets;
using Story;
namespace NitroxClient.Communication.Packets.Processors;
public class ScheduleProcessor : ClientPacketProcessor<Schedule>
{
public override void Process(Schedule schedulePacket)
{
ScheduledGoal goal = new()
{
goalKey = schedulePacket.Key,
goalType = (Story.GoalType)schedulePacket.Type,
timeExecute = schedulePacket.TimeExecute
};
if (ShouldSchedule(goal))
{
StoryGoalScheduler.main.schedule.Add(goal);
}
Log.Debug($"Processed a Schedule packet [Key: {goal.goalKey}, Type: {goal.goalType}, TimeExecute: {goal.timeExecute}]");
}
private bool ShouldSchedule(ScheduledGoal goal)
{
return goal.timeExecute >= DayNightCycle.main.timePassedAsDouble && !IsAlreadyKnown(goal.goalKey);
}
private bool IsAlreadyKnown(string goalKey)
{
return StoryGoalScheduler.main.schedule.Any(g => g.goalKey == goalKey) || // Scheduled
StoryGoalManager.main.completedGoals.Contains(goalKey); // Completed
}
}

View File

@@ -0,0 +1,55 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic.PlayerLogic;
using NitroxClient.MonoBehaviours;
using NitroxModel.Packets;
using UnityEngine;
namespace NitroxClient.Communication.Packets.Processors;
public class SeaDragonAttackTargetProcessor : ClientPacketProcessor<SeaDragonAttackTarget>
{
public override void Process(SeaDragonAttackTarget packet)
{
if (!NitroxEntity.TryGetComponentFrom(packet.SeaDragonId, out SeaDragonMeleeAttack seaDragonMeleeAttack) ||
!NitroxEntity.TryGetObjectFrom(packet.TargetId, out GameObject target))
{
return;
}
seaDragonMeleeAttack.seaDragon.Aggression.Value = packet.Aggression;
if (target.GetComponent<SubControl>())
{
// SeaDragonMeleeAttack.OnTouchFront's useful part about Cyclops attack
seaDragonMeleeAttack.animator.SetTrigger("shove");
seaDragonMeleeAttack.SendMessage("OnMeleeAttack", target, SendMessageOptions.DontRequireReceiver);
seaDragonMeleeAttack.timeLastBite = Time.time;
return;
}
// SeaDragonMeleeAttack.OnTouchFront's useful part about local player attack
Collider collider;
if (target.TryGetComponent(out RemotePlayerIdentifier remotePlayerIdentifier))
{
collider = remotePlayerIdentifier.RemotePlayer.Collider;
}
else if (target.GetComponent<Player>())
{
collider = Player.mainCollider;
}
else
{
return;
}
seaDragonMeleeAttack.timeLastBite = Time.time;
if (seaDragonMeleeAttack.attackSound)
{
using (PacketSuppressor<FMODAssetPacket>.Suppress())
{
Utils.PlayEnvSound(seaDragonMeleeAttack.attackSound, collider.transform.position, 20f);
}
}
seaDragonMeleeAttack.OnTouch(collider);
}
}

View File

@@ -0,0 +1,23 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.MonoBehaviours;
using NitroxModel.Packets;
namespace NitroxClient.Communication.Packets.Processors;
public class SeaDragonGrabExosuitProcessor : ClientPacketProcessor<SeaDragonGrabExosuit>
{
public override void Process(SeaDragonGrabExosuit packet)
{
if (!NitroxEntity.TryGetComponentFrom(packet.SeaDragonId, out SeaDragon seaDragon) ||
!NitroxEntity.TryGetComponentFrom(packet.TargetId, out Exosuit exosuit))
{
return;
}
using (PacketSuppressor<SeaDragonGrabExosuit>.Suppress())
{
seaDragon.GrabExosuit(exosuit);
seaDragon.CancelInvoke(nameof(SeaDragon.DamageExosuit));
}
}
}

View File

@@ -0,0 +1,24 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.MonoBehaviours;
using NitroxModel.Packets;
using UnityEngine;
namespace NitroxClient.Communication.Packets.Processors;
public class SeaDragonSwatAttackProcessor : ClientPacketProcessor<SeaDragonSwatAttack>
{
public override void Process(SeaDragonSwatAttack packet)
{
if (!NitroxEntity.TryGetComponentFrom(packet.SeaDragonId, out SeaDragonMeleeAttack seaDragonMeleeAttack) ||
!NitroxEntity.TryGetObjectFrom(packet.TargetId, out GameObject targetObject))
{
return;
}
using (PacketSuppressor<SeaDragonSwatAttack>.Suppress())
{
seaDragonMeleeAttack.seaDragon.Aggression.Value = packet.Aggression;
seaDragonMeleeAttack.SwatAttack(targetObject, packet.IsRightHand);
}
}
}

View File

@@ -0,0 +1,17 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.MonoBehaviours;
using NitroxModel.Packets;
using UnityEngine;
namespace NitroxClient.Communication.Packets.Processors;
public class SeaTreaderChunkPickedUpProcessor : ClientPacketProcessor<SeaTreaderChunkPickedUp>
{
public override void Process(SeaTreaderChunkPickedUp packet)
{
if (NitroxEntity.TryGetComponentFrom(packet.ChunkId, out SinkingGroundChunk sinkingGroundChunk))
{
GameObject.Destroy(sinkingGroundChunk.gameObject);
}
}
}

View File

@@ -0,0 +1,22 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.MonoBehaviours;
using NitroxClient.Unity.Helper;
using NitroxModel.Packets;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
namespace NitroxClient.Communication.Packets.Processors;
public class SeaTreaderSpawnedChunkProcessor : ClientPacketProcessor<SeaTreaderSpawnedChunk>
{
public override void Process(SeaTreaderSpawnedChunk packet)
{
if (NitroxEntity.TryGetComponentFrom(packet.CreatureId, out SeaTreader seaTreader) &&
seaTreader.TryGetComponentInChildren(out SeaTreaderSounds seaTreaderSounds))
{
GameObject chunkObject = GameObjectHelper.InstantiateWithId(seaTreaderSounds.stepChunkPrefab, packet.ChunkId);
chunkObject.transform.position = packet.Position.ToUnity();
chunkObject.transform.rotation = packet.Rotation.ToUnity();
}
}
}

View File

@@ -0,0 +1,42 @@
using NitroxClient.Communication.Abstract;
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.MonoBehaviours;
using NitroxModel.Packets;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
namespace NitroxClient.Communication.Packets.Processors
{
public class SeamothModuleActionProcessor : ClientPacketProcessor<SeamothModulesAction>
{
private readonly IPacketSender packetSender;
public SeamothModuleActionProcessor(IPacketSender packetSender)
{
this.packetSender = packetSender;
}
public override void Process(SeamothModulesAction packet)
{
using (PacketSuppressor<SeamothModulesAction>.Suppress())
{
GameObject _gameObject = NitroxEntity.RequireObjectFrom(packet.Id);
SeaMoth seamoth = _gameObject.GetComponent<SeaMoth>();
if (seamoth != null)
{
TechType techType = packet.TechType.ToUnity();
if (techType == TechType.SeamothElectricalDefense)
{
float[] chargearray = seamoth.quickSlotCharge;
float charge = chargearray[packet.SlotID];
float slotCharge = seamoth.GetSlotCharge(packet.SlotID);
GameObject gameObject = global::Utils.SpawnZeroedAt(seamoth.seamothElectricalDefensePrefab, seamoth.transform, false);
ElectricalDefense component = gameObject.GetComponent<ElectricalDefense>();
component.charge = charge;
component.chargeScalar = slotCharge;
}
}
}
}
}
}

View File

@@ -0,0 +1,23 @@
using NitroxClient.Communication.Abstract;
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.MonoBehaviours.Gui.Modals;
using NitroxModel.Packets;
namespace NitroxClient.Communication.Packets.Processors;
public class ServerStoppedProcessor : ClientPacketProcessor<ServerStopped>
{
private readonly IClient client;
public ServerStoppedProcessor(IClient client)
{
this.client = client;
}
public override void Process(ServerStopped packet)
{
// We can send the stop instruction right now instead of waiting for the timeout
client.Stop();
Modal.Get<ServerStoppedModal>()?.Show();
}
}

View File

@@ -0,0 +1,42 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxClient.GameLogic.PlayerLogic;
using NitroxModel.Packets;
namespace NitroxClient.Communication.Packets.Processors;
public class SetIntroCinematicModeProcessor : ClientPacketProcessor<SetIntroCinematicMode>
{
private readonly PlayerManager playerManager;
private readonly PlayerCinematics playerCinematics;
private readonly LocalPlayer localPlayer;
public SetIntroCinematicModeProcessor(PlayerManager playerManager, PlayerCinematics playerCinematics, LocalPlayer localPlayer)
{
this.playerManager = playerManager;
this.playerCinematics = playerCinematics;
this.localPlayer = localPlayer;
}
public override void Process(SetIntroCinematicMode packet)
{
if (localPlayer.PlayerId == packet.PlayerId)
{
if (packet.PartnerId.HasValue)
{
playerCinematics.IntroCinematicPartnerId = packet.PartnerId;
}
localPlayer.IntroCinematicMode = packet.Mode;
return;
}
if (playerManager.TryFind(packet.PlayerId, out RemotePlayer remotePlayer))
{
remotePlayer.PlayerContext.IntroCinematicMode = packet.Mode;
return;
}
Log.Debug($"SetIntroCinematicMode couldn't find Player with id {packet.PlayerId}. This is normal if player has not yet officially joined.");
}
}

Some files were not shown because too many files have changed in this diff Show More