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

23
NitroxServer/App.config Normal file
View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0" />
</startup>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<probing privatePath="lib;libs;uwe_lib" />
</assemblyBinding>
</runtime>
<system.web>
<membership defaultProvider="ClientAuthenticationMembershipProvider">
<providers>
<add name="ClientAuthenticationMembershipProvider" type="System.Web.ClientServices.Providers.ClientFormsAuthenticationMembershipProvider, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" serviceUri="" />
</providers>
</membership>
<roleManager defaultProvider="ClientRoleProvider" enabled="true">
<providers>
<add name="ClientRoleProvider" type="System.Web.ClientServices.Providers.ClientRoleProvider, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" serviceUri="" cacheTimeout="86400" />
</providers>
</roleManager>
</system.web>
</configuration>

View File

@@ -0,0 +1,30 @@
using LiteNetLib;
namespace NitroxServer.Communication;
public enum NitroxConnectionState
{
Unknown,
Disconnected,
Connected,
Reserved,
InGame
}
public static class NitroxConnectionStateExtensions
{
public static NitroxConnectionState ToNitrox(this ConnectionState connectionState)
{
if ((connectionState & ConnectionState.Connected) == ConnectionState.Connected)
{
return NitroxConnectionState.Connected;
}
if ((connectionState & ConnectionState.Disconnected) == ConnectionState.Disconnected)
{
return NitroxConnectionState.Disconnected;
}
return NitroxConnectionState.Unknown;
}
}

View File

@@ -0,0 +1,60 @@
using System.Net;
using System.Threading;
using LiteNetLib;
using LiteNetLib.Utils;
using NitroxModel.Constants;
namespace NitroxServer.Communication;
public static class LANBroadcastServer
{
private static NetManager server;
private static EventBasedNetListener listener;
private static Timer pollTimer;
public static void Start(CancellationToken ct)
{
listener = new EventBasedNetListener();
listener.NetworkReceiveUnconnectedEvent += NetworkReceiveUnconnected;
server = new NetManager(listener);
server.AutoRecycle = true;
server.BroadcastReceiveEnabled = true;
server.UnconnectedMessagesEnabled = true;
foreach (int port in LANDiscoveryConstants.BROADCAST_PORTS)
{
if (server.Start(port))
{
break;
}
}
pollTimer = new Timer(_ => server.PollEvents());
pollTimer.Change(0, 100);
Log.Debug($"{nameof(LANBroadcastServer)} started");
}
public static void Stop()
{
listener?.ClearNetworkReceiveUnconnectedEvent();
server?.Stop();
pollTimer?.Dispose();
Log.Debug($"{nameof(LANBroadcastServer)} stopped");
}
private static void NetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, UnconnectedMessageType messageType)
{
if (messageType == UnconnectedMessageType.Broadcast)
{
string requestString = reader.GetString();
if (requestString == LANDiscoveryConstants.BROADCAST_REQUEST_STRING)
{
NetDataWriter writer = new();
writer.Put(LANDiscoveryConstants.BROADCAST_RESPONSE_STRING);
writer.Put(Server.Instance.Port);
server.SendBroadcast(writer, remoteEndPoint.Port);
}
}
}
}

View File

@@ -0,0 +1,79 @@
using System;
using System.Net;
using LiteNetLib;
using LiteNetLib.Utils;
using NitroxModel.Networking;
using NitroxModel.Packets;
namespace NitroxServer.Communication.LiteNetLib;
public class LiteNetLibConnection : INitroxConnection, IEquatable<LiteNetLibConnection>
{
private readonly NetDataWriter dataWriter = new();
private readonly NetPeer peer;
public IPEndPoint Endpoint => peer;
public NitroxConnectionState State => peer.ConnectionState.ToNitrox();
public LiteNetLibConnection(NetPeer peer)
{
this.peer = peer;
}
public void SendPacket(Packet packet)
{
if (peer.ConnectionState == ConnectionState.Connected)
{
byte[] packetData = packet.Serialize();
dataWriter.Reset();
dataWriter.Put(packetData.Length);
dataWriter.Put(packetData);
peer.Send(dataWriter, (byte)packet.UdpChannel, NitroxDeliveryMethod.ToLiteNetLib(packet.DeliveryMethod));
}
else
{
Log.Warn($"Cannot send packet {packet?.GetType()} to a closed connection {peer as IPEndPoint}");
}
}
public static bool operator ==(LiteNetLibConnection left, LiteNetLibConnection right)
{
return Equals(left, right);
}
public static bool operator !=(LiteNetLibConnection left, LiteNetLibConnection right)
{
return !Equals(left, right);
}
public override bool Equals(object obj)
{
if (obj is null)
{
return false;
}
if (ReferenceEquals(this, obj))
{
return true;
}
if (obj.GetType() != GetType())
{
return false;
}
return Equals((LiteNetLibConnection)obj);
}
public override int GetHashCode()
{
return peer?.Id.GetHashCode() ?? 0;
}
public bool Equals(LiteNetLibConnection other)
{
return peer?.Id == other?.peer?.Id;
}
}

View File

@@ -0,0 +1,166 @@
using System.Buffers;
using System.Threading;
using System.Threading.Tasks;
using LiteNetLib;
using LiteNetLib.Utils;
using Mono.Nat;
using NitroxModel.Helper;
using NitroxModel.Packets;
using NitroxModel.Serialization;
using NitroxServer.Communication.Packets;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication.LiteNetLib;
public class LiteNetLibServer : NitroxServer
{
private readonly EventBasedNetListener listener;
private readonly NetManager server;
public LiteNetLibServer(PacketHandler packetHandler, PlayerManager playerManager, EntitySimulation entitySimulation, SubnauticaServerConfig serverConfig) : base(packetHandler, playerManager, entitySimulation, serverConfig)
{
listener = new EventBasedNetListener();
server = new NetManager(listener);
}
public override bool Start(CancellationToken ct = default)
{
listener.PeerConnectedEvent += PeerConnected;
listener.PeerDisconnectedEvent += PeerDisconnected;
listener.NetworkReceiveEvent += NetworkDataReceived;
listener.ConnectionRequestEvent += OnConnectionRequest;
server.ChannelsCount = (byte)typeof(Packet.UdpChannelId).GetEnumValues().Length;
server.BroadcastReceiveEnabled = true;
server.UnconnectedMessagesEnabled = true;
server.UpdateTime = 15;
server.UnsyncedEvents = true;
#if DEBUG
server.DisconnectTimeout = 300000; //Disables Timeout (for 5 min) for debug purpose (like if you jump though the server code)
#endif
if (!server.Start(portNumber))
{
return false;
}
if (useUpnpPortForwarding)
{
_ = PortForwardAsync((ushort)portNumber, ct);
}
if (useLANBroadcast)
{
LANBroadcastServer.Start(ct);
}
return true;
}
private async Task PortForwardAsync(ushort port, CancellationToken ct = default)
{
if (await NatHelper.GetPortMappingAsync(port, Protocol.Udp, ct) != null)
{
Log.Info($"Port {port} UDP is already port forwarded");
return;
}
NatHelper.ResultCodes mappingResult = await NatHelper.AddPortMappingAsync(port, Protocol.Udp, ct);
if (!ct.IsCancellationRequested)
{
switch (mappingResult)
{
case NatHelper.ResultCodes.SUCCESS:
Log.Info($"Server port {port} UDP has been automatically opened on your router (port is closed when server closes)");
break;
case NatHelper.ResultCodes.CONFLICT_IN_MAPPING_ENTRY:
Log.Warn($"Port forward for {port} UDP failed. It appears to already be port forwarded or it conflicts with another port forward rule.");
break;
case NatHelper.ResultCodes.UNKNOWN_ERROR:
Log.Warn($"Failed to port forward {port} UDP through UPnP. If using Hamachi or you've manually port-forwarded, please disregard this warning. To disable this feature you can go into the server settings.");
break;
}
}
}
public override void Stop()
{
if (!server.IsRunning)
{
return;
}
playerManager.SendPacketToAllPlayers(new ServerStopped());
// We want every player to receive this packet
Thread.Sleep(500);
server.Stop();
if (useUpnpPortForwarding)
{
if (NatHelper.DeletePortMappingAsync((ushort)portNumber, Protocol.Udp, CancellationToken.None).GetAwaiter().GetResult())
{
Log.Debug($"Port forward rule removed for {portNumber} UDP");
}
else
{
Log.Warn($"Failed to remove port forward rule {portNumber} UDP");
}
}
if (useLANBroadcast)
{
LANBroadcastServer.Stop();
}
}
public void OnConnectionRequest(ConnectionRequest request)
{
if (server.ConnectedPeersCount < maxConnections)
{
request.AcceptIfKey("nitrox");
}
else
{
request.Reject();
}
}
private void PeerConnected(NetPeer peer)
{
LiteNetLibConnection connection = new(peer);
lock (connectionsByRemoteIdentifier)
{
connectionsByRemoteIdentifier[peer.Id] = connection;
}
}
private void PeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo)
{
ClientDisconnected(GetConnection(peer.Id));
}
private void NetworkDataReceived(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);
INitroxConnection connection = GetConnection(peer.Id);
ProcessIncomingData(connection, packet);
}
finally
{
ArrayPool<byte>.Shared.Return(packetData, true);
}
}
private INitroxConnection GetConnection(int remoteIdentifier)
{
INitroxConnection connection;
lock (connectionsByRemoteIdentifier)
{
connectionsByRemoteIdentifier.TryGetValue(remoteIdentifier, out connection);
}
return connection;
}
}

View File

@@ -0,0 +1,14 @@
using System.Net;
using NitroxModel.Packets;
using NitroxModel.Packets.Processors.Abstract;
namespace NitroxServer.Communication;
public interface INitroxConnection : IProcessorContext
{
IPEndPoint Endpoint { get; }
NitroxConnectionState State { get; }
void SendPacket(Packet packet);
}

View File

@@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.Threading;
using NitroxModel.DataStructures;
using NitroxModel.Packets;
using NitroxModel.Serialization;
using NitroxServer.Communication.Packets;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication
{
public abstract class NitroxServer
{
static NitroxServer()
{
Packet.InitSerializer();
}
protected readonly int portNumber;
protected readonly int maxConnections;
protected readonly bool useUpnpPortForwarding;
protected readonly bool useLANBroadcast;
protected readonly PacketHandler packetHandler;
protected readonly EntitySimulation entitySimulation;
protected readonly Dictionary<int, INitroxConnection> connectionsByRemoteIdentifier = new();
protected readonly PlayerManager playerManager;
public NitroxServer(PacketHandler packetHandler, PlayerManager playerManager, EntitySimulation entitySimulation, SubnauticaServerConfig serverConfig)
{
this.packetHandler = packetHandler;
this.playerManager = playerManager;
this.entitySimulation = entitySimulation;
portNumber = serverConfig.ServerPort;
maxConnections = serverConfig.MaxConnections;
useUpnpPortForwarding = serverConfig.AutoPortForward;
useLANBroadcast = serverConfig.LANDiscoveryEnabled;
}
public abstract bool Start(CancellationToken ct = default);
public abstract void Stop();
protected void ClientDisconnected(INitroxConnection connection)
{
Player player = playerManager.GetPlayer(connection);
if (player != null)
{
playerManager.PlayerDisconnected(connection);
Disconnect disconnect = new(player.Id);
playerManager.SendPacketToAllPlayers(disconnect);
List<SimulatedEntity> ownershipChanges = entitySimulation.CalculateSimulationChangesFromPlayerDisconnect(player);
if (ownershipChanges.Count > 0)
{
SimulationOwnershipChange ownershipChange = new(ownershipChanges);
playerManager.SendPacketToAllPlayers(ownershipChange);
}
}
else
{
playerManager.NonPlayerDisconnected(connection);
}
}
protected void ProcessIncomingData(INitroxConnection connection, Packet packet)
{
try
{
packetHandler.Process(packet, connection);
}
catch (Exception ex)
{
Log.Error(ex, $"Exception while processing packet: {packet}");
}
}
}
}

View File

@@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
using NitroxModel.Core;
using NitroxModel.Packets;
using NitroxModel.Packets.Processors.Abstract;
using NitroxServer.Communication.Packets.Processors;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
namespace NitroxServer.Communication.Packets
{
public class PacketHandler
{
private readonly PlayerManager playerManager;
private readonly DefaultServerPacketProcessor defaultServerPacketProcessor;
private readonly Dictionary<Type, PacketProcessor> packetProcessorAuthCache = new();
private readonly Dictionary<Type, PacketProcessor> packetProcessorUnauthCache = new();
public PacketHandler(PlayerManager playerManager, DefaultServerPacketProcessor packetProcessor)
{
this.playerManager = playerManager;
defaultServerPacketProcessor = packetProcessor;
}
public void Process(Packet packet, INitroxConnection connection)
{
Player player = playerManager.GetPlayer(connection);
if (player == null)
{
ProcessUnauthenticated(packet, connection);
}
else
{
ProcessAuthenticated(packet, player);
}
}
private void ProcessAuthenticated(Packet packet, Player player)
{
Type packetType = packet.GetType();
if (!packetProcessorAuthCache.TryGetValue(packetType, out PacketProcessor processor))
{
Type packetProcessorType = typeof(AuthenticatedPacketProcessor<>).MakeGenericType(packetType);
packetProcessorAuthCache[packetType] = processor = NitroxServiceLocator.LocateOptionalService(packetProcessorType).Value as PacketProcessor;
}
if (processor != null)
{
try
{
processor.ProcessPacket(packet, player);
}
catch (Exception ex)
{
Log.Error(ex, $"Error in packet processor {processor.GetType()}");
}
}
else
{
defaultServerPacketProcessor.ProcessPacket(packet, player);
}
}
private void ProcessUnauthenticated(Packet packet, INitroxConnection connection)
{
Type packetType = packet.GetType();
if (!packetProcessorUnauthCache.TryGetValue(packetType, out PacketProcessor processor))
{
Type packetProcessorType = typeof(UnauthenticatedPacketProcessor<>).MakeGenericType(packetType);
packetProcessorUnauthCache[packetType] = processor = NitroxServiceLocator.LocateOptionalService(packetProcessorType).Value as PacketProcessor;
}
if (processor == null)
{
Log.Warn($"Received invalid, unauthenticated packet: {packet}");
return;
}
try
{
processor.ProcessPacket(packet, connection);
}
catch (Exception ex)
{
Log.Error(ex, $"Error in packet processor {processor.GetType()}");
}
}
}
}

View File

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

View File

@@ -0,0 +1,49 @@
using System.Collections.Generic;
using System.Linq;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.Packets;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication.Packets.Processors.Abstract;
public abstract class TransmitIfCanSeePacketProcessor<T> : AuthenticatedPacketProcessor<T> where T : Packet
{
private readonly PlayerManager playerManager;
private readonly EntityRegistry entityRegistry;
public TransmitIfCanSeePacketProcessor(PlayerManager playerManager, EntityRegistry entityRegistry)
{
this.playerManager = playerManager;
this.entityRegistry = entityRegistry;
}
/// <summary>
/// Transmits the provided <paramref name="packet"/> to all other players (excluding <paramref name="senderPlayer"/>)
/// who can see (<see cref="Player.CanSee"/>) entities corresponding to the provided <paramref name="entityIds"/> only if all those entities are registered.
/// </summary>
public void TransmitIfCanSeeEntities(Packet packet, Player senderPlayer, List<NitroxId> entityIds)
{
List<Entity> entities = [];
foreach (NitroxId entityId in entityIds)
{
if (entityRegistry.TryGetEntityById(entityId, out Entity entity))
{
entities.Add(entity);
}
else
{
return;
}
}
foreach (Player player in playerManager.GetConnectedPlayersExcept(senderPlayer))
{
if (entities.All(player.CanSee))
{
player.SendPacket(packet);
}
}
}
}

View File

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

View File

@@ -0,0 +1,14 @@
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication.Packets.Processors;
public class AggressiveWhenSeeTargetChangedProcessor(
PlayerManager playerManager,
EntityRegistry entityRegistry
) : TransmitIfCanSeePacketProcessor<AggressiveWhenSeeTargetChanged>(playerManager, entityRegistry)
{
public override void Process(AggressiveWhenSeeTargetChanged packet, Player sender) => TransmitIfCanSeeEntities(packet, sender, [packet.CreatureId, packet.TargetId]);
}

View File

@@ -0,0 +1,14 @@
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication.Packets.Processors;
public class AttackCyclopsTargetChangedProcessor(
PlayerManager playerManager,
EntityRegistry entityRegistry
) : TransmitIfCanSeePacketProcessor<AttackCyclopsTargetChanged>(playerManager, entityRegistry)
{
public override void Process(AttackCyclopsTargetChanged packet, Player sender) => TransmitIfCanSeeEntities(packet, sender, [packet.CreatureId, packet.TargetId]);
}

View File

@@ -0,0 +1,18 @@
using NitroxModel.Packets;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Bases;
namespace NitroxServer.Communication.Packets.Processors;
public class BaseDeconstructedProcessor : BuildingProcessor<BaseDeconstructed>
{
public BaseDeconstructedProcessor(BuildingManager buildingManager, PlayerManager playerManager) : base(buildingManager, playerManager) { }
public override void Process(BaseDeconstructed packet, Player player)
{
if (buildingManager.ReplaceBaseByGhost(packet))
{
playerManager.SendPacketToOtherPlayers(packet, player);
}
}
}

View File

@@ -0,0 +1,22 @@
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
namespace NitroxServer.Communication.Packets.Processors
{
public class BedEnterProcessor : AuthenticatedPacketProcessor<BedEnter>
{
private readonly StoryManager storyManager;
public BedEnterProcessor(StoryManager storyManager)
{
this.storyManager = storyManager;
}
public override void Process(BedEnter packet, Player player)
{
// TODO: Needs repair since the new time implementation only relies on server-side time.
// storyManager.ChangeTime(StoryManager.TimeModification.SKIP);
}
}
}

View File

@@ -0,0 +1,39 @@
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Bases;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication.Packets.Processors;
public abstract class BuildingProcessor<T> : AuthenticatedPacketProcessor<T> where T : Packet
{
internal readonly BuildingManager buildingManager;
internal readonly PlayerManager playerManager;
internal readonly EntitySimulation entitySimulation;
public BuildingProcessor(BuildingManager buildingManager, PlayerManager playerManager, EntitySimulation entitySimulation = null)
{
this.buildingManager = buildingManager;
this.playerManager = playerManager;
this.entitySimulation = entitySimulation;
}
public void SendToOtherPlayersWithOperationId(T packet, Player player, int operationId)
{
if (packet is OrderedBuildPacket buildPacket)
{
buildPacket.OperationId = operationId;
}
playerManager.SendPacketToOtherPlayers(packet, player);
}
public void ClaimBuildPiece(Entity entity, Player player)
{
SimulatedEntity simulatedEntity = entitySimulation.AssignNewEntityToPlayer(entity, player, false);
SimulationOwnershipChange ownershipChangePacket = new(simulatedEntity);
playerManager.SendPacketToAllPlayers(ownershipChangePacket);
}
}

View File

@@ -0,0 +1,53 @@
using System.Collections.Generic;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.GameLogic.Entities.Bases;
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication.Packets.Processors;
public class BuildingResyncRequestProcessor : AuthenticatedPacketProcessor<BuildingResyncRequest>
{
private readonly EntityRegistry entityRegistry;
private readonly WorldEntityManager worldEntityManager;
public BuildingResyncRequestProcessor(EntityRegistry entityRegistry, WorldEntityManager worldEntityManager)
{
this.entityRegistry = entityRegistry;
this.worldEntityManager = worldEntityManager;
}
public override void Process(BuildingResyncRequest packet, Player player)
{
Dictionary<BuildEntity, int> buildEntities = new();
Dictionary<ModuleEntity, int> moduleEntities = new();
void AddEntityToResync(Entity entity)
{
switch (entity)
{
case BuildEntity buildEntity:
buildEntities.Add(buildEntity, buildEntity.OperationId);
break;
case ModuleEntity moduleEntity:
moduleEntities.Add(moduleEntity, -1);
break;
}
}
if (packet.ResyncEverything)
{
foreach (GlobalRootEntity globalRootEntity in worldEntityManager.GetGlobalRootEntities(true))
{
AddEntityToResync(globalRootEntity);
}
}
else if (packet.EntityId != null && entityRegistry.TryGetEntityById(packet.EntityId, out Entity entity))
{
AddEntityToResync(entity);
}
player.SendPacket(new BuildingResync(buildEntities, moduleEntities));
}
}

View File

@@ -0,0 +1,54 @@
using System.Collections.Generic;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication.Packets.Processors;
public class CellVisibilityChangedProcessor : AuthenticatedPacketProcessor<CellVisibilityChanged>
{
private readonly EntitySimulation entitySimulation;
private readonly WorldEntityManager worldEntityManager;
public CellVisibilityChangedProcessor(EntitySimulation entitySimulation, WorldEntityManager worldEntityManager)
{
this.entitySimulation = entitySimulation;
this.worldEntityManager = worldEntityManager;
}
public override void Process(CellVisibilityChanged packet, Player player)
{
player.AddCells(packet.Added);
player.RemoveCells(packet.Removed);
List<Entity> totalEntities = [];
List<SimulatedEntity> totalSimulationChanges = [];
foreach (AbsoluteEntityCell addedCell in packet.Added)
{
worldEntityManager.LoadUnspawnedEntities(addedCell.BatchId, false);
totalSimulationChanges.AddRange(entitySimulation.GetSimulationChangesForCell(player, addedCell));
totalEntities.AddRange(worldEntityManager.GetEntities(addedCell));
}
foreach (AbsoluteEntityCell removedCell in packet.Removed)
{
entitySimulation.FillWithRemovedCells(player, removedCell, totalSimulationChanges);
}
// Simulation update must be broadcasted before the entities are spawned
if (totalSimulationChanges.Count > 0)
{
entitySimulation.BroadcastSimulationChanges(new(totalSimulationChanges));
}
if (totalEntities.Count > 0)
{
SpawnEntities batchEntities = new(totalEntities);
player.SendPacket(batchEntities);
}
}
}

View File

@@ -0,0 +1,27 @@
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
namespace NitroxServer.Communication.Packets.Processors
{
public class ChatMessageProcessor : AuthenticatedPacketProcessor<ChatMessage>
{
private readonly PlayerManager playerManager;
public ChatMessageProcessor(PlayerManager playerManager)
{
this.playerManager = playerManager;
}
public override void Process(ChatMessage packet, Player player)
{
if (player.PlayerContext.IsMuted)
{
player.SendPacket(new ChatMessage(ChatMessage.SERVER_ID, "You're currently muted"));
return;
}
Log.Info($"<{player.Name}>: {packet.Text}");
playerManager.SendPacketToAllPlayers(packet);
}
}
}

View File

@@ -0,0 +1,29 @@
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication.Packets.Processors;
public class ClearPlanterProcessor : AuthenticatedPacketProcessor<ClearPlanter>
{
private readonly EntityRegistry entityRegistry;
public ClearPlanterProcessor(EntityRegistry entityRegistry)
{
this.entityRegistry = entityRegistry;
}
public override void Process(ClearPlanter packet, Player player)
{
if (entityRegistry.TryGetEntityById(packet.PlanterId, out PlanterEntity planterEntity))
{
// No need to transmit this packet since the operation is automatically done on remote clients
entityRegistry.CleanChildren(planterEntity);
}
else
{
Log.ErrorOnce($"[{nameof(ClearPlanterProcessor)}] Could not find PlanterEntity with id {packet.PlanterId}");
}
}
}

View File

@@ -0,0 +1,14 @@
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication.Packets.Processors;
public class CreatureActionChangedProcessor(
PlayerManager playerManager,
EntityRegistry entityRegistry
) : TransmitIfCanSeePacketProcessor<CreatureActionChanged>(playerManager, entityRegistry)
{
public override void Process(CreatureActionChanged packet, Player sender) => TransmitIfCanSeeEntities(packet, sender, [packet.CreatureId]);
}

View File

@@ -0,0 +1,14 @@
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication.Packets.Processors;
public class CreaturePoopPerformedProcessor(
PlayerManager playerManager,
EntityRegistry entityRegistry
) : TransmitIfCanSeePacketProcessor<CreaturePoopPerformed>(playerManager, entityRegistry)
{
public override void Process(CreaturePoopPerformed packet, Player sender) => TransmitIfCanSeeEntities(packet, sender, [packet.CreatureId]);
}

View File

@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
namespace NitroxServer.Communication.Packets.Processors;
public class DefaultServerPacketProcessor : AuthenticatedPacketProcessor<Packet>
{
private readonly PlayerManager playerManager;
private readonly HashSet<Type> loggingPacketBlackList = new()
{
typeof(AnimationChangeEvent),
typeof(PlayerMovement),
typeof(ItemPosition),
typeof(PlayerStats),
typeof(StoryGoalExecuted),
typeof(FMODAssetPacket),
typeof(FMODCustomEmitterPacket),
typeof(FMODCustomLoopingEmitterPacket),
typeof(FMODStudioEmitterPacket),
typeof(PlayerCinematicControllerCall),
typeof(TorpedoShot),
typeof(TorpedoHit),
typeof(TorpedoTargetAcquired),
typeof(StasisSphereShot),
typeof(StasisSphereHit),
typeof(SeaTreaderChunkPickedUp)
};
/// <summary>
/// Packet types which don't have a server packet processor but should not be transmitted
/// </summary>
private readonly HashSet<Type> defaultPacketProcessorBlacklist = new()
{
typeof(GameModeChanged), typeof(DropSimulationOwnership),
};
public DefaultServerPacketProcessor(PlayerManager playerManager)
{
this.playerManager = playerManager;
}
public override void Process(Packet packet, Player player)
{
if (!loggingPacketBlackList.Contains(packet.GetType()))
{
Log.Debug($"Using default packet processor for: {packet} and player {player.Id}");
}
if (defaultPacketProcessorBlacklist.Contains(packet.GetType()))
{
Log.ErrorOnce($"Player {player.Name} [{player.Id}] sent a packet which is blacklisted by the server. It's likely that the said player is using a modified version of Nitrox and action could be taken accordingly.");
return;
}
playerManager.SendPacketToOtherPlayers(packet, player);
}
}

View File

@@ -0,0 +1,66 @@
using System;
using System.Net;
using System.Threading.Tasks;
using NitroxModel.Helper;
using NitroxModel.Packets;
using NitroxModel.Serialization;
using NitroxServer.Communication.Packets.Processors.Abstract;
namespace NitroxServer.Communication.Packets.Processors;
public class DiscordRequestIPProcessor : AuthenticatedPacketProcessor<DiscordRequestIP>
{
private readonly SubnauticaServerConfig serverConfig;
private string ipPort;
public DiscordRequestIPProcessor(SubnauticaServerConfig serverConfig)
{
this.serverConfig = serverConfig;
}
public override void Process(DiscordRequestIP packet, Player player)
{
if (string.IsNullOrEmpty(ipPort))
{
Task.Run(() => ProcessPacketAsync(packet, player));
return;
}
packet.IpPort = ipPort;
player.SendPacket(packet);
}
private async Task ProcessPacketAsync(DiscordRequestIP packet, Player player)
{
string result = await GetIpAsync();
if (result == "")
{
Log.Error("Couldn't get external Ip for discord request.");
return;
}
packet.IpPort = ipPort = $"{result}:{serverConfig.ServerPort}";
player.SendPacket(packet);
}
/// <summary>
/// Get the WAN IP address or the Hamachi IP address if the WAN IP address is not available.
/// </summary>
/// <returns>Found IP or blank string if none found</returns>
private static async Task<string> GetIpAsync()
{
Task<IPAddress> wanIp = NetHelper.GetWanIpAsync();
Task<IPAddress> hamachiIp = Task.Run(NetHelper.GetHamachiIp);
if (await wanIp != null)
{
return wanIp.Result.ToString();
}
if (await hamachiIp != null)
{
return hamachiIp.Result.ToString();
}
return "";
}
}

View File

@@ -0,0 +1,45 @@
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.Util;
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication.Packets.Processors;
public class EntityDestroyedPacketProcessor : AuthenticatedPacketProcessor<EntityDestroyed>
{
private readonly PlayerManager playerManager;
private readonly EntitySimulation entitySimulation;
private readonly WorldEntityManager worldEntityManager;
public EntityDestroyedPacketProcessor(PlayerManager playerManager, EntitySimulation entitySimulation, WorldEntityManager worldEntityManager)
{
this.playerManager = playerManager;
this.worldEntityManager = worldEntityManager;
this.entitySimulation = entitySimulation;
}
public override void Process(EntityDestroyed packet, Player destroyingPlayer)
{
entitySimulation.EntityDestroyed(packet.Id);
if (worldEntityManager.TryDestroyEntity(packet.Id, out Entity entity))
{
if (entity is VehicleWorldEntity vehicleWorldEntity)
{
worldEntityManager.MovePlayerChildrenToRoot(vehicleWorldEntity);
}
foreach (Player player in playerManager.GetConnectedPlayers())
{
bool isOtherPlayer = player != destroyingPlayer;
if (isOtherPlayer && player.CanSee(entity))
{
player.SendPacket(packet);
}
}
}
}
}

View File

@@ -0,0 +1,90 @@
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication.Packets.Processors;
public class EntityMetadataUpdateProcessor : AuthenticatedPacketProcessor<EntityMetadataUpdate>
{
private readonly PlayerManager playerManager;
private readonly EntityRegistry entityRegistry;
public EntityMetadataUpdateProcessor(PlayerManager playerManager, EntityRegistry entityRegistry)
{
this.playerManager = playerManager;
this.entityRegistry = entityRegistry;
}
public override void Process(EntityMetadataUpdate packet, Player sendingPlayer)
{
if (!entityRegistry.TryGetEntityById(packet.Id, out Entity entity))
{
Log.Error($"Entity metadata {packet.NewValue.GetType()} updated on an entity unknown to the server {packet.Id}");
return;
}
if (TryProcessMetadata(sendingPlayer, entity, packet.NewValue))
{
entity.Metadata = packet.NewValue;
SendUpdateToVisiblePlayers(packet, sendingPlayer, entity);
}
}
private void SendUpdateToVisiblePlayers(EntityMetadataUpdate packet, Player sendingPlayer, Entity entity)
{
foreach (Player player in playerManager.GetConnectedPlayers())
{
bool updateVisibleToPlayer = player.CanSee(entity);
// Always sync container/storage metadata to all visible players
bool isContainerMetadata = IsContainerRelatedMetadata(packet.NewValue);
if (player != sendingPlayer && (updateVisibleToPlayer || isContainerMetadata))
{
player.SendPacket(packet);
}
}
}
private bool IsContainerRelatedMetadata(EntityMetadata metadata)
{
// Check if metadata is related to containers/storage
return metadata.GetType().Name.Contains("Container") ||
metadata.GetType().Name.Contains("Storage") ||
metadata.GetType().Name.Contains("Inventory");
}
private bool TryProcessMetadata(Player sendingPlayer, Entity entity, EntityMetadata metadata)
{
return metadata switch
{
PlayerMetadata playerMetadata => ProcessPlayerMetadata(sendingPlayer, entity, playerMetadata),
// Always allow container/storage metadata updates for proper sync
_ when IsContainerRelatedMetadata(metadata) => true,
// Allow metadata updates from any player by default
_ => true
};
}
private bool ProcessPlayerMetadata(Player sendingPlayer, Entity entity, PlayerMetadata metadata)
{
if (sendingPlayer.GameObjectId == entity.Id)
{
sendingPlayer.EquippedItems.Clear();
foreach (PlayerMetadata.EquippedItem item in metadata.EquippedItems)
{
sendingPlayer.EquippedItems.Add(item.Slot, item.Id);
}
return true;
}
Log.WarnOnce($"Player {sendingPlayer.Name} tried updating metadata of another player's entity {entity.Id}");
return false;
}
}

View File

@@ -0,0 +1,36 @@
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication.Packets.Processors;
public class EntityReparentedProcessor : AuthenticatedPacketProcessor<EntityReparented>
{
private readonly EntityRegistry entityRegistry;
private readonly PlayerManager playerManager;
public EntityReparentedProcessor(EntityRegistry entityRegistry, PlayerManager playerManager)
{
this.entityRegistry = entityRegistry;
this.playerManager = playerManager;
}
public override void Process(EntityReparented packet, Player player)
{
if (!entityRegistry.TryGetEntityById(packet.Id, out Entity entity))
{
Log.Error($"Couldn't find entity for {packet.Id}");
return;
}
if (!entityRegistry.TryGetEntityById(packet.NewParentId, out Entity parentEntity))
{
Log.Error($"Couldn't find parent entity for {packet.NewParentId}");
return;
}
entityRegistry.ReparentEntity(packet.Id, packet.NewParentId);
playerManager.SendPacketToOtherPlayers(packet, player);
}
}

View File

@@ -0,0 +1,59 @@
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication.Packets.Processors
{
class EntitySpawnedByClientProcessor : AuthenticatedPacketProcessor<EntitySpawnedByClient>
{
private readonly PlayerManager playerManager;
private readonly EntityRegistry entityRegistry;
private readonly WorldEntityManager worldEntityManager;
private readonly EntitySimulation entitySimulation;
public EntitySpawnedByClientProcessor(PlayerManager playerManager, EntityRegistry entityRegistry, WorldEntityManager worldEntityManager, EntitySimulation entitySimulation)
{
this.playerManager = playerManager;
this.entityRegistry = entityRegistry;
this.worldEntityManager = worldEntityManager;
this.entitySimulation = entitySimulation;
}
public override void Process(EntitySpawnedByClient packet, Player playerWhoSpawned)
{
Entity entity = packet.Entity;
// If the entity already exists in the registry, it is fine to update. This is a normal case as the player
// may have an item in their inventory (that the registry knows about) then wants to spawn it into the world.
entityRegistry.AddOrUpdate(entity);
SimulatedEntity simulatedEntity = null;
if (entity is WorldEntity worldEntity)
{
worldEntityManager.TrackEntityInTheWorld(worldEntity);
if (packet.RequireSimulation)
{
simulatedEntity = entitySimulation.AssignNewEntityToPlayer(entity, playerWhoSpawned);
SimulationOwnershipChange ownershipChangePacket = new SimulationOwnershipChange(simulatedEntity);
playerManager.SendPacketToAllPlayers(ownershipChangePacket);
}
}
SpawnEntities spawnEntities = new(entity, simulatedEntity, packet.RequireRespawn);
foreach (Player player in playerManager.GetConnectedPlayers())
{
bool isOtherPlayer = player != playerWhoSpawned;
if (isOtherPlayer && player.CanSee(entity))
{
player.SendPacket(spawnEntities);
}
}
}
}
}

View File

@@ -0,0 +1,89 @@
using System.Collections.Generic;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Entities;
using static NitroxModel.Packets.EntityTransformUpdates;
namespace NitroxServer.Communication.Packets.Processors
{
class EntityTransformUpdatesProcessor : AuthenticatedPacketProcessor<EntityTransformUpdates>
{
private readonly PlayerManager playerManager;
private readonly WorldEntityManager worldEntityManager;
private readonly SimulationOwnershipData simulationOwnershipData;
public EntityTransformUpdatesProcessor(PlayerManager playerManager, WorldEntityManager worldEntityManager, SimulationOwnershipData simulationOwnershipData)
{
this.playerManager = playerManager;
this.worldEntityManager = worldEntityManager;
this.simulationOwnershipData = simulationOwnershipData;
}
public override void Process(EntityTransformUpdates packet, Player simulatingPlayer)
{
Dictionary<Player, List<EntityTransformUpdate>> visibleUpdatesByPlayer = InitializeVisibleUpdateMapWithOtherPlayers(simulatingPlayer);
AssignVisibleUpdatesToPlayers(simulatingPlayer, packet.Updates, visibleUpdatesByPlayer);
SendUpdatesToPlayers(visibleUpdatesByPlayer);
}
private Dictionary<Player, List<EntityTransformUpdate>> InitializeVisibleUpdateMapWithOtherPlayers(Player simulatingPlayer)
{
Dictionary<Player, List<EntityTransformUpdate>> visibleUpdatesByPlayer = new Dictionary<Player, List<EntityTransformUpdate>>();
foreach (Player player in playerManager.GetConnectedPlayers())
{
if (!player.Equals(simulatingPlayer))
{
visibleUpdatesByPlayer[player] = new List<EntityTransformUpdate>();
}
}
return visibleUpdatesByPlayer;
}
private void AssignVisibleUpdatesToPlayers(Player sendingPlayer, List<EntityTransformUpdate> updates, Dictionary<Player, List<EntityTransformUpdate>> visibleUpdatesByPlayer)
{
foreach (EntityTransformUpdate update in updates)
{
if (!simulationOwnershipData.TryGetLock(update.Id, out SimulationOwnershipData.PlayerLock playerLock) || playerLock.Player != sendingPlayer)
{
// This will happen pretty frequently when a player moves very fast (swimfast or maybe some more edge cases) so we can just ignore this
continue;
}
if (!worldEntityManager.TryUpdateEntityPosition(update.Id, update.Position, update.Rotation, out AbsoluteEntityCell currentCell, out WorldEntity worldEntity))
{
// Normal behaviour if the entity was removed at the same time as someone trying to simulate a postion update.
// we log an info inside entityManager.UpdateEntityPosition just in case.
continue;
}
foreach (KeyValuePair<Player, List<EntityTransformUpdate>> playerUpdates in visibleUpdatesByPlayer)
{
if (playerUpdates.Key.CanSee(worldEntity))
{
playerUpdates.Value.Add(update);
}
}
}
}
private void SendUpdatesToPlayers(Dictionary<Player, List<EntityTransformUpdate>> visibleUpdatesByPlayer)
{
foreach (KeyValuePair<Player, List<EntityTransformUpdate>> playerUpdates in visibleUpdatesByPlayer)
{
Player player = playerUpdates.Key;
List<EntityTransformUpdate> updates = playerUpdates.Value;
if (updates.Count > 0)
{
EntityTransformUpdates updatesPacket = new EntityTransformUpdates(updates);
player.SendPacket(updatesPacket);
}
}
}
}
}

View File

@@ -0,0 +1,23 @@
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
namespace NitroxServer.Communication.Packets.Processors
{
public class EscapePodChangedPacketProcessor : AuthenticatedPacketProcessor<EscapePodChanged>
{
private readonly PlayerManager playerManager;
public EscapePodChangedPacketProcessor(PlayerManager playerManager)
{
this.playerManager = playerManager;
}
public override void Process(EscapePodChanged packet, Player player)
{
Log.Debug(packet);
player.SubRootId = packet.EscapePodId;
playerManager.SendPacketToOtherPlayers(packet, player);
}
}
}

View File

@@ -0,0 +1,38 @@
using NitroxModel.DataStructures.Unity;
using NitroxModel.GameLogic.FMOD;
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
namespace NitroxServer.Communication.Packets.Processors;
public class FMODAssetProcessor : AuthenticatedPacketProcessor<FMODAssetPacket>
{
private readonly PlayerManager playerManager;
private readonly FMODWhitelist fmodWhitelist;
public FMODAssetProcessor(PlayerManager playerManager, FMODWhitelist fmodWhitelist)
{
this.playerManager = playerManager;
this.fmodWhitelist = fmodWhitelist;
}
public override void Process(FMODAssetPacket packet, Player sendingPlayer)
{
if (!fmodWhitelist.TryGetSoundData(packet.AssetPath, out SoundData soundData))
{
Log.Error($"[{nameof(FMODAssetProcessor)}] Whitelist has no item for {packet.AssetPath}.");
return;
}
foreach (Player player in playerManager.GetConnectedPlayers())
{
float distance = NitroxVector3.Distance(player.Position, packet.Position);
if (player != sendingPlayer && (soundData.IsGlobal || player.SubRootId.Equals(sendingPlayer.SubRootId)) && distance <= soundData.Radius)
{
packet.Volume = SoundHelper.CalculateVolume(distance, soundData.Radius, packet.Volume);
player.SendPacket(packet);
}
}
}
}

View File

@@ -0,0 +1,40 @@
using NitroxModel.DataStructures.Unity;
using NitroxModel.GameLogic.FMOD;
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
namespace NitroxServer.Communication.Packets.Processors;
public class FMODEventInstanceProcessor : AuthenticatedPacketProcessor<FMODEventInstancePacket>
{
private readonly PlayerManager playerManager;
private readonly FMODWhitelist fmodWhitelist;
public FMODEventInstanceProcessor(PlayerManager playerManager, FMODWhitelist fmodWhitelist)
{
this.playerManager = playerManager;
this.fmodWhitelist = fmodWhitelist;
}
public override void Process(FMODEventInstancePacket packet, Player sendingPlayer)
{
if (!fmodWhitelist.TryGetSoundData(packet.AssetPath, out SoundData soundData))
{
Log.Error($"[{nameof(FMODEventInstanceProcessor)}] Whitelist has no item for {packet.AssetPath}.");
return;
}
foreach (Player player in playerManager.GetConnectedPlayers())
{
float distance = NitroxVector3.Distance(player.Position, packet.Position);
if (player != sendingPlayer &&
(soundData.IsGlobal || player.SubRootId.Equals(sendingPlayer.SubRootId)) &&
distance < soundData.Radius)
{
packet.Volume = SoundHelper.CalculateVolume(distance, soundData.Radius, packet.Volume);
player.SendPacket(packet);
}
}
}
}

View File

@@ -0,0 +1,21 @@
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
namespace NitroxServer.Communication.Packets.Processors
{
class FireDousedProcessor : AuthenticatedPacketProcessor<FireDoused>
{
private readonly PlayerManager playerManager;
public FireDousedProcessor(PlayerManager playerManager)
{
this.playerManager = playerManager;
}
public override void Process(FireDoused packet, Player simulatingPlayer)
{
playerManager.SendPacketToOtherPlayers(packet, simulatingPlayer);
}
}
}

View File

@@ -0,0 +1,36 @@
using NitroxModel.DataStructures.Unity;
using NitroxModel.GameLogic.FMOD;
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
namespace NitroxServer.Communication.Packets.Processors;
public class FootstepPacketProcessor : AuthenticatedPacketProcessor<FootstepPacket>
{
private readonly float footstepAudioRange; // To modify this value, modify the last value of the event:/player/footstep_precursor_base sound in the SoundWhitelist_Subnautica.csv file
private readonly PlayerManager playerManager;
public FootstepPacketProcessor(PlayerManager playerManager, FMODWhitelist whitelist)
{
this.playerManager = playerManager;
whitelist.TryGetSoundData("event:/player/footstep_precursor_base", out SoundData soundData);
footstepAudioRange = soundData.Radius;
}
public override void Process(FootstepPacket footstepPacket, Player sendingPlayer)
{
foreach (Player player in playerManager.GetConnectedPlayers())
{
if (NitroxVector3.Distance(player.Position, sendingPlayer.Position) >= footstepAudioRange ||
player == sendingPlayer)
{
continue;
}
if(player.SubRootId.Equals(sendingPlayer.SubRootId))
{
player.SendPacket(footstepPacket);
}
}
}
}

View File

@@ -0,0 +1,12 @@
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
namespace NitroxServer.Communication.Packets.Processors;
public class GoalCompletedProcessor : AuthenticatedPacketProcessor<GoalCompleted>
{
public override void Process(GoalCompleted packet, Player player)
{
player.PersonalCompletedGoalsWithTimestamp.Add(packet.CompletedGoal, packet.CompletionTime);
}
}

View File

@@ -0,0 +1,34 @@
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Unlockables;
namespace NitroxServer.Communication.Packets.Processors
{
public class KnownTechEntryAddProcessor : AuthenticatedPacketProcessor<KnownTechEntryAdd>
{
private readonly PlayerManager playerManager;
private readonly PDAStateData pdaStateData;
public KnownTechEntryAddProcessor(PlayerManager playerManager, PDAStateData pdaStateData)
{
this.playerManager = playerManager;
this.pdaStateData = pdaStateData;
}
public override void Process(KnownTechEntryAdd packet, Player player)
{
switch (packet.Category)
{
case KnownTechEntryAdd.EntryCategory.KNOWN:
pdaStateData.AddKnownTechType(packet.TechType, packet.PartialTechTypesToRemove);
break;
case KnownTechEntryAdd.EntryCategory.ANALYZED:
pdaStateData.AddAnalyzedTechType(packet.TechType);
break;
}
playerManager.SendPacketToOtherPlayers(packet, player);
}
}
}

View File

@@ -0,0 +1,22 @@
using NitroxModel.Packets;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Bases;
namespace NitroxServer.Communication.Packets.Processors;
public class LargeWaterParkDeconstructedProcessor : BuildingProcessor<LargeWaterParkDeconstructed>
{
public LargeWaterParkDeconstructedProcessor(BuildingManager buildingManager, PlayerManager playerManager) : base(buildingManager, playerManager) { }
public override void Process(LargeWaterParkDeconstructed packet, Player player)
{
// SeparateChildrenToWaterParks must happen before ReplacePieceByGhost
// so the water park's children can be moved before it being removed
if (buildingManager.SeparateChildrenToWaterParks(packet) &&
buildingManager.ReplacePieceByGhost(player, packet, out _, out int operationId))
{
packet.BaseData = null;
SendToOtherPlayersWithOperationId(packet, player, operationId);
}
}
}

View File

@@ -0,0 +1,26 @@
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication.Packets.Processors;
public class LeakRepairedProcessor : AuthenticatedPacketProcessor<LeakRepaired>
{
private readonly WorldEntityManager worldEntityManager;
private readonly PlayerManager playerManager;
public LeakRepairedProcessor(WorldEntityManager worldEntityManager, PlayerManager playerManager)
{
this.worldEntityManager = worldEntityManager;
this.playerManager = playerManager;
}
public override void Process(LeakRepaired packet, Player player)
{
if (worldEntityManager.TryDestroyEntity(packet.LeakId, out _))
{
playerManager.SendPacketToOtherPlayers(packet, player);
}
}
}

View File

@@ -0,0 +1,18 @@
using NitroxModel.Packets;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Bases;
namespace NitroxServer.Communication.Packets.Processors;
public class ModifyConstructedAmountProcessor : BuildingProcessor<ModifyConstructedAmount>
{
public ModifyConstructedAmountProcessor(BuildingManager buildingManager, PlayerManager playerManager) : base(buildingManager, playerManager) { }
public override void Process(ModifyConstructedAmount packet, Player player)
{
if (buildingManager.ModifyConstructedAmount(packet))
{
playerManager.SendPacketToOtherPlayers(packet, player);
}
}
}

View File

@@ -0,0 +1,44 @@
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.Util;
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication.Packets.Processors
{
class ModuleAddedProcessor : AuthenticatedPacketProcessor<ModuleAdded>
{
private readonly PlayerManager playerManager;
private readonly EntityRegistry entityRegistry;
public ModuleAddedProcessor(PlayerManager playerManager, EntityRegistry entityRegistry)
{
this.playerManager = playerManager;
this.entityRegistry = entityRegistry;
}
public override void Process(ModuleAdded packet, Player player)
{
Optional<Entity> entity = entityRegistry.GetEntityById(packet.Id);
if (!entity.HasValue)
{
Log.Error($"Could not find entity {packet.Id} module added to a vehicle.");
return;
}
if (entity.Value is InventoryItemEntity inventoryItem)
{
InstalledModuleEntity moduleEntity = new(packet.Slot, inventoryItem.ClassId, inventoryItem.Id, inventoryItem.TechType, inventoryItem.Metadata, packet.ParentId, inventoryItem.ChildEntities);
// Convert the world entity into an inventory item
entityRegistry.AddOrUpdate(moduleEntity);
// Have other players respawn the item inside the inventory.
playerManager.SendPacketToOtherPlayers(new SpawnEntities(moduleEntity, forceRespawn: true), player);
}
}
}
}

View File

@@ -0,0 +1,44 @@
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.Util;
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication.Packets.Processors
{
class ModuleRemovedProcessor : AuthenticatedPacketProcessor<ModuleRemoved>
{
private readonly PlayerManager playerManager;
private readonly EntityRegistry entityRegistry;
public ModuleRemovedProcessor(PlayerManager playerManager, EntityRegistry entityRegistry)
{
this.playerManager = playerManager;
this.entityRegistry = entityRegistry;
}
public override void Process(ModuleRemoved packet, Player player)
{
Optional<Entity> entity = entityRegistry.GetEntityById(packet.Id);
if (!entity.HasValue)
{
Log.Error($"Could not find entity {packet.Id} module added to a vehicle.");
return;
}
if (entity.Value is InstalledModuleEntity installedModule)
{
InventoryItemEntity inventoryEntity = new(installedModule.Id, installedModule.ClassId, installedModule.TechType, installedModule.Metadata, packet.NewParentId, installedModule.ChildEntities);
// Convert the world entity into an inventory item
entityRegistry.AddOrUpdate(inventoryEntity);
// Have other players respawn the item inside the inventory.
playerManager.SendPacketToOtherPlayers(new SpawnEntities(inventoryEntity, forceRespawn: true), player);
}
}
}
}

View File

@@ -0,0 +1,23 @@
using NitroxModel.Packets;
using NitroxModel.Serialization;
using NitroxServer.Communication.Packets.Processors.Abstract;
namespace NitroxServer.Communication.Packets.Processors
{
public class MultiplayerSessionPolicyRequestProcessor : UnauthenticatedPacketProcessor<MultiplayerSessionPolicyRequest>
{
private readonly SubnauticaServerConfig config;
public MultiplayerSessionPolicyRequestProcessor(SubnauticaServerConfig config)
{
this.config = config;
}
// This will extend in the future when we look into different options for auth
public override void Process(MultiplayerSessionPolicyRequest packet, INitroxConnection connection)
{
Log.Info("Providing session policies...");
connection.SendPacket(new MultiplayerSessionPolicy(packet.CorrelationId, config.DisableConsole, config.MaxConnections, config.IsPasswordRequired()));
}
}
}

View File

@@ -0,0 +1,35 @@
using NitroxModel.MultiplayerSession;
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
namespace NitroxServer.Communication.Packets.Processors
{
public class MultiplayerSessionReservationRequestProcessor : UnauthenticatedPacketProcessor<MultiplayerSessionReservationRequest>
{
private readonly PlayerManager playerManager;
public MultiplayerSessionReservationRequestProcessor(PlayerManager playerManager)
{
this.playerManager = playerManager;
}
public override void Process(MultiplayerSessionReservationRequest packet, INitroxConnection connection)
{
Log.Info($"Processing reservation request from {packet.AuthenticationContext.Username}");
string correlationId = packet.CorrelationId;
PlayerSettings playerSettings = packet.PlayerSettings;
AuthenticationContext authenticationContext = packet.AuthenticationContext;
MultiplayerSessionReservation reservation = playerManager.ReservePlayerContext(
connection,
playerSettings,
authenticationContext,
correlationId);
Log.Info($"Reservation processed successfully: Username: {packet.AuthenticationContext.Username} - {reservation}");
connection.SendPacket(reservation);
}
}
}

View File

@@ -0,0 +1,25 @@
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Unlockables;
namespace NitroxServer.Communication.Packets.Processors
{
public class PDAEncyclopediaEntryAddProcessor : AuthenticatedPacketProcessor<PDAEncyclopediaEntryAdd>
{
private readonly PlayerManager playerManager;
private readonly PDAStateData pdaStateData;
public PDAEncyclopediaEntryAddProcessor(PlayerManager playerManager, PDAStateData pdaStateData)
{
this.playerManager = playerManager;
this.pdaStateData = pdaStateData;
}
public override void Process(PDAEncyclopediaEntryAdd packet, Player player)
{
pdaStateData.AddEncyclopediaEntry(packet.Key);
playerManager.SendPacketToOtherPlayers(packet, player);
}
}
}

View File

@@ -0,0 +1,32 @@
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Unlockables;
namespace NitroxServer.Communication.Packets.Processors
{
public class PDALogEntryAddProcessor : AuthenticatedPacketProcessor<PDALogEntryAdd>
{
private readonly PlayerManager playerManager;
private readonly PDAStateData pdaState;
private readonly ScheduleKeeper scheduleKeeper;
public PDALogEntryAddProcessor(PlayerManager playerManager, PDAStateData pdaState, ScheduleKeeper scheduleKeeper)
{
this.playerManager = playerManager;
this.pdaState = pdaState;
this.scheduleKeeper = scheduleKeeper;
}
public override void Process(PDALogEntryAdd packet, Player player)
{
pdaState.AddPDALogEntry(new PDALogEntry(packet.Key, packet.Timestamp));
if (scheduleKeeper.ContainsScheduledGoal(packet.Key))
{
scheduleKeeper.UnScheduleGoal(packet.Key);
}
playerManager.SendPacketToOtherPlayers(packet, player);
}
}
}

View File

@@ -0,0 +1,43 @@
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Entities;
using NitroxServer.GameLogic.Unlockables;
namespace NitroxServer.Communication.Packets.Processors;
public class PDAScanFinishedPacketProcessor : AuthenticatedPacketProcessor<PDAScanFinished>
{
private readonly PlayerManager playerManager;
private readonly PDAStateData pdaStateData;
private readonly WorldEntityManager worldEntityManager;
public PDAScanFinishedPacketProcessor(PlayerManager playerManager, PDAStateData pdaStateData, WorldEntityManager worldEntityManager)
{
this.playerManager = playerManager;
this.pdaStateData = pdaStateData;
this.worldEntityManager = worldEntityManager;
}
public override void Process(PDAScanFinished packet, Player player)
{
if (!packet.WasAlreadyResearched)
{
pdaStateData.UpdateEntryUnlockedProgress(packet.TechType, packet.UnlockedAmount, packet.FullyResearched);
}
playerManager.SendPacketToOtherPlayers(packet, player);
if (packet.Id != null)
{
if (packet.Destroy)
{
worldEntityManager.TryDestroyEntity(packet.Id, out _);
}
else
{
pdaStateData.AddScannerFragment(packet.Id);
}
}
}
}

View File

@@ -0,0 +1,55 @@
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.Util;
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication.Packets.Processors;
public class PickupItemPacketProcessor : AuthenticatedPacketProcessor<PickupItem>
{
private readonly EntityRegistry entityRegistry;
private readonly WorldEntityManager worldEntityManager;
private readonly PlayerManager playerManager;
private readonly SimulationOwnershipData simulationOwnershipData;
public PickupItemPacketProcessor(EntityRegistry entityRegistry, WorldEntityManager worldEntityManager, PlayerManager playerManager, SimulationOwnershipData simulationOwnershipData)
{
this.entityRegistry = entityRegistry;
this.worldEntityManager = worldEntityManager;
this.playerManager = playerManager;
this.simulationOwnershipData = simulationOwnershipData;
}
public override void Process(PickupItem packet, Player player)
{
NitroxId id = packet.Item.Id;
if (simulationOwnershipData.RevokeOwnerOfId(id))
{
ushort serverId = ushort.MaxValue;
SimulationOwnershipChange simulationOwnershipChange = new(id, serverId, SimulationLockType.TRANSIENT);
playerManager.SendPacketToAllPlayers(simulationOwnershipChange);
}
StopTrackingExistingWorldEntity(id);
entityRegistry.AddOrUpdate(packet.Item);
// Have other players respawn the item inside the inventory.
playerManager.SendPacketToOtherPlayers(new SpawnEntities(packet.Item, forceRespawn: true), player);
}
private void StopTrackingExistingWorldEntity(NitroxId id)
{
Optional<Entity> entity = entityRegistry.GetEntityById(id);
if (entity.HasValue && entity.Value is WorldEntity worldEntity)
{
// Do not track this entity in the open world anymore.
worldEntityManager.StopTrackingEntity(worldEntity);
}
}
}

View File

@@ -0,0 +1,19 @@
using NitroxModel.Packets;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Bases;
namespace NitroxServer.Communication.Packets.Processors;
public class PieceDeconstructedProcessor : BuildingProcessor<PieceDeconstructed>
{
public PieceDeconstructedProcessor(BuildingManager buildingManager, PlayerManager playerManager) : base(buildingManager, playerManager) { }
public override void Process(PieceDeconstructed packet, Player player)
{
if (buildingManager.ReplacePieceByGhost(player, packet, out _, out int operationId))
{
packet.BaseData = null;
SendToOtherPlayersWithOperationId(packet, player, operationId);
}
}
}

View File

@@ -0,0 +1,13 @@
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
namespace NitroxServer.Communication.Packets.Processors;
public class PinnedRecipeMovedProcessor : AuthenticatedPacketProcessor<PinnedRecipeMoved>
{
public override void Process(PinnedRecipeMoved packet, Player player)
{
player.PinnedRecipePreferences.Clear();
player.PinnedRecipePreferences.AddRange(packet.RecipePins);
}
}

View File

@@ -0,0 +1,23 @@
using NitroxModel.Packets;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Bases;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication.Packets.Processors;
public class PlaceBaseProcessor : BuildingProcessor<PlaceBase>
{
public PlaceBaseProcessor(BuildingManager buildingManager, PlayerManager playerManager, EntitySimulation entitySimulation) : base(buildingManager, playerManager, entitySimulation){ }
public override void Process(PlaceBase packet, Player player)
{
if (buildingManager.CreateBase(packet))
{
ClaimBuildPiece(packet.BuildEntity, player);
// End-players can process elementary operations without this data (packet would be heavier for no reason)
packet.Deflate();
playerManager.SendPacketToOtherPlayers(packet, player);
}
}
}

View File

@@ -0,0 +1,18 @@
using NitroxModel.Packets;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Bases;
namespace NitroxServer.Communication.Packets.Processors;
public class PlaceGhostProcessor : BuildingProcessor<PlaceGhost>
{
public PlaceGhostProcessor(BuildingManager buildingManager, PlayerManager playerManager) : base(buildingManager, playerManager) { }
public override void Process(PlaceGhost packet, Player player)
{
if (buildingManager.AddGhost(packet))
{
playerManager.SendPacketToOtherPlayers(packet, player);
}
}
}

View File

@@ -0,0 +1,23 @@
using NitroxModel.Packets;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Bases;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication.Packets.Processors;
public class PlaceModuleProcessor : BuildingProcessor<PlaceModule>
{
public PlaceModuleProcessor(BuildingManager buildingManager, PlayerManager playerManager, EntitySimulation entitySimulation) : base(buildingManager, playerManager, entitySimulation) { }
public override void Process(PlaceModule packet, Player player)
{
if (buildingManager.AddModule(packet))
{
if (packet.ModuleEntity.ParentId == null)
{
ClaimBuildPiece(packet.ModuleEntity, player);
}
playerManager.SendPacketToOtherPlayers(packet, player);
}
}
}

View File

@@ -0,0 +1,40 @@
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.Packets;
using NitroxModel.Serialization;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
namespace NitroxServer.Communication.Packets.Processors
{
class PlayerDeathEventProcessor : AuthenticatedPacketProcessor<PlayerDeathEvent>
{
private readonly PlayerManager playerManager;
private readonly SubnauticaServerConfig serverConfig;
public PlayerDeathEventProcessor(PlayerManager playerManager, SubnauticaServerConfig config)
{
this.playerManager = playerManager;
this.serverConfig = config;
}
public override void Process(PlayerDeathEvent packet, Player player)
{
if (serverConfig.IsHardcore())
{
player.IsPermaDeath = true;
PlayerKicked playerKicked = new PlayerKicked("Permanent death from hardcore mode");
player.SendPacket(playerKicked);
}
player.LastStoredPosition = packet.DeathPosition;
player.LastStoredSubRootID = player.SubRootId;
if (player.Permissions > Perms.MODERATOR)
{
player.SendPacket(new ChatMessage(ChatMessage.SERVER_ID, "You can use /back to go to your death location"));
}
playerManager.SendPacketToOtherPlayers(packet, player);
}
}
}

View File

@@ -0,0 +1,26 @@
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
namespace NitroxServer.Communication.Packets.Processors
{
public class PlayerHeldItemChangedProcessor : AuthenticatedPacketProcessor<PlayerHeldItemChanged>
{
private readonly PlayerManager playerManager;
public PlayerHeldItemChangedProcessor(PlayerManager playerManager)
{
this.playerManager = playerManager;
}
public override void Process(PlayerHeldItemChanged packet, Player player)
{
if (packet.IsFirstTime != null && !player.UsedItems.Contains(packet.IsFirstTime))
{
player.UsedItems.Add(packet.IsFirstTime);
}
playerManager.SendPacketToOtherPlayers(packet, player);
}
}
}

View File

@@ -0,0 +1,36 @@
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication.Packets.Processors;
public class PlayerInCyclopsMovementProcessor : AuthenticatedPacketProcessor<PlayerInCyclopsMovement>
{
private readonly PlayerManager playerManager;
private readonly EntityRegistry entityRegistry;
public PlayerInCyclopsMovementProcessor(PlayerManager playerManager, EntityRegistry entityRegistry)
{
this.playerManager = playerManager;
this.entityRegistry = entityRegistry;
}
public override void Process(PlayerInCyclopsMovement packet, Player player)
{
if (entityRegistry.TryGetEntityById(player.PlayerContext.PlayerNitroxId, out PlayerWorldEntity playerWorldEntity))
{
playerWorldEntity.Transform.LocalPosition = packet.LocalPosition;
playerWorldEntity.Transform.LocalRotation = packet.LocalRotation;
player.Position = playerWorldEntity.Transform.Position;
player.Rotation = playerWorldEntity.Transform.Rotation;
playerManager.SendPacketToOtherPlayers(packet, player);
}
else
{
Log.ErrorOnce($"{nameof(PlayerWorldEntity)} couldn't be found for player {player.Name}. It is adviced the player reconnects before losing too much progression.");
}
}
}

View File

@@ -0,0 +1,128 @@
using System.Collections.Generic;
using System.Linq;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.Unity;
using NitroxModel.DataStructures.Util;
using NitroxModel.MultiplayerSession;
using NitroxModel.Networking;
using NitroxModel.Packets;
using NitroxModel.Serialization;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Bases;
using NitroxServer.GameLogic.Entities;
using NitroxServer.Serialization.World;
namespace NitroxServer.Communication.Packets.Processors
{
public class PlayerJoiningMultiplayerSessionProcessor : UnauthenticatedPacketProcessor<PlayerJoiningMultiplayerSession>
{
private readonly PlayerManager playerManager;
private readonly ScheduleKeeper scheduleKeeper;
private readonly StoryManager storyManager;
private readonly World world;
private readonly EntityRegistry entityRegistry;
private readonly SubnauticaServerConfig serverConfig;
private readonly NtpSyncer ntpSyncer;
private readonly SessionSettings sessionSettings;
public PlayerJoiningMultiplayerSessionProcessor(ScheduleKeeper scheduleKeeper, StoryManager storyManager, PlayerManager playerManager, World world, EntityRegistry entityRegistry, SubnauticaServerConfig serverConfig, NtpSyncer ntpSyncer, SessionSettings sessionSettings)
{
this.scheduleKeeper = scheduleKeeper;
this.storyManager = storyManager;
this.playerManager = playerManager;
this.world = world;
this.entityRegistry = entityRegistry;
this.serverConfig = serverConfig;
this.ntpSyncer = ntpSyncer;
this.sessionSettings = sessionSettings;
}
public override void Process(PlayerJoiningMultiplayerSession packet, INitroxConnection connection)
{
Player player = playerManager.PlayerConnected(connection, packet.ReservationKey, out bool wasBrandNewPlayer);
NitroxId assignedEscapePodId = world.EscapePodManager.AssignPlayerToEscapePod(player.Id, out Optional<EscapePodWorldEntity> newlyCreatedEscapePod);
if (wasBrandNewPlayer)
{
player.SubRootId = assignedEscapePodId;
}
if (newlyCreatedEscapePod.HasValue)
{
SpawnEntities spawnNewEscapePod = new(newlyCreatedEscapePod.Value);
playerManager.SendPacketToOtherPlayers(spawnNewEscapePod, player);
}
// Make players on localhost admin by default.
if (connection.Endpoint.Address.IsLocalhost())
{
Log.Info($"Granted admin to '{player.Name}' because they're playing on the host machine");
player.Permissions = Perms.ADMIN;
}
List<SimulatedEntity> simulations = world.EntitySimulation.AssignGlobalRootEntitiesAndGetData(player);
player.Entity = wasBrandNewPlayer ? SetupPlayerEntity(player) : RespawnExistingEntity(player);
List<GlobalRootEntity> globalRootEntities = world.WorldEntityManager.GetGlobalRootEntities(true);
bool isFirstPlayer = playerManager.GetConnectedPlayers().Count == 1;
InitialPlayerSync initialPlayerSync = new(player.GameObjectId,
wasBrandNewPlayer,
assignedEscapePodId,
player.EquippedItems,
player.UsedItems,
player.QuickSlotsBindingIds,
world.GameData.PDAState.GetInitialPDAData(),
world.GameData.StoryGoals.GetInitialStoryGoalData(scheduleKeeper, player),
player.Position,
player.Rotation,
player.SubRootId,
player.Stats,
GetOtherPlayers(player),
globalRootEntities,
simulations,
player.GameMode,
player.Permissions,
wasBrandNewPlayer ? IntroCinematicMode.LOADING : IntroCinematicMode.COMPLETED,
new(new(player.PingInstancePreferences), player.PinnedRecipePreferences.ToList()),
storyManager.GetTimeData(),
isFirstPlayer,
BuildingManager.GetEntitiesOperations(globalRootEntities),
serverConfig.KeepInventoryOnDeath,
sessionSettings
);
player.SendPacket(initialPlayerSync);
}
private IEnumerable<PlayerContext> GetOtherPlayers(Player player)
{
return playerManager.GetConnectedPlayers().Where(p => p != player)
.Select(p => p.PlayerContext);
}
private PlayerWorldEntity SetupPlayerEntity(Player player)
{
NitroxTransform transform = new(player.Position, player.Rotation, NitroxVector3.One);
PlayerWorldEntity playerEntity = new PlayerWorldEntity(transform, 0, null, false, player.GameObjectId, NitroxTechType.None, null, null, new List<Entity>());
entityRegistry.AddOrUpdate(playerEntity);
world.WorldEntityManager.TrackEntityInTheWorld(playerEntity);
return playerEntity;
}
private PlayerWorldEntity RespawnExistingEntity(Player player)
{
if (entityRegistry.TryGetEntityById(player.PlayerContext.PlayerNitroxId, out PlayerWorldEntity playerWorldEntity))
{
return playerWorldEntity;
}
Log.Error($"Unable to find player entity for {player.Name}. Re-creating one");
return SetupPlayerEntity(player);
}
}
}

View File

@@ -0,0 +1,37 @@
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.Util;
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication.Packets.Processors
{
class PlayerMovementProcessor : AuthenticatedPacketProcessor<PlayerMovement>
{
private readonly PlayerManager playerManager;
private readonly EntityRegistry entityRegistry;
public PlayerMovementProcessor(PlayerManager playerManager, EntityRegistry entityRegistry)
{
this.playerManager = playerManager;
this.entityRegistry = entityRegistry;
}
public override void Process(PlayerMovement packet, Player player)
{
Optional<PlayerWorldEntity> playerEntity = entityRegistry.GetEntityById<PlayerWorldEntity>(player.PlayerContext.PlayerNitroxId);
if (playerEntity.HasValue)
{
playerEntity.Value.Transform.Position = packet.Position;
playerEntity.Value.Transform.Rotation = packet.BodyRotation;
}
player.Position = packet.Position;
player.Rotation = packet.BodyRotation;
playerManager.SendPacketToOtherPlayers(packet, player);
}
}
}

View File

@@ -0,0 +1,13 @@
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
namespace NitroxServer.Communication.Packets.Processors
{
public class PlayerQuickSlotsBindingChangedProcessor : AuthenticatedPacketProcessor<PlayerQuickSlotsBindingChanged>
{
public override void Process(PlayerQuickSlotsBindingChanged packet, Player player)
{
player.QuickSlotsBindingIds = packet.SlotItemIds;
}
}
}

View File

@@ -0,0 +1,23 @@
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication.Packets.Processors;
public class PlayerSeeOutOfCellEntityProcessor : AuthenticatedPacketProcessor<PlayerSeeOutOfCellEntity>
{
private readonly EntityRegistry entityRegistry;
public PlayerSeeOutOfCellEntityProcessor(EntityRegistry entityRegistry)
{
this.entityRegistry = entityRegistry;
}
public override void Process(PlayerSeeOutOfCellEntity packet, Player player)
{
if (entityRegistry.GetEntityById(packet.EntityId).HasValue)
{
player.OutOfCellVisibleEntities.Add(packet.EntityId);
}
}
}

View File

@@ -0,0 +1,27 @@
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
namespace NitroxServer.Communication.Packets.Processors;
public class PlayerStatsProcessor : AuthenticatedPacketProcessor<PlayerStats>
{
private readonly PlayerManager playerManager;
public PlayerStatsProcessor(PlayerManager playerManager)
{
this.playerManager = playerManager;
}
public override void Process(PlayerStats packet, Player player)
{
if (packet.PlayerId != player.Id)
{
Log.WarnOnce($"[{nameof(PlayerStatsProcessor)}] Player ID mismatch (received: {packet.PlayerId}, real: {player.Id})");
packet.PlayerId = player.Id;
}
player.Stats = new PlayerStatsData(packet.Oxygen, packet.MaxOxygen, packet.Health, packet.Food, packet.Water, packet.InfectionAmount);
playerManager.SendPacketToOtherPlayers(packet, player);
}
}

View File

@@ -0,0 +1,27 @@
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
namespace NitroxServer.Communication.Packets.Processors
{
public class PlayerSyncFinishedProcessor : AuthenticatedPacketProcessor<PlayerSyncFinished>
{
private readonly PlayerManager playerManager;
public PlayerSyncFinishedProcessor(PlayerManager playerManager)
{
this.playerManager = playerManager;
}
public override void Process(PlayerSyncFinished packet, Player player)
{
// If this is the first player connecting we need to restart time at this exact moment
if (playerManager.GetConnectedPlayers().Count == 1)
{
Server.Instance.ResumeServer();
}
playerManager.FinishProcessingReservation(player);
}
}
}

View File

@@ -0,0 +1,58 @@
using System.Collections.Generic;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication.Packets.Processors;
public class PlayerUnseeOutOfCellEntityProcessor : AuthenticatedPacketProcessor<PlayerUnseeOutOfCellEntity>
{
private readonly SimulationOwnershipData simulationOwnershipData;
private readonly PlayerManager playerManager;
private readonly EntitySimulation entitySimulation;
private readonly EntityRegistry entityRegistry;
public PlayerUnseeOutOfCellEntityProcessor(SimulationOwnershipData simulationOwnershipData, PlayerManager playerManager, EntitySimulation entitySimulation, EntityRegistry entityRegistry)
{
this.simulationOwnershipData = simulationOwnershipData;
this.playerManager = playerManager;
this.entitySimulation = entitySimulation;
this.entityRegistry = entityRegistry;
}
public override void Process(PlayerUnseeOutOfCellEntity packet, Player player)
{
// Most of this packet's utility is in the below Remove
if (!player.OutOfCellVisibleEntities.Remove(packet.EntityId) ||
!entityRegistry.TryGetEntityById(packet.EntityId, out Entity entity))
{
return;
}
// If player can still see the entity even after removing it from the OutOfCellVisibleEntities, then we don't need to change anything
if (player.CanSee(entity))
{
return;
}
// If the player doesn't own the entity's simulation then we don't need to do anything
if (!simulationOwnershipData.RevokeIfOwner(packet.EntityId, player))
{
return;
}
List<Player> otherPlayers = playerManager.GetConnectedPlayersExcept(player);
if (entitySimulation.TryAssignEntityToPlayers(otherPlayers, entity, out SimulatedEntity simulatedEntity))
{
entitySimulation.BroadcastSimulationChanges([simulatedEntity]);
}
else
{
// No player has taken simulation on the entity
playerManager.SendPacketToAllPlayers(new DropSimulationOwnership(packet.EntityId));
}
}
}

View File

@@ -0,0 +1,46 @@
using System.Collections.Generic;
using NitroxModel.Packets;
using NitroxModel.Serialization;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
using NitroxServer.Serialization;
namespace NitroxServer.Communication.Packets.Processors;
public class PvPAttackProcessor : AuthenticatedPacketProcessor<PvPAttack>
{
private readonly SubnauticaServerConfig serverConfig;
private readonly PlayerManager playerManager;
// TODO: In the future, do a whole config for damage sources
private static readonly Dictionary<PvPAttack.AttackType, float> damageMultiplierByType = new()
{
{ PvPAttack.AttackType.KnifeHit, 0.5f },
{ PvPAttack.AttackType.HeatbladeHit, 1f }
};
public PvPAttackProcessor(SubnauticaServerConfig serverConfig, PlayerManager playerManager)
{
this.serverConfig = serverConfig;
this.playerManager = playerManager;
}
public override void Process(PvPAttack packet, Player player)
{
if (!serverConfig.PvPEnabled)
{
return;
}
if (!playerManager.TryGetPlayerById(packet.TargetPlayerId, out Player targetPlayer))
{
return;
}
if (!damageMultiplierByType.TryGetValue(packet.Type, out float multiplier))
{
return;
}
packet.Damage *= multiplier;
targetPlayer.SendPacket(packet);
}
}

View File

@@ -0,0 +1,28 @@
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Unlockables;
namespace NitroxServer.Communication.Packets.Processors
{
public class RadioPlayPendingMessageProcessor : AuthenticatedPacketProcessor<RadioPlayPendingMessage>
{
private readonly StoryGoalData storyGoalData;
private readonly PlayerManager playerManager;
public RadioPlayPendingMessageProcessor(StoryGoalData storyGoalData, PlayerManager playerManager)
{
this.storyGoalData = storyGoalData;
this.playerManager = playerManager;
}
public override void Process(RadioPlayPendingMessage packet, Player player)
{
if (!storyGoalData.RemovedLatestRadioMessage())
{
Log.Warn($"Tried to remove the latest radio message but the radio queue is empty: {packet}");
}
playerManager.SendPacketToOtherPlayers(packet, player);
}
}
}

View File

@@ -0,0 +1,14 @@
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication.Packets.Processors;
public class RangedAttackLastTargetUpdateProcessor(
PlayerManager playerManager,
EntityRegistry entityRegistry
) : TransmitIfCanSeePacketProcessor<RangedAttackLastTargetUpdate>(playerManager, entityRegistry)
{
public override void Process(RangedAttackLastTargetUpdate packet, Player sender) => TransmitIfCanSeeEntities(packet, sender, [packet.CreatureId, packet.TargetId]);
}

View File

@@ -0,0 +1,19 @@
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
namespace NitroxServer.Communication.Packets.Processors;
public class PinnedRecipeProcessor : AuthenticatedPacketProcessor<RecipePinned>
{
public override void Process(RecipePinned packet, Player player)
{
if (packet.Pinned)
{
player.PinnedRecipePreferences.Add(packet.TechType);
}
else
{
player.PinnedRecipePreferences.Remove(packet.TechType);
}
}
}

View File

@@ -0,0 +1,41 @@
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.Util;
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication.Packets.Processors;
public class RemoveCreatureCorpseProcessor : AuthenticatedPacketProcessor<RemoveCreatureCorpse>
{
private readonly PlayerManager playerManager;
private readonly EntitySimulation entitySimulation;
private readonly WorldEntityManager worldEntityManager;
public RemoveCreatureCorpseProcessor(PlayerManager playerManager, EntitySimulation entitySimulation, WorldEntityManager worldEntityManager)
{
this.playerManager = playerManager;
this.worldEntityManager = worldEntityManager;
this.entitySimulation = entitySimulation;
}
public override void Process(RemoveCreatureCorpse packet, Player destroyingPlayer)
{
// TODO: In the future, for more immersion (though that's a neglectable +), have a corpse entity on server-side or a dedicated metadata for this entity (CorpseMetadata)
// So that even players rejoining can see it (before it despawns)
entitySimulation.EntityDestroyed(packet.CreatureId);
if (worldEntityManager.TryDestroyEntity(packet.CreatureId, out Entity entity))
{
foreach (Player player in playerManager.GetConnectedPlayers())
{
bool isOtherPlayer = player != destroyingPlayer;
if (isOtherPlayer && player.CanSee(entity))
{
player.SendPacket(packet);
}
}
}
}
}

View File

@@ -0,0 +1,25 @@
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
namespace NitroxServer.Communication.Packets.Processors
{
public class ScheduleProcessor : AuthenticatedPacketProcessor<Schedule>
{
private readonly PlayerManager playerManager;
private readonly ScheduleKeeper scheduleKeeper;
public ScheduleProcessor(PlayerManager playerManager, ScheduleKeeper scheduleKeeper)
{
this.playerManager = playerManager;
this.scheduleKeeper = scheduleKeeper;
}
public override void Process(Schedule packet, Player player)
{
scheduleKeeper.ScheduleGoal(NitroxScheduledGoal.From(packet.TimeExecute, packet.Key, packet.Type));
playerManager.SendPacketToOtherPlayers(packet, player);
}
}
}

View File

@@ -0,0 +1,14 @@
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication.Packets.Processors;
public class SeaDragonAttackTargetProcessor(
PlayerManager playerManager,
EntityRegistry entityRegistry
) : TransmitIfCanSeePacketProcessor<SeaDragonAttackTarget>(playerManager, entityRegistry)
{
public override void Process(SeaDragonAttackTarget packet, Player sender) => TransmitIfCanSeeEntities(packet, sender, [packet.SeaDragonId, packet.TargetId]);
}

View File

@@ -0,0 +1,14 @@
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication.Packets.Processors;
public class SeaDragonGrabExosuitProcessor(
PlayerManager playerManager,
EntityRegistry entityRegistry
) : TransmitIfCanSeePacketProcessor<SeaDragonGrabExosuit>(playerManager, entityRegistry)
{
public override void Process(SeaDragonGrabExosuit packet, Player sender) => TransmitIfCanSeeEntities(packet, sender, [packet.SeaDragonId, packet.TargetId]);
}

View File

@@ -0,0 +1,14 @@
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication.Packets.Processors;
public class SeaDragonSwatAttackProcessor(
PlayerManager playerManager,
EntityRegistry entityRegistry
) : TransmitIfCanSeePacketProcessor<SeaDragonSwatAttack>(playerManager, entityRegistry)
{
public override void Process(SeaDragonSwatAttack packet, Player sender) => TransmitIfCanSeeEntities(packet, sender, [packet.SeaDragonId, packet.TargetId]);
}

View File

@@ -0,0 +1,14 @@
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication.Packets.Processors;
public class SeaTreaderSpawnedChunkProcessor(
PlayerManager playerManager,
EntityRegistry entityRegistry
) : TransmitIfCanSeePacketProcessor<SeaTreaderSpawnedChunk>(playerManager, entityRegistry)
{
public override void Process(SeaTreaderSpawnedChunk packet, Player sender) => TransmitIfCanSeeEntities(packet, sender, [packet.CreatureId]);
}

View File

@@ -0,0 +1,23 @@
using NitroxModel.DataStructures.Util;
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.ConsoleCommands.Processor;
namespace NitroxServer.Communication.Packets.Processors
{
public class ServerCommandProcessor : AuthenticatedPacketProcessor<ServerCommand>
{
private readonly ConsoleCommandProcessor cmdProcessor;
public ServerCommandProcessor(ConsoleCommandProcessor cmdProcessor)
{
this.cmdProcessor = cmdProcessor;
}
public override void Process(ServerCommand packet, Player player)
{
Log.Info($"{player.Name} issued command: /{packet.Cmd}");
cmdProcessor.ProcessCommand(packet.Cmd, Optional.Of(player), player.Permissions);
}
}
}

View File

@@ -0,0 +1,42 @@
using System.Linq;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
namespace NitroxServer.Communication.Packets.Processors;
public class SetIntroCinematicModeProcessor : AuthenticatedPacketProcessor<SetIntroCinematicMode>
{
private readonly PlayerManager playerManager;
public SetIntroCinematicModeProcessor(PlayerManager playerManager)
{
this.playerManager = playerManager;
}
public override void Process(SetIntroCinematicMode packet, Player player)
{
if (packet.PlayerId != player.Id)
{
Log.Warn($"Received {nameof(SetIntroCinematicMode)} packet where packet.{nameof(SetIntroCinematicMode.PlayerId)} was not equal to sending playerId");
return;
}
packet.PartnerId = null; // Resetting incoming packets just to be safe we don't relay any PartnerId. Server has only authority.
player.PlayerContext.IntroCinematicMode = packet.Mode;
playerManager.SendPacketToOtherPlayers(packet, player);
Log.Debug($"Set IntroCinematicMode to {packet.Mode} for {player.PlayerContext.PlayerName}");
Player[] allWaitingPlayers = playerManager.ConnectedPlayers().Where(p => p.PlayerContext.IntroCinematicMode == IntroCinematicMode.WAITING).ToArray();
if (allWaitingPlayers.Length >= 2)
{
Log.Info($"Starting IntroCinematic for {allWaitingPlayers[0].PlayerContext.PlayerName} and {allWaitingPlayers[1].PlayerContext.PlayerName}");
allWaitingPlayers[0].PlayerContext.IntroCinematicMode = allWaitingPlayers[1].PlayerContext.IntroCinematicMode = IntroCinematicMode.START;
playerManager.SendPacketToAllPlayers(new SetIntroCinematicMode(allWaitingPlayers[0].Id, IntroCinematicMode.START, allWaitingPlayers[1].Id));
playerManager.SendPacketToAllPlayers(new SetIntroCinematicMode(allWaitingPlayers[1].Id, IntroCinematicMode.START, allWaitingPlayers[0].Id));
}
}
}

View File

@@ -0,0 +1,12 @@
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
namespace NitroxServer.Communication.Packets.Processors;
public class SignalPingPreferenceChangedProcessor : AuthenticatedPacketProcessor<SignalPingPreferenceChanged>
{
public override void Process(SignalPingPreferenceChanged packet, Player player)
{
player.PingInstancePreferences[packet.PingKey] = new(packet.Color, packet.Visible);
}
}

View File

@@ -0,0 +1,35 @@
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication.Packets.Processors;
public class SimulationOwnershipRequestProcessor : AuthenticatedPacketProcessor<SimulationOwnershipRequest>
{
private readonly PlayerManager playerManager;
private readonly SimulationOwnershipData simulationOwnershipData;
private readonly EntitySimulation entitySimulation;
public SimulationOwnershipRequestProcessor(PlayerManager playerManager, SimulationOwnershipData simulationOwnershipData, EntitySimulation entitySimulation)
{
this.playerManager = playerManager;
this.simulationOwnershipData = simulationOwnershipData;
this.entitySimulation = entitySimulation;
}
public override void Process(SimulationOwnershipRequest ownershipRequest, Player player)
{
bool aquiredLock = simulationOwnershipData.TryToAcquire(ownershipRequest.Id, player, ownershipRequest.LockType);
if (aquiredLock)
{
bool shouldEntityMove = entitySimulation.ShouldSimulateEntityMovement(ownershipRequest.Id);
SimulationOwnershipChange simulationOwnershipChange = new(ownershipRequest.Id, player.Id, ownershipRequest.LockType, shouldEntityMove);
playerManager.SendPacketToOtherPlayers(simulationOwnershipChange, player);
}
SimulationOwnershipResponse responseToPlayer = new(ownershipRequest.Id, aquiredLock, ownershipRequest.LockType);
player.SendPacket(responseToPlayer);
}
}

View File

@@ -0,0 +1,48 @@
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Unlockables;
namespace NitroxServer.Communication.Packets.Processors;
public class StoryGoalExecutedProcessor : AuthenticatedPacketProcessor<StoryGoalExecuted>
{
private readonly PlayerManager playerManager;
private readonly StoryGoalData storyGoalData;
private readonly ScheduleKeeper scheduleKeeper;
private readonly PDAStateData pdaStateData;
public StoryGoalExecutedProcessor(PlayerManager playerManager, StoryGoalData storyGoalData, ScheduleKeeper scheduleKeeper, PDAStateData pdaStateData)
{
this.playerManager = playerManager;
this.storyGoalData = storyGoalData;
this.scheduleKeeper = scheduleKeeper;
this.pdaStateData = pdaStateData;
}
public override void Process(StoryGoalExecuted packet, Player player)
{
Log.Debug($"Processing StoryGoalExecuted: {packet}");
// The switch is structure is similar to StoryGoal.Execute()
bool added = storyGoalData.CompletedGoals.Add(packet.Key);
switch (packet.Type)
{
case StoryGoalExecuted.EventType.RADIO:
if (added)
{
storyGoalData.RadioQueue.Enqueue(packet.Key);
}
break;
case StoryGoalExecuted.EventType.PDA:
if (packet.Timestamp.HasValue)
{
pdaStateData.AddPDALogEntry(new(packet.Key, packet.Timestamp.Value));
}
break;
}
scheduleKeeper.UnScheduleGoal(packet.Key);
playerManager.SendPacketToOtherPlayers(packet, player);
}
}

View File

@@ -0,0 +1,26 @@
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication.Packets.Processors
{
class SubRootChangedPacketProcessor : AuthenticatedPacketProcessor<SubRootChanged>
{
private readonly PlayerManager playerManager;
private readonly EntityRegistry entityRegistry;
public SubRootChangedPacketProcessor(PlayerManager playerManager, EntityRegistry entityRegistry)
{
this.playerManager = playerManager;
this.entityRegistry = entityRegistry;
}
public override void Process(SubRootChanged packet, Player player)
{
entityRegistry.ReparentEntity(player.GameObjectId, packet.SubRootId.OrNull());
player.SubRootId = packet.SubRootId;
playerManager.SendPacketToOtherPlayers(packet, player);
}
}
}

View File

@@ -0,0 +1,26 @@
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.Packets;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Bases;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication.Packets.Processors;
public class UpdateBaseProcessor : BuildingProcessor<UpdateBase>
{
public UpdateBaseProcessor(BuildingManager buildingManager, PlayerManager playerManager, EntitySimulation entitySimulation) : base(buildingManager, playerManager, entitySimulation) { }
public override void Process(UpdateBase packet, Player player)
{
if (buildingManager.UpdateBase(player, packet, out int operationId))
{
if (packet.BuiltPieceEntity is GlobalRootEntity entity)
{
ClaimBuildPiece(entity, player);
}
// End-players can process elementary operations without this data (packet would be heavier for no reason)
packet.Deflate();
SendToOtherPlayersWithOperationId(packet, player, operationId);
}
}
}

View File

@@ -0,0 +1,38 @@
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication.Packets.Processors;
public class VehicleDockingProcessor : AuthenticatedPacketProcessor<VehicleDocking>
{
private readonly PlayerManager playerManager;
private readonly EntityRegistry entityRegistry;
public VehicleDockingProcessor(PlayerManager playerManager, EntityRegistry entityRegistry)
{
this.playerManager = playerManager;
this.entityRegistry = entityRegistry;
}
public override void Process(VehicleDocking packet, Player player)
{
if (!entityRegistry.TryGetEntityById(packet.VehicleId, out Entity vehicleEntity))
{
Log.Error($"Unable to find vehicle to dock {packet.VehicleId}");
return;
}
if (!entityRegistry.TryGetEntityById(packet.DockId, out Entity dockEntity))
{
Log.Error($"Unable to find dock {packet.DockId} for docking vehicle {packet.VehicleId}");
return;
}
entityRegistry.ReparentEntity(vehicleEntity, dockEntity);
playerManager.SendPacketToOtherPlayers(packet, player);
}
}

View File

@@ -0,0 +1,63 @@
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.Unity;
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication.Packets.Processors;
public class VehicleMovementsPacketProcessor : AuthenticatedPacketProcessor<VehicleMovements>
{
private static readonly NitroxVector3 CyclopsSteeringWheelRelativePosition = new(-0.05f, 0.97f, -23.54f);
private readonly PlayerManager playerManager;
private readonly EntityRegistry entityRegistry;
private readonly SimulationOwnershipData simulationOwnershipData;
public VehicleMovementsPacketProcessor(PlayerManager playerManager, EntityRegistry entityRegistry, SimulationOwnershipData simulationOwnershipData)
{
this.playerManager = playerManager;
this.entityRegistry = entityRegistry;
this.simulationOwnershipData = simulationOwnershipData;
}
public override void Process(VehicleMovements packet, Player player)
{
for (int i = packet.Data.Count - 1; i >= 0; i--)
{
MovementData movementData = packet.Data[i];
if (simulationOwnershipData.GetPlayerForLock(movementData.Id) != player)
{
Log.WarnOnce($"Player {player.Name} tried updating {movementData.Id}'s position but they don't have the lock on it");
// TODO: In the future, add "packet.Data.RemoveAt(i);" and "continue;" to prevent those abnormal situations
}
if (entityRegistry.TryGetEntityById(movementData.Id, out WorldEntity worldEntity))
{
worldEntity.Transform.Position = movementData.Position;
worldEntity.Transform.Rotation = movementData.Rotation;
if (movementData is DrivenVehicleMovementData)
{
// Cyclops' driving wheel is at a known position so we need to adapt the position of the player accordingly
if (worldEntity.TechType.Name.Equals("Cyclops"))
{
player.Entity.Transform.LocalPosition = CyclopsSteeringWheelRelativePosition;
player.Position = player.Entity.Transform.Position;
}
else
{
player.Position = movementData.Position;
player.Rotation = movementData.Rotation;
}
}
}
}
if (packet.Data.Count > 0)
{
playerManager.SendPacketToOtherPlayers(packet, player);
}
}
}

View File

@@ -0,0 +1,22 @@
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
namespace NitroxServer.Communication.Packets.Processors;
public class VehicleOnPilotModeChangedProcessor : AuthenticatedPacketProcessor<VehicleOnPilotModeChanged>
{
private readonly PlayerManager playerManager;
public VehicleOnPilotModeChangedProcessor(PlayerManager playerManager)
{
this.playerManager = playerManager;
}
public override void Process(VehicleOnPilotModeChanged packet, Player player)
{
player.PlayerContext.DrivingVehicle = packet.IsPiloting ? packet.VehicleId : null;
playerManager.SendPacketToOtherPlayers(packet, player);
}
}

View File

@@ -0,0 +1,41 @@
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer.Communication.Packets.Processors;
public class VehicleUndockingProcessor : AuthenticatedPacketProcessor<VehicleUndocking>
{
private readonly PlayerManager playerManager;
private readonly EntityRegistry entityRegistry;
public VehicleUndockingProcessor(PlayerManager playerManager, EntityRegistry entityRegistry)
{
this.playerManager = playerManager;
this.entityRegistry = entityRegistry;
}
public override void Process(VehicleUndocking packet, Player player)
{
if (packet.UndockingStart)
{
if (!entityRegistry.TryGetEntityById(packet.VehicleId, out Entity vehicleEntity))
{
Log.Error($"Unable to find vehicle to undock {packet.VehicleId}");
return;
}
if (!entityRegistry.GetEntityById(vehicleEntity.ParentId).HasValue)
{
Log.Error($"Unable to find docked vehicles parent {vehicleEntity.ParentId} to undock from");
return;
}
entityRegistry.RemoveFromParent(vehicleEntity);
}
playerManager.SendPacketToOtherPlayers(packet, player);
}
}

View File

@@ -0,0 +1,21 @@
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.Packets;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Bases;
namespace NitroxServer.Communication.Packets.Processors;
public class WaterParkDeconstructedProcessor : BuildingProcessor<WaterParkDeconstructed>
{
public WaterParkDeconstructedProcessor(BuildingManager buildingManager, PlayerManager playerManager) : base(buildingManager, playerManager) { }
public override void Process(WaterParkDeconstructed packet, Player player)
{
if (buildingManager.ReplacePieceByGhost(player, packet, out Entity removedEntity, out int operationId) &&
buildingManager.CreateWaterParkPiece(packet, removedEntity))
{
packet.BaseData = null;
SendToOtherPlayersWithOperationId(packet, player, operationId);
}
}
}

View File

@@ -0,0 +1,27 @@
using NitroxModel.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
namespace NitroxServer.Communication.Packets.Processors
{
class WeldActionProcessor : AuthenticatedPacketProcessor<WeldAction>
{
private readonly SimulationOwnershipData simulationOwnershipData;
public WeldActionProcessor(SimulationOwnershipData simulationOwnershipData)
{
this.simulationOwnershipData = simulationOwnershipData;
}
public override void Process(WeldAction packet, Player player)
{
Player simulatingPlayer = simulationOwnershipData.GetPlayerForLock(packet.Id);
if (simulatingPlayer != null)
{
Log.Debug($"Send WeldAction to simulating player {simulatingPlayer.Name} for entity {packet.Id}");
simulatingPlayer.SendPacket(packet);
}
}
}
}

View File

@@ -0,0 +1,64 @@
using System;
using NitroxModel.DataStructures.Util;
namespace NitroxServer.ConsoleCommands.Abstract
{
public abstract partial class Command
{
public ref struct CallArgs
{
public Command Command { get; }
public Optional<Player> Sender { get; }
public Span<string> Args { get; }
public bool IsConsole => !Sender.HasValue;
public string SenderName => Sender.HasValue ? Sender.Value.Name : "SERVER";
public CallArgs(Command command, Optional<Player> sender, Span<string> args)
{
Command = command;
Sender = sender;
Args = args;
}
public bool IsValid(int index)
{
return index < Args.Length && index >= 0 && Args.Length != 0;
}
public string GetTillEnd(int startIndex = 0)
{
// TODO: Proper argument capture/parse instead of this argument join hack
if (Args.Length > 0)
{
return string.Join(" ", Args.Slice(startIndex).ToArray());
}
return string.Empty;
}
public string Get(int index)
{
return Get<string>(index);
}
public T Get<T>(int index)
{
IParameter<object> param = Command.Parameters[index];
string arg = IsValid(index) ? Args[index] : null;
if (arg == null)
{
return default(T);
}
if (typeof(T) == typeof(string))
{
return (T)(object)arg;
}
return (T)param.Read(arg);
}
}
}
}

View File

@@ -0,0 +1,149 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NitroxModel.Core;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.Util;
using NitroxModel.Helper;
using NitroxModel.Packets;
using NitroxServer.GameLogic;
namespace NitroxServer.ConsoleCommands.Abstract
{
public abstract partial class Command
{
private int optional, required;
public virtual IEnumerable<string> Aliases { get; }
public string Name { get; }
public string Description { get; }
public Perms RequiredPermLevel { get; }
public PermsFlag Flags { get; }
public bool AllowedArgOverflow { get; set; }
public List<IParameter<object>> Parameters { get; }
protected Command(string name, Perms perms, PermsFlag flag, string description) : this(name, perms, description)
{
Flags = flag;
}
protected Command(string name, Perms perms, string description)
{
Validate.NotNull(name);
Name = name;
Flags = PermsFlag.NONE;
RequiredPermLevel = perms;
AllowedArgOverflow = false;
Aliases = Array.Empty<string>();
Parameters = new List<IParameter<object>>();
Description = string.IsNullOrEmpty(description) ? "No description provided" : description;
}
protected abstract void Execute(CallArgs args);
public void TryExecute(Optional<Player> sender, Span<string> args)
{
if (args.Length < required)
{
SendMessage(sender, $"Error: Invalid Parameters\nUsage: {ToHelpText(false, true)}");
return;
}
if (!AllowedArgOverflow && args.Length > optional + required)
{
SendMessage(sender, $"Error: Too many Parameters\nUsage: {ToHelpText(false, true)}");
return;
}
try
{
Execute(new CallArgs(this, sender, args));
}
catch (ArgumentException ex)
{
SendMessage(sender, $"Error: {ex.Message}");
}
catch (Exception ex)
{
Log.Error(ex, "Fatal error while trying to execute the command");
}
}
public bool CanExecute(Perms treshold)
{
return RequiredPermLevel <= treshold;
}
public string ToHelpText(bool singleCommand, bool cropText = false)
{
StringBuilder cmd = new(Name);
if (Aliases.Any())
{
cmd.AppendFormat("/{0}", string.Join("/", Aliases));
}
cmd.AppendFormat(" {0}", string.Join(" ", Parameters));
if (singleCommand)
{
string parameterPreText = Parameters.Count == 0 ? "" : Environment.NewLine;
string parameterText = $"{parameterPreText}{string.Join("\n", Parameters.Select(p => $"{p,-47} - {p.GetDescription()}"))}";
return cropText ? $"{cmd}" : $"{cmd,-32} - {Description} {parameterText}";
}
return cropText ? $"{cmd}" : $"{cmd,-32} - {Description}";
}
protected void AddParameter<T>(T param) where T : IParameter<object>
{
Validate.NotNull(param as object);
Parameters.Add(param);
if (param.IsRequired)
{
required++;
}
else
{
optional++;
}
}
/// <summary>
/// Send a message to an existing player
/// </summary>
public static void SendMessageToPlayer(Optional<Player> player, string message)
{
if (player.HasValue)
{
player.Value.SendPacket(new ChatMessage(ChatMessage.SERVER_ID, message));
}
}
/// <summary>
/// Send a message to an existing player and logs it in the console
/// </summary>
public static void SendMessage(Optional<Player> player, string message)
{
SendMessageToPlayer(player, message);
if (!player.HasValue)
{
Log.Info(message);
}
}
/// <summary>
/// Send a message to all connected players
/// </summary>
public static void SendMessageToAllPlayers(string message)
{
PlayerManager playerManager = NitroxServiceLocator.LocateService<PlayerManager>();
playerManager.SendPacketToAllPlayers(new ChatMessage(ChatMessage.SERVER_ID, message));
Log.Info($"[BROADCAST] {message}");
}
}
}

View File

@@ -0,0 +1,43 @@
using NitroxModel.Helper;
namespace NitroxServer.ConsoleCommands.Abstract
{
public abstract class Parameter<T> : IParameter<T>
{
public bool IsRequired { get; }
public string Name { get; }
private string Description { get; }
protected Parameter(string name, bool isRequired, string description)
{
Validate.IsFalse(string.IsNullOrEmpty(name));
Name = name;
IsRequired = isRequired;
Description = description;
}
public abstract bool IsValid(string arg);
public abstract T Read(string arg);
public virtual string GetDescription()
{
return Description;
}
public override string ToString()
{
return $"{(IsRequired ? '{' : '[')}{Name}{(IsRequired ? '}' : ']')}";
}
}
public interface IParameter<out T>
{
bool IsRequired { get; }
string Name { get; }
bool IsValid(string arg);
T Read(string arg);
string GetDescription();
}
}

View File

@@ -0,0 +1,41 @@
using System;
using System.Linq;
using NitroxModel.Helper;
namespace NitroxServer.ConsoleCommands.Abstract.Type
{
public class TypeBoolean : Parameter<bool>, IParameter<object>
{
private static readonly string[] noValues = new string[]
{
bool.FalseString,
"no",
"off"
};
private static readonly string[] yesValues = new string[]
{
bool.TrueString,
"yes",
"on"
};
public TypeBoolean(string name, bool isRequired, string description) : base(name, isRequired, description) { }
public override bool IsValid(string arg)
{
return yesValues.Contains(arg, StringComparer.OrdinalIgnoreCase) || noValues.Contains(arg, StringComparer.OrdinalIgnoreCase);
}
public override bool Read(string arg)
{
Validate.IsTrue(IsValid(arg), "Invalid boolean value received");
return yesValues.Contains(arg, StringComparer.OrdinalIgnoreCase);
}
object IParameter<object>.Read(string arg)
{
return Read(arg);
}
}
}

View File

@@ -0,0 +1,29 @@
using System;
using NitroxModel.Helper;
namespace NitroxServer.ConsoleCommands.Abstract.Type
{
public class TypeEnum<T> : Parameter<object> where T : struct, Enum
{
public TypeEnum(string name, bool required, string description) : base(name, required, description)
{
Validate.IsTrue(typeof(T).IsEnum, $"Type {typeof(T).FullName} isn't an enum");
}
public override bool IsValid(string arg)
{
return Enum.TryParse<T>(arg, true, out _);
}
public override object Read(string arg)
{
Validate.IsTrue(Enum.TryParse(arg, true, out T value), $"Unknown value received (pick from: {string.Join(", ", Enum.GetNames(typeof(T)))})");
return value;
}
public override string GetDescription()
{
return $"{base.GetDescription()} (values: {string.Join(", ", Enum.GetNames(typeof(T)))})";
}
}
}

View File

@@ -0,0 +1,25 @@
using NitroxModel.Helper;
namespace NitroxServer.ConsoleCommands.Abstract.Type
{
public class TypeFloat : Parameter<float>, IParameter<object>
{
public TypeFloat(string name, bool isRequired, string description) : base(name, isRequired, description) { }
public override bool IsValid(string arg)
{
return float.TryParse(arg, out _);
}
public override float Read(string arg)
{
Validate.IsTrue(float.TryParse(arg, out float value), "Invalid decimal number received");
return value;
}
object IParameter<object>.Read(string arg)
{
return Read(arg);
}
}
}

View File

@@ -0,0 +1,25 @@
using NitroxModel.Helper;
namespace NitroxServer.ConsoleCommands.Abstract.Type
{
public class TypeInt : Parameter<int>, IParameter<object>
{
public TypeInt(string name, bool isRequired, string description) : base(name, isRequired, description) { }
public override bool IsValid(string arg)
{
return int.TryParse(arg, out _);
}
public override int Read(string arg)
{
Validate.IsTrue(int.TryParse(arg, out int value), "Invalid integer received");
return value;
}
object IParameter<object>.Read(string arg)
{
return Read(arg);
}
}
}

View File

@@ -0,0 +1,24 @@
using System;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
namespace NitroxServer.ConsoleCommands.Abstract.Type;
public class TypeNitroxId(string name, bool isRequired, string description) : Parameter<NitroxId>(name, isRequired, description)
{
public override bool IsValid(string arg)
{
return IsValid(arg, out _);
}
private static bool IsValid(string arg, out Guid result)
{
return Guid.TryParse(arg, out result);
}
public override NitroxId Read(string arg)
{
Validate.IsTrue(IsValid(arg, out Guid result), "Received an invalid NitroxId");
return new NitroxId(result);
}
}

View File

@@ -0,0 +1,27 @@
using NitroxModel.Core;
using NitroxModel.Helper;
using NitroxServer.GameLogic;
namespace NitroxServer.ConsoleCommands.Abstract.Type
{
public class TypePlayer : Parameter<Player>
{
private static readonly PlayerManager playerManager = NitroxServiceLocator.LocateService<PlayerManager>();
public TypePlayer(string name, bool required, string description) : base(name, required, description)
{
Validate.NotNull(playerManager, "PlayerManager can't be null to resolve the command");
}
public override bool IsValid(string arg)
{
return playerManager.TryGetPlayerByName(arg, out _);
}
public override Player Read(string arg)
{
Validate.IsTrue(playerManager.TryGetPlayerByName(arg, out Player player), "Player not found");
return player;
}
}
}

View File

@@ -0,0 +1,20 @@
using NitroxModel.Helper;
namespace NitroxServer.ConsoleCommands.Abstract.Type
{
public class TypeString : Parameter<string>
{
public TypeString(string name, bool isRequired, string description) : base(name, isRequired, description) { }
public override bool IsValid(string arg)
{
return !string.IsNullOrEmpty(arg);
}
public override string Read(string arg)
{
Validate.IsTrue(IsValid(arg), "Received null/empty instead of a valid string");
return arg;
}
}
}

View File

@@ -0,0 +1,42 @@
using NitroxModel.DataStructures.GameLogic;
using NitroxServer.ConsoleCommands.Abstract;
using NitroxServer.ConsoleCommands.Abstract.Type;
using NitroxServer.GameLogic;
namespace NitroxServer.ConsoleCommands;
// TODO: When we make the new command system, move this stuff to it
public class AuroraCommand : Command
{
private readonly StoryManager storyManager;
// We shouldn't let the server use this command because it needs some stuff to happen client-side like goals
public AuroraCommand(StoryManager storyManager) : base("aurora", Perms.ADMIN, PermsFlag.NO_CONSOLE, "Manage Aurora's state")
{
AddParameter(new TypeString("countdown/restore/explode", true, "Which action to apply to Aurora"));
this.storyManager = storyManager;
}
protected override void Execute(CallArgs args)
{
string action = args.Get<string>(0);
switch (action.ToLower())
{
case "countdown":
storyManager.BroadcastExplodeAurora(true);
break;
case "restore":
storyManager.BroadcastRestoreAurora();
break;
case "explode":
storyManager.BroadcastExplodeAurora(false);
break;
default:
// Same message as in the abstract class, in method TryExecute
SendMessage(args.Sender, $"Error: Invalid Parameters\nUsage: {ToHelpText(false, true)}");
break;
}
}
}

View File

@@ -0,0 +1,43 @@
using System.IO;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.Serialization;
using NitroxServer.ConsoleCommands.Abstract;
using NitroxServer.ConsoleCommands.Abstract.Type;
namespace NitroxServer.ConsoleCommands
{
internal class AutoSaveCommand : Command
{
private readonly Server server;
private readonly SubnauticaServerConfig serverConfig;
public AutoSaveCommand(Server server, SubnauticaServerConfig serverConfig) : base("autosave", Perms.ADMIN, "Toggles the map autosave")
{
AddParameter(new TypeBoolean("on/off", true, "Whether autosave should be on or off"));
this.server = server;
this.serverConfig = serverConfig;
}
protected override void Execute(CallArgs args)
{
bool toggle = args.Get<bool>(0);
using (serverConfig.Update(Path.Combine(KeyValueStore.Instance.GetSavesFolderDir(), server.Name)))
{
if (toggle)
{
serverConfig.DisableAutoSave = false;
Server.Instance.EnablePeriodicSaving();
SendMessage(args.Sender, "Enabled periodical saving");
}
else
{
serverConfig.DisableAutoSave = true;
Server.Instance.DisablePeriodicSaving();
SendMessage(args.Sender, "Disabled periodical saving");
}
}
}
}
}

View File

@@ -0,0 +1,26 @@
using NitroxModel.DataStructures.GameLogic;
using NitroxServer.ConsoleCommands.Abstract;
namespace NitroxServer.ConsoleCommands
{
internal class BackCommand : Command
{
public BackCommand() : base("back", Perms.MODERATOR, PermsFlag.NO_CONSOLE, "Teleports you back on your last location")
{
}
protected override void Execute(CallArgs args)
{
Player player = args.Sender.Value;
if (player.LastStoredPosition == null)
{
SendMessage(args.Sender, "No previous location...");
return;
}
player.Teleport(player.LastStoredPosition.Value, player.LastStoredSubRootID);
SendMessage(args.Sender, $"Teleported back to {player.LastStoredPosition.Value}");
}
}
}

View File

@@ -0,0 +1,18 @@
using NitroxModel.DataStructures.GameLogic;
using NitroxServer.ConsoleCommands.Abstract;
namespace NitroxServer.ConsoleCommands
{
internal class BackupCommand : Command
{
public BackupCommand() : base("backup", Perms.ADMIN, "Creates a backup of the save")
{
}
protected override void Execute(CallArgs args)
{
Server.Instance.BackUp();
SendMessageToPlayer(args.Sender, "World has been backed up");
}
}
}

View File

@@ -0,0 +1,24 @@
using System.Collections.Generic;
using NitroxModel.DataStructures.GameLogic;
using NitroxServer.ConsoleCommands.Abstract;
using NitroxServer.ConsoleCommands.Abstract.Type;
namespace NitroxServer.ConsoleCommands
{
internal class BroadcastCommand : Command
{
public override IEnumerable<string> Aliases { get; } = new[] { "say" };
public BroadcastCommand() : base("broadcast", Perms.MODERATOR, "Broadcasts a message on the server")
{
AddParameter(new TypeString("message", true, "The message to be broadcast"));
AllowedArgOverflow = true;
}
protected override void Execute(CallArgs args)
{
SendMessageToAllPlayers(args.GetTillEnd());
}
}
}

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