first commit

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

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
</startup>
<runtime>
<!-- This is required so that assembly loading won't throw error for unsigned assemblies. -->
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<probing privatePath="lib" />
<dependentAssembly>
<assemblyIdentity name="Mono.Cecil" publicKeyToken="50cebf1cceb9d05e" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-0.10.4.0" newVersion="0.10.4.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

View File

@@ -0,0 +1,56 @@
using System;
using System.Threading;
namespace NitroxServer_Subnautica;
public static class AppMutex
{
private static readonly SemaphoreSlim mutexReleaseGate = new(1);
private static readonly SemaphoreSlim callerGate = new(1);
public static void Hold(Action onWaitingForMutex = null, CancellationToken ct = default)
{
Thread thread = new(o =>
{
bool first = true;
Mutex mutex = new(false, typeof(AppMutex).Assembly.FullName, out bool _);
try
{
try
{
while (!mutex.WaitOne(100, false))
{
ct.ThrowIfCancellationRequested();
if (first)
{
first = false;
onWaitingForMutex?.Invoke();
}
}
}
catch (AbandonedMutexException)
{
// Mutex was abandoned in another process, it will still get acquired
}
}
finally
{
callerGate.Release();
mutexReleaseGate.Wait(-1);
mutex.ReleaseMutex();
}
});
mutexReleaseGate.Wait(-1, ct);
callerGate.Wait(0, ct);
thread.Start();
while (!callerGate.Wait(100, ct))
{
}
}
public static void Release()
{
mutexReleaseGate.Release();
}
}

View File

@@ -0,0 +1,102 @@
using System;
using System.IO;
using System.IO.Pipes;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using NitroxModel.Helper;
namespace NitroxServer_Subnautica.Communication;
/// <summary>
/// Exposes an IPC channel for other local processes to communicate with the server.
/// </summary>
public class IpcHost : IDisposable
{
private readonly CancellationTokenSource commandReadCancellation;
private readonly NamedPipeServerStream server = new($"Nitrox Server {NitroxEnvironment.CurrentProcessId}", PipeDirection.In, 1);
private IpcHost(CancellationTokenSource commandReadCancellation)
{
this.commandReadCancellation = commandReadCancellation;
}
public static IpcHost StartReadingCommands(Action<string> onCommandReceived, CancellationToken cancellationToken = default)
{
Log.Info("Starting IPC host for command input");
ArgumentNullException.ThrowIfNull(onCommandReceived);
IpcHost host = new(CancellationTokenSource.CreateLinkedTokenSource(cancellationToken));
Thread thread = new(async () =>
{
while (!cancellationToken.IsCancellationRequested)
{
try
{
string command = await host.ReadStringAsync(cancellationToken);
onCommandReceived(command);
}
catch (OperationCanceledException)
{
// ignored
}
}
});
thread.IsBackground = true;
thread.Start();
return host;
}
public async Task<string> ReadStringAsync(CancellationToken cancellationToken = default)
{
if (!await WaitForConnection())
{
return "";
}
try
{
byte[] sizeBytes = new byte[4];
await server.ReadExactlyAsync(sizeBytes, cancellationToken);
byte[] stringBytes = new byte[BitConverter.ToUInt32(sizeBytes)];
await server.ReadExactlyAsync(stringBytes, cancellationToken);
return Encoding.UTF8.GetString(stringBytes);
}
catch (Exception)
{
return "";
}
}
public void Dispose()
{
commandReadCancellation?.Cancel();
server.Dispose();
}
private async Task<bool> WaitForConnection()
{
if (server.IsConnected)
{
return true;
}
try
{
await server.WaitForConnectionAsync();
return true;
}
catch (IOException)
{
try
{
server.Disconnect();
}
catch (Exception)
{
// ignored
}
}
return false;
}
}

View File

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

View File

@@ -0,0 +1,26 @@
using NitroxModel_Subnautica.Packets;
using NitroxServer.Communication.Packets.Processors.Abstract;
using NitroxServer.GameLogic;
namespace NitroxServer_Subnautica.Communication.Packets.Processors
{
/// <summary>
/// This is the absolute damage state. The current simulation owner is the only one who sends this packet to the server
/// </summary>
public class CyclopsDamageProcessor : AuthenticatedPacketProcessor<CyclopsDamage>
{
private readonly PlayerManager playerManager;
public CyclopsDamageProcessor(PlayerManager playerManager)
{
this.playerManager = playerManager;
}
public override void Process(CyclopsDamage packet, NitroxServer.Player simulatingPlayer)
{
Log.Debug($"New cyclops damage from {simulatingPlayer.Id} {packet}");
playerManager.SendPacketToOtherPlayers(packet, simulatingPlayer);
}
}
}

View File

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

View File

@@ -0,0 +1,81 @@
using System.Collections.Generic;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel_Subnautica.DataStructures;
using NitroxServer.GameLogic.Entities;
namespace NitroxServer_Subnautica.GameLogic.Entities;
public class SimulationWhitelist : ISimulationWhitelist
{
/// <inheritdoc cref="ISimulationWhitelist.MovementWhitelist" />
public static readonly HashSet<NitroxTechType> MovementWhitelist = new()
{
TechType.Shocker.ToDto(),
TechType.Biter.ToDto(),
TechType.Blighter.ToDto(),
TechType.BoneShark.ToDto(),
TechType.Crabsnake.ToDto(),
TechType.CrabSquid.ToDto(),
TechType.Crash.ToDto(),
TechType.GhostLeviathan.ToDto(),
TechType.GhostLeviathanJuvenile.ToDto(),
TechType.GhostRayBlue.ToDto(),
TechType.GhostRayRed.ToDto(),
TechType.Mesmer.ToDto(),
TechType.LavaLizard.ToDto(),
TechType.LavaEyeye.ToDto(),
TechType.LavaBoomerang.ToDto(),
TechType.LargeFloater.ToDto(),
TechType.LargeKoosh.ToDto(),
TechType.SpineEel.ToDto(),
TechType.Spinefish.ToDto(),
TechType.Sandshark.ToDto(),
TechType.SeaDragon.ToDto(),
TechType.SeaEmperor.ToDto(),
TechType.SeaEmperorBaby.ToDto(),
TechType.SeaEmperorJuvenile.ToDto(),
TechType.SeaEmperorLeviathan.ToDto(),
TechType.ReaperLeviathan.ToDto(),
TechType.Stalker.ToDto(),
TechType.Warper.ToDto(),
TechType.Bladderfish.ToDto(),
TechType.Boomerang.ToDto(),
TechType.Cutefish.ToDto(),
TechType.Eyeye.ToDto(),
TechType.Jellyray.ToDto(),
TechType.GarryFish.ToDto(),
TechType.Gasopod.ToDto(),
TechType.HoleFish.ToDto(),
TechType.Hoopfish.ToDto(),
TechType.Hoverfish.ToDto(),
TechType.Oculus.ToDto(),
TechType.RabbitRay.ToDto(),
TechType.Reefback.ToDto(),
TechType.Reginald.ToDto(),
TechType.SeaTreader.ToDto(),
TechType.Skyray.ToDto(),
TechType.Spadefish.ToDto(),
TechType.Spinefish.ToDto(),
TechType.BlueAmoeba.ToDto(),
TechType.Shuttlebug.ToDto(),
TechType.CaveCrawler.ToDto(),
TechType.Floater.ToDto(),
TechType.LavaLarva.ToDto(),
TechType.Rockgrub.ToDto(),
TechType.Shuttlebug.ToDto(),
TechType.Bloom.ToDto(),
TechType.RockPuncher.ToDto(),
TechType.Peeper.ToDto(),
TechType.Jumper.ToDto(),
TechType.Constructor.ToDto()
};
/// <inheritdoc cref="ISimulationWhitelist.UtilityWhitelist" />
public static readonly HashSet<NitroxTechType> UtilityWhitelist = new()
{
TechType.CrashHome.ToDto()
};
HashSet<NitroxTechType> ISimulationWhitelist.MovementWhitelist => MovementWhitelist;
HashSet<NitroxTechType> ISimulationWhitelist.UtilityWhitelist => UtilityWhitelist;
}

View File

@@ -0,0 +1,15 @@
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxServer.GameLogic.Entities.Spawning;
using NitroxServer.Helper;
namespace NitroxServer_Subnautica.GameLogic.Entities.Spawning.EntityBootstrappers;
public class CrashHomeBootstrapper : IEntityBootstrapper
{
public void Prepare(ref WorldEntity entity, DeterministicGenerator deterministicBatchGenerator)
{
// Set 0 for spawnTime so that CrashHome.Update can spawn a Crash if Start() couldn't
entity.Metadata = new CrashHomeMetadata(0);
}
}

View File

@@ -0,0 +1,17 @@
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxServer.GameLogic.Entities.Spawning;
using NitroxServer.Helper;
namespace NitroxServer_Subnautica.GameLogic.Entities.Spawning;
public class GeyserBootstrapper : IEntityBootstrapper
{
public void Prepare(ref WorldEntity entity, DeterministicGenerator deterministicBatchGenerator)
{
entity = new GeyserWorldEntity(entity.Transform, entity.Level, entity.ClassId,
entity.SpawnedByServer, entity.Id, entity.TechType,
entity.Metadata, entity.ParentId, entity.ChildEntities,
XORRandom.NextFloat(), 15 * XORRandom.NextFloat());
// The value 15 doesn't mean anything in particular, it's just an initial eruption time window so geysers don't all erupt at the same time at first
}
}

View File

@@ -0,0 +1,125 @@
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.Unity;
using NitroxServer.GameLogic.Entities.Spawning;
using NitroxServer.Helper;
using static NitroxServer_Subnautica.GameLogic.Entities.Spawning.ReefbackSpawnData;
namespace NitroxServer_Subnautica.GameLogic.Entities.Spawning;
public class ReefbackBootstrapper : IEntityBootstrapper
{
private readonly float creatureProbabilitySum = 0;
private readonly float plantsProbabilitySum = 0;
public ReefbackBootstrapper()
{
foreach (ReefbackSlotCreature creature in SpawnableCreatures)
{
creatureProbabilitySum += creature.Probability;
}
foreach (ReefbackSlotPlant plant in SpawnablePlants)
{
plantsProbabilitySum += plant.Probability;
}
}
public void Prepare(ref WorldEntity entity, DeterministicGenerator generator)
{
// From ReefbackLife.Initialize
if (entity.Transform.LocalScale.X <= 0.8f)
{
return;
}
// In case the grassIndex is chosen randomly
int grassIndex = XORRandom.NextIntRange(1, GRASS_VARIANTS_COUNT);
entity = new ReefbackEntity(entity.Transform, entity.Level, entity.ClassId,
entity.SpawnedByServer, entity.Id, entity.TechType,
entity.Metadata, entity.ParentId, entity.ChildEntities,
grassIndex, entity.Transform.Position);
NitroxTransform plantSlotsRootTransform = DuplicateTransform(PlantSlotsRootTransform);
plantSlotsRootTransform.SetParent(entity.Transform, false);
// ReefbackLife.SpawnPlants equivalent
for (int i = 0; i < PLANT_SLOTS_COUNT; i++)
{
NitroxTransform slotTransform = DuplicateTransform(PlantSlotsCoordinates[i]);
slotTransform.SetParent(plantSlotsRootTransform, false);
float random = XORRandom.NextFloat() * plantsProbabilitySum;
float totalProbability = 0f;
int chosenPlantIndex = 0;
for (int k = 0; k < SpawnablePlants.Count; k++)
{
totalProbability += SpawnablePlants[k].Probability;
if (random <= totalProbability)
{
chosenPlantIndex = k;
break;
}
}
ReefbackSlotPlant slotPlant = SpawnablePlants[chosenPlantIndex];
string randomId = slotPlant.ClassIds[XORRandom.NextIntRange(0, slotPlant.ClassIds.Count)];
NitroxId id = generator.NextId();
NitroxTransform plantTransform = new(slotTransform.Position, slotPlant.StartRotationQuaternion, NitroxVector3.One);
plantTransform.SetParent(plantSlotsRootTransform);
// It is necessary to set parent to null afterwards so that the entity doesn't accidentally modifies the transform by losing reference to the parent
plantTransform.SetParent(null, false);
ReefbackChildEntity plantEntity = new(plantTransform, entity.Level, randomId, true, id, NitroxTechType.None, null, entity.Id, [],
ReefbackChildEntity.ReefbackChildType.PLANT);
entity.ChildEntities.Add(plantEntity);
}
NitroxTransform creatureSlotsRootTransform = DuplicateTransform(CreatureSlotsRootTransform);
creatureSlotsRootTransform.SetParent(entity.Transform, false);
// ReefbackLife.SpawnCreatures equivalent
for (int i = 0; i < CREATURE_SLOTS_COUNT; i++)
{
NitroxTransform slotTransform = DuplicateTransform(CreatureSlotsCoordinates[i]);
slotTransform.SetParent(creatureSlotsRootTransform, false);
float random = XORRandom.NextFloat() * creatureProbabilitySum;
float totalProbability = 0f;
int chosenCreatureIndex = 0;
for (int k = 0; k < SpawnableCreatures.Count; k++)
{
totalProbability += SpawnableCreatures[k].Probability;
if (random <= totalProbability)
{
chosenCreatureIndex = k;
break;
}
}
ReefbackSlotCreature slotCreature = SpawnableCreatures[chosenCreatureIndex];
int spawnCount = XORRandom.NextIntRange(slotCreature.MinNumber, slotCreature.MaxNumber + 1);
for (int j = 0; j < spawnCount; j++)
{
NitroxId id = generator.NextId();
NitroxTransform creatureTransform = new(slotTransform.LocalPosition + XORRandom.NextInsideSphere(5f), slotTransform.LocalRotation, NitroxVector3.One);
creatureTransform.SetParent(CreatureSlotsRootTransform, false);
creatureTransform.SetParent(null, false);
ReefbackChildEntity creatureEntity = new(creatureTransform, entity.Level, slotCreature.ClassId, true, id, NitroxTechType.None, null, entity.Id, [],
ReefbackChildEntity.ReefbackChildType.CREATURE);
entity.ChildEntities.Add(creatureEntity);
}
}
}
private static NitroxTransform DuplicateTransform(NitroxTransform transform)
{
return new(transform.LocalPosition, transform.LocalRotation, transform.LocalScale);
}
}

View File

@@ -0,0 +1,133 @@
using System.Collections.Generic;
using NitroxModel.DataStructures.Unity;
namespace NitroxServer_Subnautica.GameLogic.Entities.Spawning;
/// <summary>
/// Data from <see cref="ReefbackSlotsData"/> generated by ReefbackLife_OnEnable_Patch.cs
/// </summary>
public static class ReefbackSpawnData
{
public const int PLANT_SLOTS_COUNT = 28;
public const int CREATURE_SLOTS_COUNT = 10;
public const int GRASS_VARIANTS_COUNT = 3;
public static readonly NitroxTransform PlantSlotsRootTransform = new(new(0f, 0.49f, -0.22f), new(0.5039341f, 0.4992565f, 0.4960347f, -0.5007424f), new(1f, 1f, 1f));
public static readonly NitroxTransform CreatureSlotsRootTransform = new(new(0f, 0f, 0f), new(0f, 0f, 0f, 1f), new(1f, 1f, 1f));
public static List<ReefbackSlotCreature> SpawnableCreatures { get; } =
[
new() { Probability = 1f, MinNumber = 1, MaxNumber = 2, ClassId = "3fcd548b-781f-46ba-b076-7412608deeef" },
new() { Probability = 1f, MinNumber = 1, MaxNumber = 2, ClassId = "fa4cfe65-4eaf-4d51-ba0d-e8cc9632fd47" },
new() { Probability = 1f, MinNumber = 1, MaxNumber = 2, ClassId = "0a993944-87d3-441e-b21d-6c314f723cc7" },
new() { Probability = 1f, MinNumber = 1, MaxNumber = 2, ClassId = "bf9ccd04-60af-4144-aaa1-4ac184c686c2" },
new() { Probability = 1f, MinNumber = 1, MaxNumber = 1, ClassId = "79c1aef0-e505-469c-ab36-c22c76aeae44" },
new() { Probability = 1f, MinNumber = 1, MaxNumber = 1, ClassId = "495befa0-0e6b-400d-9734-227e5a732f75" },
new() { Probability = 1f, MinNumber = 1, MaxNumber = 1, ClassId = "284ceeb6-b437-4aca-a8bd-d54f336cbef8" },
new() { Probability = 1f, MinNumber = 1, MaxNumber = 2, ClassId = "cf171ce2-e3d2-4cec-9757-60dbd480e486" },
new() { Probability = 1f, MinNumber = 1, MaxNumber = 2, ClassId = "d040bec1-0368-4f7c-aed6-93b5e1852d45" },
new() { Probability = 0.5f, MinNumber = 1, MaxNumber = 3, ClassId = "4064a71a-c464-4db2-942a-56391fe69951" },
new() { Probability = 1f, MinNumber = 1, MaxNumber = 1, ClassId = "ce23b9ee-fd98-4677-9919-20248356f7cf" },
new() { Probability = 1f, MinNumber = 1, MaxNumber = 1, ClassId = "8ffbb5b5-21b4-4687-9118-730d59330c9a" },
new() { Probability = 1f, MinNumber = 1, MaxNumber = 1, ClassId = "a7b70c23-8e57-43e0-ab39-e02a29341376" },
new() { Probability = 1f, MinNumber = 1, MaxNumber = 1, ClassId = "08cb3290-504b-4191-97ee-6af1588af5c0" },
];
public static List<ReefbackSlotPlant> SpawnablePlants { get; } =
[
new() { ClassIds = ["061af756-643c-42ad-9645-a522f1338084", "93a9886d-f2d3-4b6c-8e5f-216f569f82b2"], Probability = 1f, StartRotation = new(270f, 0f, 0f) },
new() { ClassIds = ["fc7c1098-13af-417a-8038-0053b65498e5", "61a5e0e6-01d5-4ae2-aea6-1186cd769025", "31834aae-35ce-49c1-b5ba-ac4227750679", "99cdec62-302b-4999-ba49-f50c73575a4d"], Probability = 2f, StartRotation = new(270f, 0f, 0f) },
new() { ClassIds = ["e80b22ff-064d-46ca-b71e-456d6b3426ab"], Probability = 1f, StartRotation = new(0f, 0f, 0f) },
new() { ClassIds = ["6d9e37de-f808-4621-a762-e0d6340b30dc"], Probability = 1f, StartRotation = new(270f, 0f, 0f) },
new() { ClassIds = ["242b7f63-7553-456b-8d16-b318040097ae", "1fcbf0f8-01fd-4454-a48d-3b3266e5b84e", "11bd0c8e-6d57-46cb-928a-f0e825726674", "7c55e785-a250-41ae-869d-c4be026f9ce6", "43a597df-da05-4f5f-92df-29d76c0b2f53", "133ae1eb-99ec-4b1d-b32b-9c9daf144b8f"], Probability = 1f, StartRotation = new(0f, 0f, 0f) },
new() { ClassIds = ["fb941ab6-9c74-4673-b6a5-2dcb40720d34"], Probability = 1f, StartRotation = new(270f, 0f, 0f) },
new() { ClassIds = ["7f656699-358a-416d-9ecd-f911e3d51bf1", "54dad6b2-77c8-4f9a-9294-2621ca296754"], Probability = 1f, StartRotation = new(225f, 0f, 0f) },
new() { ClassIds = ["e8047056-e202-49b3-829f-7458615103ac", "3dbab1b9-cc52-4da4-8633-89b33add18f4"], Probability = 1f, StartRotation = new(270f, 0f, 0f) },
new() { ClassIds = ["e0608e57-e9df-4f43-bb3a-8c56a42d2c1f", "1edd7411-8f1d-4e7a-8378-0ce7ccb6ea82", "48d6184a-320e-41d2-abca-5b96a94e72e0", "3d4d3892-e43a-45b1-85b8-4a6462257c79"], Probability = 0.3f, StartRotation = new(270f, 0f, 0f) },
new() { ClassIds = ["34b59c1d-876e-4962-a8f7-e205d189d2be", "1f384257-9d4a-4307-829f-024c0e1ce1c0", "0719b0fa-95df-4b37-a581-4f1e07424c62", "28fb4ab7-e1eb-4de3-89a9-98f54394e0f6"], Probability = 0.3f, StartRotation = new(270f, 0f, 0f) },
new() { ClassIds = ["2d970c98-6f77-4270-8be2-91dc863d15d5", "eb6634e5-3a58-4a0d-ae4e-b673e1fa51ea", "df03263c-ebfb-4e7c-b002-1ec3d67c1215", "c197a6ca-f910-43db-92ab-2e35e423a6f1"], Probability = 0.3f, StartRotation = new(270f, 0f, 0f) },
new() { ClassIds = ["70eb6270-bf5e-4d6a-8182-484ffcfd8de6", "f0713f3d-586b-4c71-88a3-18dd6c3dd2a4", "9a643563-9278-4c77-8bd2-f9b4b1a1053a", "4e31161e-c812-4c8c-bfd4-00cf4b743884"], Probability = 0.3f, StartRotation = new(270f, 0f, 0f) },
new() { ClassIds = ["171c6a5b-879b-4785-be7a-6584b2c8c442"], Probability = 1f, StartRotation = new(270f, 0f, 0f) },
new() { ClassIds = ["84794dd0-2c70-4239-9536-230d56811ad4"], Probability = 1f, StartRotation = new(0f, 0f, 0f) },
new() { ClassIds = ["aa1abbb9-716c-44b8-a2b8-cb4d9d0f22bb", "7ecc9cdd-3afc-4005-bff7-01ba62e95a03", "26940e53-d3eb-4770-ae99-6ce4335445d3", "c87e584c-7e38-4589-b408-8eca51f474c1", "a71da66c-6d43-45c1-bc7f-a789cfc61e46"], Probability = 2f, StartRotation = new(0f, 0f, 0f) },
new() { ClassIds = ["22bf7b03-8154-410b-a6fb-8ba315f68987", "450bf7b5-b6cf-4139-921f-3cb9ea505d5f", "c71f41ce-b586-4e85-896e-d25e8b5b9de0", "598c95d8-7420-4907-8f70-ba18b4e6adcb"], Probability = 1.5f, StartRotation = new(0f, 0f, 0f) },
new() { ClassIds = ["36fcb5c8-07f6-4d20-b026-f8c41b8e2358"], Probability = 1f, StartRotation = new(0f, 0f, 0f) },
new() { ClassIds = ["4525e0f3-9c9a-449f-8d6c-48088711ac99"], Probability = 1f, StartRotation = new(0f, 0f, 0f) },
new() { ClassIds = ["b707aa52-1a27-43c4-9500-f346befb8251"], Probability = 1f, StartRotation = new(0f, 0f, 0f) },
new() { ClassIds = ["1a806d20-dc8f-4e6e-9281-f353ed155abf"], Probability = 1f, StartRotation = new(0f, 0f, 0f) },
new() { ClassIds = ["4601400c-5e12-4e4a-9e45-4cab5f06a598"], Probability = 1f, StartRotation = new(0f, 0f, 0f) },
new() { ClassIds = ["31ccc496-c26b-4ed9-8e86-3334582d8d5b", "4bc33bd6-cfa1-46a7-bac8-074ba3b76044"], Probability = 3f, StartRotation = new(0f, 0f, 0f) },
];
public static List<NitroxTransform> CreatureSlotsCoordinates { get; } =
[
new(new(-22.9f, 17f, 0f), new(0f, 0f, 0f, 1f), new(1f, 1f, 1f)),
new(new(5.1f, 17.9f, 22.12f), new(0f, 0f, 0f, 1f), new(1f, 1f, 1f)),
new(new(5.1f, 17.9f, -11.6f), new(0f, 0f, 0f, 1f), new(1f, 1f, 1f)),
new(new(-5.1f, 17.9f, 5.57f), new(0f, 0f, 0f, 1f), new(1f, 1f, 1f)),
new(new(23.62f, 17.9f, 5.57f), new(0f, 0f, 0f, 1f), new(1f, 1f, 1f)),
new(new(-16.4f, 17.9f, 25.9f), new(0f, 0f, 0f, 1f), new(1f, 1f, 1f)),
new(new(-8.7f, 17.9f, -30.3f), new(0f, 0f, 0f, 1f), new(1f, 1f, 1f)),
new(new(15.4f, 17.9f, -30.3f), new(0f, 0f, 0f, 1f), new(1f, 1f, 1f)),
new(new(20.9f, 17.9f, -13.9f), new(0f, 0f, 0f, 1f), new(1f, 1f, 1f)),
new(new(-17.3f, 17.9f, 22.12f), new(0f, 0f, 0f, 1f), new(1f, 1f, 1f)),
];
public static List<NitroxTransform> PlantSlotsCoordinates { get; } =
[
new(new(-4.460001f, 0f, 10f), new(0.6152681f, -0.3477891f, -0.2830537f, 0.6483584f), new(1f, 1f, 1f)),
new(new(-3.481001f, 6.719f, 8.196002f), new(0.4696728f, -0.1773425f, -0.3922334f, 0.7707854f), new(1f, 1f, 1f)),
new(new(7.452f, 2.863f, 8.1f), new(0.5340302f, 0.3158718f, 0.2073223f, 0.7563427f), new(1f, 1f, 1f)),
new(new(5.12f, -7.519994f, 7.599998f), new(0.7192559f, 0.07982004f, -0.09835583f, 0.6831002f), new(1f, 1f, 1f)),
new(new(15.631f, 2.149f, 7.193f), new(0.4893234f, 0.05595651f, -0.1356857f, 0.8596632f), new(1f, 1f, 1f)),
new(new(7.3761f, 6.8008f, 6.9998f), new(-0.5506698f, -0.5092787f, -0.486309f, -0.4482206f), new(1f, 1f, 1f)),
new(new(20.231f, 6.098f, 3.22f), new(-0.580992f, -0.5104024f, 0.01308015f, -0.6338507f), new(1f, 1f, 1f)),
new(new(17.307f, -2.754f, 6.889f), new(-0.6737934f, -0.3733953f, -0.5835185f, -0.2570692f), new(1f, 1f, 1f)),
new(new(10.411f, -8.146f, 6.401f), new(-0.4313123f, -0.6596806f, -0.2941538f, -0.5406151f), new(1f, 1f, 1f)),
new(new(10.903f, -12.232f, 6.880001f), new(-0.7411356f, -0.3174673f, -0.1293507f, -0.5772356f), new(1f, 1f, 1f)),
new(new(-11.188f, 5.817f, 7.423f), new(-0.6473981f, 0.005354047f, -0.5044534f, -0.5712914f), new(1f, 1f, 1f)),
new(new(-7.186f, -5.598001f, 9.095995f), new(-0.5481309f, -0.3323998f, -0.6145787f, -0.4597346f), new(1f, 1f, 1f)),
new(new(18.337f, -9.234001f, 3.704004f), new(-0.54471f, -0.6272081f, -0.5153005f, -0.2106334f), new(1f, 1f, 1f)),
new(new(3.708f, 14.976f, 7.343f), new(-0.5388643f, -0.5702056f, -0.4023229f, -0.4718338f), new(1f, 1f, 1f)),
new(new(4.624f, -2.568f, 8.295f), new(-0.4445406f, -0.4816067f, -0.7076716f, -0.2638929f), new(1f, 1f, 1f)),
new(new(2.332f, -12.735f, 8.085f), new(-0.5660617f, -0.5615287f, -0.575052f, -0.1832343f), new(1f, 1f, 1f)),
new(new(8.237998f, 14.162f, 6.878995f), new(-0.3394944f, -0.7236302f, -0.4614432f, -0.3849328f), new(1f, 1f, 1f)),
new(new(-8.105976f, 1.567999f, 9.326996f), new(0.1392731f, -0.7048326f, -0.6932754f, -0.05642127f), new(1f, 1f, 1f)),
new(new(-11.8123f, -2.4774f, 7.2488f), new(-0.5196269f, -0.4540258f, -0.4950719f, -0.5279701f), new(1f, 1f, 1f)),
new(new(0.7281f, 3.8064f, 8.772f), new(-0.5932296f, -0.4087047f, -0.4483426f, -0.5291768f), new(1f, 1f, 1f)),
new(new(0.1910008f, -4.7408f, 8.755607f), new(-0.7313722f, -0.04450077f, -0.1555427f, -0.6625111f), new(1f, 1f, 1f)),
new(new(15.488f, 10.689f, 5.394005f), new(-0.6516617f, -0.3269425f, -0.2105236f, -0.6512492f), new(1f, 1f, 1f)),
new(new(21.61f, 0.04200077f, 4.873005f), new(-0.6519138f, -0.6227682f, -0.315786f, -0.2957152f), new(1f, 1f, 1f)),
new(new(-12.5287f, -10.26078f, 6.387112f), new(-0.3034183f, -0.62968f, -0.6713617f, -0.2464023f), new(1f, 1f, 1f)),
new(new(-10.744f, 10.656f, 6.481f), new(-0.4014427f, -0.5361503f, -0.452841f, -0.5884911f), new(1f, 1f, 1f)),
new(new(-1.322f, -8.544998f, 8.412007f), new(-0.3098519f, -0.268013f, -0.6520668f, -0.6379418f), new(1f, 1f, 1f)),
new(new(0.9206981f, 11.9358f, 7.261901f), new(-0.2040951f, -0.5998042f, -0.6168172f, -0.4670296f), new(1f, 1f, 1f)),
new(new(9.23f, -0.908f, 9.564f), new(-0.6778914f, -0.05161315f, 0.0538789f, -0.7313663f), new(1f, 1f, 1f)),
];
/// <summary>
/// Based on <see cref="ReefbackSlotsData.ReefbackSlotCreature"/>
/// </summary>
public struct ReefbackSlotCreature
{
public int MinNumber;
public int MaxNumber;
public float Probability;
public string ClassId;
}
/// <summary>
/// Based on <see cref="ReefbackSlotsData.ReefbackSlotPlant"/>
/// </summary>
public struct ReefbackSlotPlant
{
public List<string> ClassIds;
public float Probability;
public NitroxVector3 StartRotation
{
set => StartRotationQuaternion = NitroxQuaternion.FromEuler(value);
}
public NitroxQuaternion StartRotationQuaternion;
}
}

View File

@@ -0,0 +1,34 @@
using System.Collections.Generic;
namespace NitroxServer_Subnautica.GameLogic.Entities.Spawning
{
public static class SlotsHelper
{
private static Dictionary<EntitySlotData.EntitySlotType, EntitySlot.Type> typeMapping = new Dictionary<EntitySlotData.EntitySlotType, EntitySlot.Type>
{
{ EntitySlotData.EntitySlotType.Small, EntitySlot.Type.Small },
{ EntitySlotData.EntitySlotType.Medium, EntitySlot.Type.Medium },
{ EntitySlotData.EntitySlotType.Large, EntitySlot.Type.Large },
{ EntitySlotData.EntitySlotType.Tall, EntitySlot.Type.Tall },
{ EntitySlotData.EntitySlotType.Creature, EntitySlot.Type.Creature }
};
public static List<EntitySlot.Type> ConvertSlotTypes(EntitySlotData.EntitySlotType entitySlotType)
{
List<EntitySlot.Type> slotsTypes = new List<EntitySlot.Type>();
foreach (KeyValuePair<EntitySlotData.EntitySlotType, EntitySlot.Type> mapping in typeMapping)
{
EntitySlotData.EntitySlotType slotType = mapping.Key;
EntitySlot.Type type = mapping.Value;
if ((entitySlotType & slotType) == slotType)
{
slotsTypes.Add(type);
}
}
return slotsTypes;
}
}
}

View File

@@ -0,0 +1,14 @@
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxServer.GameLogic.Entities.Spawning;
using NitroxServer.Helper;
namespace NitroxServer_Subnautica.GameLogic.Entities.Spawning;
public class StayAtLeashPositionBootstrapper : IEntityBootstrapper
{
public void Prepare(ref WorldEntity spawnedEntity, DeterministicGenerator generator)
{
spawnedEntity.Metadata = new StayAtLeashPositionMetadata(spawnedEntity.Transform.Position);
}
}

View File

@@ -0,0 +1,34 @@
using System.Collections.Generic;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel_Subnautica.DataStructures;
using NitroxServer.GameLogic.Entities.Spawning;
using NitroxServer.Helper;
using NitroxServer_Subnautica.GameLogic.Entities.Spawning.EntityBootstrappers;
namespace NitroxServer_Subnautica.GameLogic.Entities.Spawning;
public class SubnauticaEntityBootstrapperManager : IEntityBootstrapperManager
{
private static readonly Dictionary<NitroxTechType, IEntityBootstrapper> entityBootstrappersByTechType = new()
{
[TechType.CrashHome.ToDto()] = new CrashHomeBootstrapper(),
[TechType.ReaperLeviathan.ToDto()] = new StayAtLeashPositionBootstrapper(),
[TechType.SeaDragon.ToDto()] = new StayAtLeashPositionBootstrapper(),
[TechType.GhostLeviathan.ToDto()] = new StayAtLeashPositionBootstrapper(),
};
private static readonly Dictionary<string, IEntityBootstrapper> entityBootstrappersByClassId = new()
{
["ce0b4131-86e2-444b-a507-45f7b824a286"] = new GeyserBootstrapper(),
["8d3d3c8b-9290-444a-9fea-8e5493ecd6fe"] = new ReefbackBootstrapper()
};
public void PrepareEntityIfRequired(ref WorldEntity spawnedEntity, DeterministicGenerator generator)
{
if (entityBootstrappersByTechType.TryGetValue(spawnedEntity.TechType, out IEntityBootstrapper bootstrapper) ||
(!string.IsNullOrEmpty(spawnedEntity.ClassId) && entityBootstrappersByClassId.TryGetValue(spawnedEntity.ClassId, out bootstrapper)))
{
bootstrapper.Prepare(ref spawnedEntity, generator);
}
}
}

View File

@@ -0,0 +1,70 @@
using System.Collections.Generic;
using System.Linq;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.Unity;
using NitroxModel_Subnautica.DataStructures;
using NitroxServer.GameLogic.Entities.Spawning;
using NitroxServer.UnityStubs;
namespace NitroxServer_Subnautica.GameLogic.Entities.Spawning
{
public class SubnauticaEntitySpawnPointFactory : EntitySpawnPointFactory
{
private readonly Dictionary<string, EntitySpawnPoint> spawnPointsByUid = new Dictionary<string, EntitySpawnPoint>();
public override List<EntitySpawnPoint> From(AbsoluteEntityCell absoluteEntityCell, NitroxTransform transform, GameObject gameObject)
{
List<EntitySpawnPoint> spawnPoints = new List<EntitySpawnPoint>();
EntitySlotsPlaceholder entitySlotsPlaceholder = gameObject.GetComponent<EntitySlotsPlaceholder>();
if (gameObject.CreateEmptyObject)
{
SerializedEntitySpawnPoint entitySpawnPoint = new(gameObject.SerializedComponents, gameObject.Layer, absoluteEntityCell, transform);
HandleParenting(spawnPoints, entitySpawnPoint, gameObject);
spawnPoints.Add(entitySpawnPoint);
}
else if (!ReferenceEquals(entitySlotsPlaceholder, null))
{
foreach (EntitySlotData entitySlotData in entitySlotsPlaceholder.slotsData)
{
List<EntitySlot.Type> slotTypes = SlotsHelper.ConvertSlotTypes(entitySlotData.allowedTypes);
List<string> stringSlotTypes = slotTypes.Select(s => s.ToString()).ToList();
EntitySpawnPoint entitySpawnPoint = new(absoluteEntityCell,
entitySlotData.localPosition.ToDto(),
entitySlotData.localRotation.ToDto(),
stringSlotTypes,
entitySlotData.density,
entitySlotData.biomeType.ToString());
HandleParenting(spawnPoints, entitySpawnPoint, gameObject);
}
}
else
{
EntitySpawnPoint entitySpawnPoint = new(absoluteEntityCell, transform.LocalPosition, transform.LocalRotation, transform.LocalScale, gameObject.ClassId);
HandleParenting(spawnPoints, entitySpawnPoint, gameObject);
}
return spawnPoints;
}
private void HandleParenting(List<EntitySpawnPoint> spawnPoints, EntitySpawnPoint entitySpawnPoint, GameObject gameObject)
{
if (gameObject.Parent != null && spawnPointsByUid.TryGetValue(gameObject.Parent, out EntitySpawnPoint parent))
{
entitySpawnPoint.Parent = parent;
parent.Children.Add(entitySpawnPoint);
}
spawnPointsByUid[gameObject.Id] = entitySpawnPoint;
if (gameObject.Parent == null)
{
spawnPoints.Add(entitySpawnPoint);
}
}
}
}

View File

@@ -0,0 +1,21 @@
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxServer.GameLogic;
using NitroxServer.Serialization.World;
namespace NitroxServer_Subnautica.GameLogic;
public class SubnauticaWorldModifier : IWorldModifier
{
// This constant is defined by Subnautica and should never be modified
private const int TOTAL_LEAKS = 11;
public void ModifyWorld(World world)
{
// Creating entities for the 11 RadiationLeakPoint located at (Aurora Scene) //Aurora-MainPrefab/Aurora/radiationleaks/RadiationLeaks(Clone)
for (int i = 0; i < TOTAL_LEAKS; i++)
{
RadiationLeakEntity leakEntity = new(new(), i, new(0));
world.WorldEntityManager.AddOrUpdateGlobalRootEntity(leakEntity);
}
}
}

View File

@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<OutputType>Exe</OutputType>
<RootNamespace>NitroxServer_Subnautica</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\NitroxModel-Subnautica\NitroxModel-Subnautica.csproj" />
<ProjectReference Include="..\NitroxServer\NitroxServer.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AssetsTools.NET" Version="3.0.0-preview1" />
<PackageReference Include="SixLabors.ImageSharp" Version="[2.1.10]" />
</ItemGroup>
<ItemGroup>
<Reference Include="protobuf-net">
<HintPath>..\Nitrox.Assets.Subnautica\protobuf-net.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Content Include="..\Nitrox.Assets.Subnautica\**\*.tpk" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<Import Project="..\Nitrox.Shared.targets" />
</Project>

View File

@@ -0,0 +1,602 @@
global using NitroxModel.Logger;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using NitroxModel;
using NitroxModel.Core;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.Util;
using NitroxModel.Helper;
using NitroxServer;
using NitroxServer_Subnautica.Communication;
using NitroxServer.ConsoleCommands.Processor;
namespace NitroxServer_Subnautica;
[SuppressMessage("Usage", "DIMA001:Dependency Injection container is used directly")]
public class Program
{
private static Lazy<string> gameInstallDir;
private static readonly CircularBuffer<string> inputHistory = new(1000);
private static int currentHistoryIndex;
private static readonly CancellationTokenSource serverCts = new();
private static async Task Main(string[] args)
{
AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolver.Handler;
AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += AssemblyResolver.Handler;
await StartServer(args);
}
/// <summary>
/// Initialize server here so that the JIT can compile the EntryPoint method without having to resolve dependencies
/// that require the <see cref="AppDomain.AssemblyResolve" /> handler.
/// </summary>
/// <remarks>
/// https://stackoverflow.com/a/6089153/1277156
/// </remarks>
[MethodImpl(MethodImplOptions.NoInlining)]
private static async Task StartServer(string[] args)
{
// The thread that writers to console is paused while selecting text in console. So console writer needs to be async.
Log.Setup(true, isConsoleApp: !args.Contains("--embedded", StringComparer.OrdinalIgnoreCase));
AppDomain.CurrentDomain.UnhandledException += CurrentDomainOnUnhandledException;
PosixSignalRegistration.Create(PosixSignal.SIGTERM, CloseWindowHandler);
PosixSignalRegistration.Create(PosixSignal.SIGQUIT, CloseWindowHandler);
PosixSignalRegistration.Create(PosixSignal.SIGINT, CloseWindowHandler);
PosixSignalRegistration.Create(PosixSignal.SIGHUP, CloseWindowHandler);
CultureManager.ConfigureCultureInfo();
if (!Console.IsInputRedirected)
{
Console.TreatControlCAsInput = true;
}
Log.Info($"Starting NitroxServer {NitroxEnvironment.ReleasePhase} v{NitroxEnvironment.Version} for {GameInfo.Subnautica.FullName}");
Log.Debug($@"Process start args: ""{string.Join(@""", """, Environment.GetCommandLineArgs())}""");
Task handleConsoleInputTask;
Server server;
try
{
handleConsoleInputTask = HandleConsoleInputAsync(ConsoleCommandHandler(), serverCts.Token);
AppMutex.Hold(() => Log.Info("Waiting on other Nitrox servers to initialize before starting.."), serverCts.Token);
Stopwatch watch = Stopwatch.StartNew();
// Allow game path to be given as command argument
string gameDir;
if (args.Length > 0 && Directory.Exists(args[0]) && File.Exists(Path.Combine(args[0], GameInfo.Subnautica.ExeName)))
{
gameDir = Path.GetFullPath(args[0]);
gameInstallDir = new Lazy<string>(() => gameDir);
}
else
{
gameInstallDir = new Lazy<string>(() =>
{
return gameDir = NitroxUser.GamePath;
});
}
Log.Info($"Using game files from: \'{gameInstallDir.Value}\'");
// TODO: Fix DI to not be slow (should not use IO in type constructors). Instead, use Lazy<T> (et al). This way, cancellation can be faster.
NitroxServiceLocator.InitializeDependencyContainer(new SubnauticaServerAutoFacRegistrar());
NitroxServiceLocator.BeginNewLifetimeScope();
server = NitroxServiceLocator.LocateService<Server>();
string serverSaveName = Server.GetSaveName(args, "My World");
Log.SaveName = serverSaveName;
using (CancellationTokenSource portWaitCts = CancellationTokenSource.CreateLinkedTokenSource(serverCts.Token))
{
TimeSpan portWaitTimeout = TimeSpan.FromSeconds(30);
portWaitCts.CancelAfter(portWaitTimeout);
await WaitForAvailablePortAsync(server.Port, portWaitTimeout, portWaitCts.Token);
}
if (!serverCts.IsCancellationRequested)
{
if (!server.Start(serverSaveName, serverCts))
{
throw new Exception("Unable to start server.");
}
else
{
Log.Info($"Server started ({Math.Round(watch.Elapsed.TotalSeconds, 1)}s)");
Log.Info("To get help for commands, run help in console or /help in chatbox");
}
}
}
finally
{
// Allow other servers to start initializing.
AppMutex.Release();
}
await handleConsoleInputTask;
server.Stop(true);
try
{
if (Environment.UserInteractive && Console.In != StreamReader.Null && Debugger.IsAttached)
{
Task.Delay(100).Wait(); // Wait for async logs to flush to console
Console.WriteLine($"{Environment.NewLine}Press any key to continue . . .");
Console.ReadKey(true);
}
}
catch
{
// ignored
}
Action<string> ConsoleCommandHandler()
{
ConsoleCommandProcessor commandProcessor = null;
return submit =>
{
try
{
commandProcessor ??= NitroxServiceLocator.LocateService<ConsoleCommandProcessor>();
}
catch (Exception)
{
// ignored
}
commandProcessor?.ProcessCommand(submit, Optional.Empty, Perms.CONSOLE);
};
}
}
private static void CloseWindowHandler(PosixSignalContext context)
{
context.Cancel = false;
serverCts?.Cancel();
}
/// <summary>
/// Handles per-key input of the console and passes input submit to <see cref="ConsoleCommandProcessor" />.
/// </summary>
private static async Task HandleConsoleInputAsync(Action<string> submitHandler, CancellationToken ct = default)
{
ConcurrentQueue<string> commandQueue = new();
if (Console.IsInputRedirected)
{
Log.Info("Server input stream is redirected");
_ = Task.Run(() =>
{
while (!ct.IsCancellationRequested)
{
string commandRead = Console.ReadLine();
commandQueue.Enqueue(commandRead);
}
}, ct).ContinueWith(t =>
{
if (t.IsFaulted)
{
Log.Error(t.Exception);
}
}, ct);
}
else
{
Log.Info("Server input stream is available");
StringBuilder inputLineBuilder = new();
void ClearInputLine()
{
currentHistoryIndex = 0;
inputLineBuilder.Clear();
Console.Write($"\r{new string(' ', Console.WindowWidth - 1)}\r");
}
void RedrawInput(int start = 0, int end = 0)
{
int lastPosition = Console.CursorLeft;
// Expand range to end if end value is -1
if (start > -1 && end == -1)
{
end = Math.Max(inputLineBuilder.Length - start, 0);
}
if (start == 0 && end == 0)
{
// Redraw entire line
Console.Write($"\r{new string(' ', Console.WindowWidth - 1)}\r{inputLineBuilder}");
}
else
{
// Redraw part of line
string changedInputSegment = inputLineBuilder.ToString(start, end);
Console.CursorVisible = false;
Console.Write($"{changedInputSegment}{new string(' ', inputLineBuilder.Length - changedInputSegment.Length - Console.CursorLeft + 1)}");
Console.CursorVisible = true;
}
Console.CursorLeft = lastPosition;
}
_ = Task.Run(async () =>
{
while (!ct.IsCancellationRequested)
{
if (!Console.KeyAvailable)
{
try
{
await Task.Delay(10, ct);
}
catch (TaskCanceledException)
{
// ignored
}
continue;
}
ConsoleKeyInfo keyInfo = Console.ReadKey(true);
// Handle (ctrl) hotkeys
if ((keyInfo.Modifiers & ConsoleModifiers.Control) != 0)
{
switch (keyInfo.Key)
{
case ConsoleKey.C:
if (inputLineBuilder.Length > 0)
{
ClearInputLine();
continue;
}
await serverCts.CancelAsync();
return;
case ConsoleKey.D:
await serverCts.CancelAsync();
return;
default:
// Unhandled modifier key
continue;
}
}
if (keyInfo.Modifiers == 0)
{
switch (keyInfo.Key)
{
case ConsoleKey.LeftArrow when Console.CursorLeft > 0:
Console.CursorLeft--;
continue;
case ConsoleKey.RightArrow when Console.CursorLeft < inputLineBuilder.Length:
Console.CursorLeft++;
continue;
case ConsoleKey.Backspace:
if (inputLineBuilder.Length > Console.CursorLeft - 1 && Console.CursorLeft > 0)
{
inputLineBuilder.Remove(Console.CursorLeft - 1, 1);
Console.CursorLeft--;
Console.Write(' ');
Console.CursorLeft--;
RedrawInput();
}
continue;
case ConsoleKey.Delete:
if (inputLineBuilder.Length > 0 && Console.CursorLeft < inputLineBuilder.Length)
{
inputLineBuilder.Remove(Console.CursorLeft, 1);
RedrawInput(Console.CursorLeft, inputLineBuilder.Length - Console.CursorLeft);
}
continue;
case ConsoleKey.Home:
Console.CursorLeft = 0;
continue;
case ConsoleKey.End:
Console.CursorLeft = inputLineBuilder.Length;
continue;
case ConsoleKey.Escape:
ClearInputLine();
continue;
case ConsoleKey.Tab:
if (Console.CursorLeft + 4 < Console.WindowWidth)
{
inputLineBuilder.Insert(Console.CursorLeft, " ");
RedrawInput(Console.CursorLeft, -1);
Console.CursorLeft += 4;
}
continue;
case ConsoleKey.UpArrow when inputHistory.Count > 0 && currentHistoryIndex > -inputHistory.Count:
inputLineBuilder.Clear();
inputLineBuilder.Append(inputHistory[--currentHistoryIndex]);
RedrawInput();
Console.CursorLeft = Math.Min(inputLineBuilder.Length, Console.WindowWidth);
continue;
case ConsoleKey.DownArrow when inputHistory.Count > 0 && currentHistoryIndex < 0:
if (currentHistoryIndex == -1)
{
ClearInputLine();
continue;
}
inputLineBuilder.Clear();
inputLineBuilder.Append(inputHistory[++currentHistoryIndex]);
RedrawInput();
Console.CursorLeft = Math.Min(inputLineBuilder.Length, Console.WindowWidth);
continue;
}
}
// Handle input submit to submit handler
if (keyInfo.Key == ConsoleKey.Enter)
{
string submit = inputLineBuilder.ToString();
if (inputHistory.Count == 0 || inputHistory[inputHistory.LastChangedIndex] != submit)
{
inputHistory.Add(submit);
}
currentHistoryIndex = 0;
commandQueue.Enqueue(submit);
inputLineBuilder.Clear();
Console.WriteLine();
continue;
}
// If unhandled key, append as input.
if (keyInfo.KeyChar != 0)
{
Console.Write(keyInfo.KeyChar);
if (Console.CursorLeft - 1 < inputLineBuilder.Length)
{
inputLineBuilder.Insert(Console.CursorLeft - 1, keyInfo.KeyChar);
RedrawInput(Console.CursorLeft, -1);
}
else
{
inputLineBuilder.Append(keyInfo.KeyChar);
}
}
}
}, ct).ContinueWith(t =>
{
if (t.IsFaulted)
{
Log.Error(t.Exception);
}
}, ct);
}
using IpcHost ipcHost = IpcHost.StartReadingCommands(command => commandQueue.Enqueue(command), ct);
if (!Console.IsInputRedirected)
{
// Important to not hang process: keep command handler on the main thread when input not redirected (i.e. don't Task.Run)
while (!ct.IsCancellationRequested)
{
while (commandQueue.TryDequeue(out string command))
{
submitHandler(command);
}
try
{
await Task.Delay(10, ct);
}
catch (OperationCanceledException)
{
// ignored
}
}
}
else
{
// Important to not hang process (when running launcher from release exe): free main thread if input redirected
await Task.Run(async () =>
{
while (!ct.IsCancellationRequested)
{
while (commandQueue.TryDequeue(out string command))
{
submitHandler(command);
}
try
{
await Task.Delay(10, ct);
}
catch (OperationCanceledException)
{
// ignored
}
}
}, ct).ContinueWithHandleError();
}
}
private static async Task WaitForAvailablePortAsync(int port, TimeSpan timeout = default, CancellationToken ct = default)
{
if (timeout == default)
{
timeout = TimeSpan.FromSeconds(30);
}
else
{
Validate.IsTrue(timeout.TotalSeconds >= 5, "Timeout must be at least 5 seconds.");
}
int messageLength = 0;
void PrintPortWarn(TimeSpan timeRemaining)
{
string message = $"Port {port} UDP is already in use. Please change the server port or close out any program that may be using it. Retrying for {Math.Floor(timeRemaining.TotalSeconds)} seconds until it is available...";
messageLength = message.Length;
Log.Warn(message);
}
DateTimeOffset time = DateTimeOffset.UtcNow;
bool first = true;
try
{
while (true)
{
ct.ThrowIfCancellationRequested();
IPEndPoint endPoint = IPGlobalProperties.GetIPGlobalProperties().GetActiveUdpListeners().FirstOrDefault(ip => ip.Port == port);
if (endPoint == null)
{
break;
}
if (first)
{
first = false;
PrintPortWarn(timeout);
}
else if (Environment.UserInteractive && !Console.IsInputRedirected && Console.In != StreamReader.Null)
{
// If not first time, move cursor up the number of lines it takes up to overwrite previous message
int numberOfLines = (int)Math.Ceiling( ((double)messageLength + 15) / Console.BufferWidth );
for (int i = 0; i < numberOfLines; i++)
{
if (Console.CursorTop > 0) // Check to ensure we don't go out of bounds
{
Console.CursorTop--;
}
}
Console.CursorLeft = 0;
PrintPortWarn(timeout - (DateTimeOffset.UtcNow - time));
}
await Task.Delay(500, ct);
}
}
catch (OperationCanceledException)
{
// ignored
}
}
private static void CurrentDomainOnUnhandledException(object sender, UnhandledExceptionEventArgs e)
{
if (e.ExceptionObject is Exception ex)
{
Log.Error(ex);
}
if (!Environment.UserInteractive || Console.IsInputRedirected || Console.In == StreamReader.Null)
{
return;
}
// TODO: Implement log file opening by server name
/*string mostRecentLogFile = Log.GetMostRecentLogFile(); // Log.SaveName
if (mostRecentLogFile == null)
{
return;
}
Log.Info("Press L to open log file before closing. Press any other key to close . . .");*/
Log.Info("Press L to open log folder before closing. Press any other key to close . . .");
ConsoleKeyInfo key = Console.ReadKey(true);
if (key.Key == ConsoleKey.L)
{
// Log.Info($"Opening log file at: {mostRecentLogFile}..");
// using Process process = FileSystem.Instance.OpenOrExecuteFile(mostRecentLogFile);
Process.Start(new ProcessStartInfo
{
FileName = Log.LogDirectory,
Verb = "open",
UseShellExecute = true
})?.Dispose();
}
Environment.Exit(1);
}
private static class AssemblyResolver
{
private static string currentExecutableDirectory;
private static readonly Dictionary<string, Assembly> resolvedAssemblyCache = [];
public static Assembly Handler(object sender, ResolveEventArgs args)
{
static Assembly ResolveFromLib(ReadOnlySpan<char> dllName)
{
dllName = dllName.Slice(0, Math.Max(dllName.IndexOf(','), 0));
if (dllName.IsEmpty)
{
return null;
}
if (!dllName.EndsWith(".dll"))
{
dllName = string.Concat(dllName, ".dll");
}
if (dllName.EndsWith(".resources.dll"))
{
return null;
}
string dllNameStr = dllName.ToString();
// If available, return cached assembly
if (resolvedAssemblyCache.TryGetValue(dllNameStr, out Assembly val))
{
return val;
}
// Load DLLs where this program (exe) is located
string dllPath = Path.Combine(GetExecutableDirectory(), "lib", dllNameStr);
// Prefer to use Newtonsoft dll from game instead of our own due to protobuf issues. TODO: Remove when we do our own deserialization of game data instead of using the game's protobuf.
if (dllPath.IndexOf("Newtonsoft.Json.dll", StringComparison.OrdinalIgnoreCase) >= 0 || !File.Exists(dllPath))
{
// Try find game managed libraries
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
dllPath = Path.Combine(gameInstallDir.Value, "Resources", "Data", "Managed", dllNameStr);
}
else
{
dllPath = Path.Combine(gameInstallDir.Value, "Subnautica_Data", "Managed", dllNameStr);
}
}
try
{
// Read assemblies as bytes as to not lock the file so that Nitrox can patch assemblies while server is running.
Assembly assembly = Assembly.Load(File.ReadAllBytes(dllPath));
return resolvedAssemblyCache[dllNameStr] = assembly;
}
catch
{
return null;
}
}
Assembly assembly = ResolveFromLib(args.Name);
if (assembly == null && !args.Name.Contains(".resources"))
{
assembly = Assembly.Load(args.Name);
}
return assembly;
}
private static string GetExecutableDirectory()
{
if (currentExecutableDirectory != null)
{
return currentExecutableDirectory;
}
string pathAttempt = Assembly.GetEntryAssembly()?.Location;
if (string.IsNullOrWhiteSpace(pathAttempt))
{
using Process proc = Process.GetCurrentProcess();
pathAttempt = proc.MainModule?.FileName;
}
return currentExecutableDirectory = new Uri(Path.GetDirectoryName(pathAttempt ?? ".") ?? Directory.GetCurrentDirectory()).LocalPath;
}
}
}

View File

@@ -0,0 +1,30 @@
using System.Reflection;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyDescription("Nitrox server implemenation for the game Subnautica")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
// COMMON: [assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("eff1d7a5-efd6-413a-8d5f-dc2408e4c9b7")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
// COMMON: [assembly: AssemblyVersion("X.X.X.X")]
// COMMON: [assembly: AssemblyFileVersion("X.X.X.X")]

View File

@@ -0,0 +1,11 @@
{
"profiles": {
"NitroxServer-Subnautica": {
"commandName": "Project"
},
"Launch test save": {
"commandName": "Project",
"commandLineArgs": "--save test"
}
}
}

View File

@@ -0,0 +1,24 @@
using AddressablesTools.Catalog;
using AddressablesTools.JSON;
using Newtonsoft.Json;
namespace AddressablesTools
{
public static class AddressablesJsonParser
{
internal static ContentCatalogDataJson CCDJsonFromString(string data)
{
return JsonConvert.DeserializeObject<ContentCatalogDataJson>(data);
}
public static ContentCatalogData FromString(string data)
{
ContentCatalogDataJson ccdJson = CCDJsonFromString(data);
ContentCatalogData catalogData = new ContentCatalogData();
catalogData.Read(ccdJson);
return catalogData;
}
}
}

View File

@@ -0,0 +1,16 @@
namespace AddressablesTools.Catalog
{
internal class ClassJsonObject
{
public string AssemblyName { get; }
public string ClassName { get; }
public string JsonText { get; }
public ClassJsonObject(string assemblyName, string className, string jsonText)
{
AssemblyName = assemblyName;
ClassName = className;
JsonText = jsonText;
}
}
}

View File

@@ -0,0 +1,177 @@
using AddressablesTools.JSON;
using System;
using System.Collections.Generic;
using System.IO;
namespace AddressablesTools.Catalog
{
public class ContentCatalogData
{
public string LocatorId { get; set; }
public ObjectInitializationData InstanceProviderData { get; set; }
public ObjectInitializationData SceneProviderData { get; set; }
public ObjectInitializationData[] ResourceProviderData { get; set; }
public string[] ProviderIds { get; set; }
public string[] InternalIds { get; set; }
public SerializedType[] ResourceTypes { get; set; }
public string[] InternalIdPrefixes { get; set; }
public Dictionary<object, List<ResourceLocation>> Resources { get; set; }
internal void Read(ContentCatalogDataJson data)
{
LocatorId = data.m_LocatorId;
InstanceProviderData = new ObjectInitializationData();
InstanceProviderData.Read(data.m_InstanceProviderData);
SceneProviderData = new ObjectInitializationData();
SceneProviderData.Read(data.m_SceneProviderData);
ResourceProviderData = new ObjectInitializationData[data.m_ResourceProviderData.Length];
for (int i = 0; i < ResourceProviderData.Length; i++)
{
ResourceProviderData[i] = new ObjectInitializationData();
ResourceProviderData[i].Read(data.m_ResourceProviderData[i]);
}
ProviderIds = new string[data.m_ProviderIds.Length];
for (int i = 0; i < ProviderIds.Length; i++)
{
ProviderIds[i] = data.m_ProviderIds[i];
}
InternalIds = new string[data.m_InternalIds.Length];
for (int i = 0; i < InternalIds.Length; i++)
{
InternalIds[i] = data.m_InternalIds[i];
}
ResourceTypes = new SerializedType[data.m_resourceTypes.Length];
for (int i = 0; i < ResourceTypes.Length; i++)
{
ResourceTypes[i] = new SerializedType();
ResourceTypes[i].Read(data.m_resourceTypes[i]);
}
InternalIdPrefixes = new string[data.m_InternalIdPrefixes.Length];
for (int i = 0; i < InternalIdPrefixes.Length; i++)
{
InternalIdPrefixes[i] = data.m_InternalIdPrefixes[i];
}
ReadResources(data);
}
private void ReadResources(ContentCatalogDataJson data)
{
List<Bucket> buckets;
MemoryStream bucketStream = new MemoryStream(Convert.FromBase64String(data.m_BucketDataString));
using (BinaryReader bucketReader = new BinaryReader(bucketStream))
{
int bucketCount = bucketReader.ReadInt32();
buckets = new List<Bucket>(bucketCount);
for (int i = 0; i < bucketCount; i++)
{
int offset = bucketReader.ReadInt32();
int entryCount = bucketReader.ReadInt32();
int[] entries = new int[entryCount];
for (int j = 0; j < entryCount; j++)
{
entries[j] = bucketReader.ReadInt32();
}
buckets.Add(new Bucket(offset, entries));
}
}
List<object> keys;
MemoryStream keyDataStream = new MemoryStream(Convert.FromBase64String(data.m_KeyDataString));
using (BinaryReader keyReader = new BinaryReader(keyDataStream))
{
int keyCount = keyReader.ReadInt32();
keys = new List<object>(keyCount);
for (int i = 0; i < keyCount; i++)
{
keyDataStream.Position = buckets[i].offset;
keys.Add(SerializedObjectDecoder.Decode(keyReader));
}
}
List<ResourceLocation> locations;
MemoryStream entryDataStream = new MemoryStream(Convert.FromBase64String(data.m_EntryDataString));
MemoryStream extraDataStream = new MemoryStream(Convert.FromBase64String(data.m_ExtraDataString));
using (BinaryReader entryReader = new BinaryReader(entryDataStream))
using (BinaryReader extraReader = new BinaryReader(extraDataStream))
{
int entryCount = entryReader.ReadInt32();
locations = new List<ResourceLocation>(entryCount);
for (int i = 0; i < entryCount; i++)
{
int internalIdIndex = entryReader.ReadInt32();
int providerIndex = entryReader.ReadInt32();
int dependencyKeyIndex = entryReader.ReadInt32();
int depHash = entryReader.ReadInt32();
int dataIndex = entryReader.ReadInt32();
int primaryKeyIndex = entryReader.ReadInt32();
int resourceTypeIndex = entryReader.ReadInt32();
string internalId = InternalIds[internalIdIndex];
string providerId = ProviderIds[providerIndex];
object dependencyKey = null;
if (dependencyKeyIndex >= 0)
{
dependencyKey = keys[dependencyKeyIndex];
}
object objData = null;
if (dataIndex >= 0)
{
extraDataStream.Position = dataIndex;
objData = SerializedObjectDecoder.Decode(extraReader);
}
object primaryKey = keys[primaryKeyIndex];
SerializedType resourceType = ResourceTypes[resourceTypeIndex];
var loc = new ResourceLocation();
loc.ReadCompact(internalId, providerId, dependencyKey, objData, depHash, primaryKey, resourceType);
locations.Add(loc);
}
}
Resources = new Dictionary<object, List<ResourceLocation>>(buckets.Count);
for (int i = 0; i < buckets.Count; i++)
{
int[] bucketEntries = buckets[i].entries;
List<ResourceLocation> locs = new List<ResourceLocation>(bucketEntries.Length);
for (int j = 0; j < bucketEntries.Length; j++)
{
locs.Add(locations[bucketEntries[j]]);
}
Resources[keys[i]] = locs;
}
}
private struct Bucket
{
public int offset;
public int[] entries;
public Bucket(int offset, int[] entries)
{
this.offset = offset;
this.entries = entries;
}
}
}
}

View File

@@ -0,0 +1,19 @@
using AddressablesTools.JSON;
namespace AddressablesTools.Catalog
{
public class ObjectInitializationData
{
public string Id { get; set; }
public SerializedType ObjectType { get; set; }
public string Data { get; set; }
internal void Read(ObjectInitializationDataJson obj)
{
Id = obj.m_Id;
ObjectType = new SerializedType();
ObjectType.Read(obj.m_ObjectType);
Data = obj.m_Data;
}
}
}

View File

@@ -0,0 +1,29 @@
namespace AddressablesTools.Catalog
{
public class ResourceLocation
{
public string InternalId { get; set; }
public string ProviderId { get; set; }
public object Dependency { get; set; }
public object Data { get; set; }
public int HashCode { get; set; }
public int DependencyHashCode { get; set; }
public string PrimaryKey { get; set; }
public SerializedType Type { get; set; }
internal void ReadCompact(
string internalId, string providerId, object dependencyKey, object data,
int depHashCode, object primaryKey, SerializedType resourceType
)
{
InternalId = internalId;
ProviderId = providerId;
Dependency = dependencyKey;
Data = data;
HashCode = internalId.GetHashCode() * 31 + providerId.GetHashCode();
DependencyHashCode = depHashCode;
PrimaryKey = primaryKey.ToString();
Type = resourceType;
}
}
}

View File

@@ -0,0 +1,109 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
namespace AddressablesTools.Catalog
{
internal static class SerializedObjectDecoder
{
internal enum ObjectType
{
AsciiString,
UnicodeString,
UInt16,
UInt32,
Int32,
Hash128,
Type,
JsonObject
}
internal static object Decode(BinaryReader br)
{
ObjectType type = (ObjectType)br.ReadByte();
switch (type)
{
case ObjectType.AsciiString:
{
string str = ReadString4(br);
return str;
}
case ObjectType.UnicodeString:
{
string str = ReadString4Unicode(br);
return str;
}
case ObjectType.UInt16:
{
return br.ReadUInt16();
}
case ObjectType.UInt32:
{
return br.ReadUInt32();
}
case ObjectType.Int32:
{
return br.ReadInt32();
}
case ObjectType.Hash128:
{
// read as string for now
string str = ReadString1(br);
return str;
}
case ObjectType.Type:
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
throw new NotSupportedException($"{nameof(ObjectType)}.{nameof(ObjectType.Type)} is only supported on windows because it uses {nameof(Type.GetTypeFromCLSID)}");
}
string str = ReadString1(br);
return Type.GetTypeFromCLSID(new Guid(str));
}
case ObjectType.JsonObject:
{
string assemblyName = ReadString1(br);
string className = ReadString1(br);
string jsonText = ReadString4Unicode(br);
ClassJsonObject jsonObj = new ClassJsonObject(assemblyName, className, jsonText);
return jsonObj;
}
default:
{
return null;
}
}
}
private static string ReadString1(BinaryReader br)
{
int length = br.ReadByte();
string str = Encoding.ASCII.GetString(br.ReadBytes(length));
return str;
}
private static string ReadString4(BinaryReader br)
{
int length = br.ReadInt32();
string str = Encoding.ASCII.GetString(br.ReadBytes(length));
return str;
}
private static string ReadString4Unicode(BinaryReader br)
{
int length = br.ReadInt32();
string str = Encoding.Unicode.GetString(br.ReadBytes(length));
return str;
}
}
}

View File

@@ -0,0 +1,16 @@
using AddressablesTools.JSON;
namespace AddressablesTools.Catalog
{
public class SerializedType
{
public string AssemblyName { get; set; }
public string ClassName { get; set; }
internal void Read(SerializedTypeJson type)
{
AssemblyName = type.m_AssemblyName;
ClassName = type.m_ClassName;
}
}
}

View File

@@ -0,0 +1,20 @@
namespace AddressablesTools.JSON
{
#pragma warning disable IDE1006
internal class ContentCatalogDataJson
{
public string m_LocatorId { get; set; }
public ObjectInitializationDataJson m_InstanceProviderData { get; set; }
public ObjectInitializationDataJson m_SceneProviderData { get; set; }
public ObjectInitializationDataJson[] m_ResourceProviderData { get; set; }
public string[] m_ProviderIds { get; set; }
public string[] m_InternalIds { get; set; }
public string m_KeyDataString { get; set; }
public string m_BucketDataString { get; set; }
public string m_EntryDataString { get; set; }
public string m_ExtraDataString { get; set; }
public SerializedTypeJson[] m_resourceTypes { get; set; }
public string[] m_InternalIdPrefixes { get; set; }
}
#pragma warning restore IDE1006
}

View File

@@ -0,0 +1,11 @@
namespace AddressablesTools.JSON
{
#pragma warning disable IDE1006
internal class ObjectInitializationDataJson
{
public string m_Id { get; set; }
public SerializedTypeJson m_ObjectType { get; set; }
public string m_Data { get; set; }
}
#pragma warning restore IDE1006
}

View File

@@ -0,0 +1,10 @@
namespace AddressablesTools.JSON
{
#pragma warning disable IDE1006
internal class SerializedTypeJson
{
public string m_AssemblyName { get; set; }
public string m_ClassName { get; set; }
}
#pragma warning restore IDE1006
}

View File

@@ -0,0 +1,29 @@
using System.IO;
using AssetsTools.NET.Extra;
using NitroxModel.Helper;
using NitroxServer_Subnautica.Resources.Parsers.Helper;
namespace NitroxServer_Subnautica.Resources.Parsers;
public abstract class AssetParser
{
protected static readonly string rootPath;
protected static readonly AssetsManager assetsManager;
private static readonly ThreadSafeMonoCecilTempGenerator monoGen;
static AssetParser()
{
rootPath = ResourceAssetsParser.FindDirectoryContainingResourceAssets();
assetsManager = new AssetsManager();
assetsManager.LoadClassPackage(Path.Combine(NitroxUser.AssetsPath, "Resources", "classdata.tpk"));
assetsManager.LoadClassDatabaseFromPackage("2019.4.36f1");
assetsManager.SetMonoTempGenerator(monoGen = new ThreadSafeMonoCecilTempGenerator(Path.Combine(rootPath, "Managed")));
}
public static void Dispose()
{
assetsManager.UnloadAll(true);
monoGen.Dispose();
}
}

View File

@@ -0,0 +1,28 @@
using System.IO;
using System.Runtime.InteropServices;
using AssetsTools.NET;
using AssetsTools.NET.Extra;
namespace NitroxServer_Subnautica.Resources.Parsers.Abstract;
public abstract class BundleFileParser<T> : AssetParser
{
protected static AssetsFileInstance assetFileInst;
protected static AssetsFile bundleFile;
protected BundleFileParser(string bundleName, int index)
{
string standaloneFolderName = "StandaloneWindows64";
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
standaloneFolderName = "StandaloneOSX";
}
string bundlePath = Path.Combine(ResourceAssetsParser.FindDirectoryContainingResourceAssets(), "StreamingAssets", "aa", standaloneFolderName, bundleName);
BundleFileInstance bundleFileInst = assetsManager.LoadBundleFile(bundlePath);
assetFileInst = assetsManager.LoadAssetsFileFromBundle(bundleFileInst, index, true);
bundleFile = assetFileInst.file;
}
public abstract T ParseFile();
}

View File

@@ -0,0 +1,18 @@
using System.IO;
using AssetsTools.NET;
using AssetsTools.NET.Extra;
namespace NitroxServer_Subnautica.Resources.Parsers.Abstract;
public abstract class ResourceFileParser<T> : AssetParser
{
protected static readonly AssetsFileInstance resourceInst;
protected static readonly AssetsFile resourceFile;
static ResourceFileParser()
{
resourceInst = assetsManager.LoadAssetsFile(Path.Combine(rootPath, "resources.assets"), true);
resourceFile = resourceInst.file;
}
public abstract T ParseFile();
}

View File

@@ -0,0 +1,19 @@
using AssetsTools.NET;
using AssetsTools.NET.Extra;
using NitroxServer_Subnautica.Resources.Parsers.Abstract;
using NitroxServer_Subnautica.Resources.Parsers.Helper;
namespace NitroxServer_Subnautica.Resources.Parsers;
public class EntityDistributionsParser : ResourceFileParser<string>
{
public override string ParseFile()
{
AssetFileInfo assetFileInfo = resourceFile.GetAssetInfo(assetsManager, "EntityDistributions", AssetClassID.TextAsset);
AssetTypeValueField assetValue = assetsManager.GetBaseField(resourceInst, assetFileInfo);
string json = assetValue["m_Script"].AsString;
assetsManager.UnloadAll();
return json;
}
}

View File

@@ -0,0 +1,23 @@
using AssetsTools.NET;
using NitroxModel.DataStructures.Unity;
using UnityEngine;
namespace NitroxServer_Subnautica.Resources.Parsers.Helper;
public static class AssetTypeValueFieldExtension
{
public static Vector3 ToVector3(this AssetTypeValueField valueField)
{
return new Vector3(valueField["x"].AsFloat, valueField["y"].AsFloat, valueField["z"].AsFloat);
}
public static NitroxVector3 ToNitroxVector3(this AssetTypeValueField valueField)
{
return new NitroxVector3(valueField["x"].AsFloat, valueField["y"].AsFloat, valueField["z"].AsFloat);
}
public static NitroxQuaternion ToNitroxQuaternion(this AssetTypeValueField valueField)
{
return new NitroxQuaternion(valueField["x"].AsFloat, valueField["y"].AsFloat, valueField["z"].AsFloat, valueField["w"].AsFloat);
}
}

View File

@@ -0,0 +1,147 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using AssetsTools.NET;
using AssetsTools.NET.Extra;
using NitroxModel.DataStructures.Unity;
namespace NitroxServer_Subnautica.Resources.Parsers.Helper;
public class AssetsBundleManager : AssetsManager
{
private readonly string aaRootPath;
private readonly Dictionary<AssetsFileInstance, string[]> dependenciesByAssetFileInst = new();
private ThreadSafeMonoCecilTempGenerator monoTempGenerator;
public AssetsBundleManager(string aaRootPath)
{
this.aaRootPath = aaRootPath;
}
public string CleanBundlePath(string bundlePath)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
bundlePath = bundlePath.Replace('\\', '/');
}
return aaRootPath + bundlePath.Substring(bundlePath.IndexOf('}') + 1);
}
public AssetsFileInstance LoadBundleWithDependencies(string[] bundlePaths)
{
BundleFileInstance bundleFile = LoadBundleFile(CleanBundlePath(bundlePaths[0]));
AssetsFileInstance assetFileInstance = LoadAssetsFileFromBundle(bundleFile, 0);
dependenciesByAssetFileInst[assetFileInstance] = bundlePaths;
return assetFileInstance;
}
/// <summary>
/// Copied from https://github.com/nesrak1/AssetsTools.NET#full-monobehaviour-writing-example
/// </summary>
/// <param name="inst"><see cref="AssetsFileInstance" /> instance currently used</param>
/// <param name="targetGameObjectValue"><see cref="AssetFileInfo" /> of the target GameObject</param>
/// <param name="targetClassName">Class name of the target MonoBehaviour</param>
public AssetFileInfo GetMonoBehaviourFromGameObject(AssetsFileInstance inst, AssetFileInfo targetGameObjectValue, string targetClassName)
{
//example for finding a specific script and modifying the script on a GameObject
AssetTypeValueField playerBf = GetBaseField(inst, targetGameObjectValue);
AssetTypeValueField playerComponentArr = playerBf["m_Component"]["Array"];
AssetFileInfo monoBehaviourInf = null;
//first let's search for the MonoBehaviour we want in a GameObject
foreach (AssetTypeValueField child in playerComponentArr.Children)
{
//get component info (but don't deserialize yet, loading assets we don't need is wasteful)
AssetTypeValueField childPtr = child["component"];
AssetExternal childExt = GetExtAsset(inst, childPtr, true);
AssetFileInfo childInf = childExt.info;
//skip if not MonoBehaviour
if (childInf.GetTypeId(inst.file) != (int)AssetClassID.MonoBehaviour)
{
continue;
}
//actually deserialize the MonoBehaviour asset now
AssetTypeValueField childBf = GetExtAssetSafe(inst, childPtr).baseField;
AssetTypeValueField monoScriptPtr = childBf["m_Script"];
//get MonoScript from MonoBehaviour
AssetExternal monoScriptExt = GetExtAsset(childExt.file, monoScriptPtr);
AssetTypeValueField monoScriptBf = monoScriptExt.baseField;
string className = monoScriptBf["m_ClassName"].AsString;
if (className == targetClassName)
{
monoBehaviourInf = childInf;
break;
}
}
return monoBehaviourInf;
}
public NitroxTransform GetTransformFromGameObject(AssetsFileInstance assetFileInst, AssetTypeValueField rootGameObject)
{
AssetTypeValueField componentArray = rootGameObject["m_Component"]["Array"];
AssetTypeValueField transformRef = componentArray[0]["component"];
AssetTypeValueField transformField = GetExtAsset(assetFileInst, transformRef).baseField;
return new(transformField["m_LocalPosition"].ToNitroxVector3(), transformField["m_LocalRotation"].ToNitroxQuaternion(), transformField["m_LocalScale"].ToNitroxVector3());
}
public new void SetMonoTempGenerator(IMonoBehaviourTemplateGenerator generator)
{
monoTempGenerator = (ThreadSafeMonoCecilTempGenerator)generator;
base.SetMonoTempGenerator(generator);
}
/// <summary>
/// Returns a ready to use <see cref="AssetsManager" /> with loaded <see cref="AssetsManager.classDatabase" />, <see cref="AssetsManager.classPackage" /> and
/// <see cref="IMonoBehaviourTemplateGenerator" />.
/// </summary>
public AssetsBundleManager Clone()
{
AssetsBundleManager bundleManagerInst = new(aaRootPath) { classDatabase = classDatabase, classPackage = classPackage };
bundleManagerInst.SetMonoTempGenerator(monoTempGenerator);
return bundleManagerInst;
}
/// <inheritdoc cref="AssetsManager.UnloadAll" />
public new void UnloadAll(bool unloadClassData = false)
{
if (unloadClassData)
{
monoTempGenerator.Dispose();
}
dependenciesByAssetFileInst.Clear();
base.UnloadAll(unloadClassData);
}
private AssetExternal GetExtAssetSafe(AssetsFileInstance relativeTo, AssetTypeValueField valueField)
{
string[] bundlePaths = dependenciesByAssetFileInst[relativeTo];
for (int i = 0; i < bundlePaths.Length; i++)
{
if (i != 0)
{
BundleFileInstance dependenciesBundleFile = LoadBundleFile(CleanBundlePath(bundlePaths[i]));
LoadAssetsFileFromBundle(dependenciesBundleFile, 0);
}
try
{
return GetExtAsset(relativeTo, valueField);
}
catch (Exception)
{
// ignored
}
}
throw new InvalidOperationException("Could find AssetTypeValueField in given dependencies");
}
}

View File

@@ -0,0 +1,19 @@
using AssetsTools.NET;
using AssetsTools.NET.Extra;
namespace NitroxServer_Subnautica.Resources.Parsers.Helper;
public static class AssetsFileMetadataExtension
{
public static AssetFileInfo GetAssetInfo(this AssetsFile assetsFile, AssetsManager assetsManager, string assetName, AssetClassID classID)
{
foreach (AssetFileInfo assetInfo in assetsFile.GetAssetsOfType(classID))
{
if (AssetHelper.GetAssetNameFast(assetsFile, assetsManager.classDatabase, assetInfo).Equals(assetName))
{
return assetInfo;
}
}
return null;
}
}

View File

@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Threading;
using AssetsTools.NET;
using AssetsTools.NET.Extra;
using Mono.Cecil;
namespace NitroxServer_Subnautica.Resources.Parsers.Helper;
public class ThreadSafeMonoCecilTempGenerator : IMonoBehaviourTemplateGenerator, IDisposable
{
private readonly MonoCecilTempGenerator generator;
private readonly Lock locker = new();
public ThreadSafeMonoCecilTempGenerator(string managedPath)
{
generator = new MonoCecilTempGenerator(managedPath);
}
public AssetTypeTemplateField GetTemplateField(
AssetTypeTemplateField baseField,
string assemblyName,
string nameSpace,
string className,
UnityVersion unityVersion)
{
lock (locker)
{
return generator.GetTemplateField(baseField, assemblyName, nameSpace, className, unityVersion);
}
}
public void Dispose()
{
foreach (KeyValuePair<string, AssemblyDefinition> pair in generator.loadedAssemblies)
{
pair.Value.Dispose();
}
generator.loadedAssemblies.Clear();
}
}

View File

@@ -0,0 +1,436 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using AddressablesTools;
using AddressablesTools.Catalog;
using AssetsTools.NET;
using AssetsTools.NET.Extra;
using Newtonsoft.Json;
using NitroxModel.DataStructures.Unity;
using NitroxModel.Helper;
using NitroxServer_Subnautica.Resources.Parsers.Helper;
using NitroxServer.GameLogic.Entities;
using NitroxServer.Resources;
namespace NitroxServer_Subnautica.Resources.Parsers;
public class PrefabPlaceholderGroupsParser : IDisposable
{
private readonly string prefabDatabasePath;
private readonly string aaRootPath;
private readonly AssetsBundleManager am;
private readonly ThreadSafeMonoCecilTempGenerator monoGen;
private readonly JsonSerializer serializer;
private readonly ConcurrentDictionary<string, string> classIdByRuntimeKey = new();
private readonly ConcurrentDictionary<string, string[]> addressableCatalog = new();
private readonly ConcurrentDictionary<string, PrefabPlaceholderAsset> placeholdersByClassId = new();
private readonly ConcurrentDictionary<string, PrefabPlaceholdersGroupAsset> groupsByClassId = new();
public ConcurrentDictionary<string, string[]> RandomPossibilitiesByClassId = [];
public PrefabPlaceholderGroupsParser()
{
string resourcePath = ResourceAssetsParser.FindDirectoryContainingResourceAssets();
string managedPath = Path.Combine(resourcePath, "Managed");
string streamingAssetsPath = Path.Combine(resourcePath, "StreamingAssets");
prefabDatabasePath = Path.Combine(streamingAssetsPath, "SNUnmanagedData", "prefabs.db");
aaRootPath = Path.Combine(streamingAssetsPath, "aa");
am = new AssetsBundleManager(aaRootPath);
// ReSharper disable once StringLiteralTypo)
am.LoadClassPackage(Path.Combine(NitroxUser.AssetsPath, "Resources", "classdata.tpk"));
am.LoadClassDatabaseFromPackage("2019.4.36f1");
am.SetMonoTempGenerator(monoGen = new(managedPath));
serializer = new()
{
TypeNameHandling = TypeNameHandling.Auto
};
}
public Dictionary<string, PrefabPlaceholdersGroupAsset> ParseFile()
{
// Get all prefab-classIds linked to the (partial) bundle path
Dictionary<string, string> prefabDatabase = LoadPrefabDatabase(prefabDatabasePath);
// Loading all prefabs by their classId and file paths (first the path to the prefab then the dependencies)
LoadAddressableCatalog(prefabDatabase);
string nitroxCachePath = Path.Combine(NitroxUser.AppDataPath, "Cache");
Directory.CreateDirectory(nitroxCachePath);
Dictionary<string, PrefabPlaceholdersGroupAsset> prefabPlaceholdersGroupPaths = null;
string prefabPlaceholdersGroupAssetCachePath = Path.Combine(nitroxCachePath, "PrefabPlaceholdersGroupAssetsCache.json");
if (File.Exists(prefabPlaceholdersGroupAssetCachePath))
{
Cache? cache = DeserializeCache(prefabPlaceholdersGroupAssetCachePath);
if (cache.HasValue)
{
prefabPlaceholdersGroupPaths = cache.Value.PrefabPlaceholdersGroupPaths;
RandomPossibilitiesByClassId = cache.Value.RandomPossibilitiesByClassId;
Log.Info($"Successfully loaded cache with {prefabPlaceholdersGroupPaths.Count} prefab placeholder groups and {RandomPossibilitiesByClassId.Count} random spawn behaviours.");
}
}
// Fallback solution
if (prefabPlaceholdersGroupPaths == null)
{
prefabPlaceholdersGroupPaths = MakeAndSerializeCache(prefabPlaceholdersGroupAssetCachePath);
Log.Info($"Successfully built cache with {prefabPlaceholdersGroupPaths.Count} prefab placeholder groups and {RandomPossibilitiesByClassId.Count} random spawn behaviours. Future server starts will take less time.");
}
// Select only prefabs with a PrefabPlaceholdersGroups component in the root ans link them with their dependencyPaths
// Do not remove: the internal cache list is slowing down the process more than loading a few assets again. There maybe is a better way in the new AssetToolsNetVersion but we need a byte to texture library bc ATNs sub-package is only for netstandard.
am.UnloadAll();
// Get all needed data for the filtered PrefabPlaceholdersGroups to construct PrefabPlaceholdersGroupAssets and add them to the dictionary by classId
return prefabPlaceholdersGroupPaths;
}
private Dictionary<string, PrefabPlaceholdersGroupAsset> MakeAndSerializeCache(string filePath)
{
ConcurrentDictionary<string, string[]> prefabPlaceholdersGroupPaths = GetAllPrefabPlaceholdersGroupsFast();
Dictionary<string, PrefabPlaceholdersGroupAsset> prefabPlaceholdersGroupAssets = new(GetPrefabPlaceholderGroupAssetsByGroupClassId(prefabPlaceholdersGroupPaths));
using StreamWriter stream = File.CreateText(filePath);
serializer.Serialize(stream, new Cache(prefabPlaceholdersGroupAssets, RandomPossibilitiesByClassId));
return prefabPlaceholdersGroupAssets;
}
private Cache? DeserializeCache(string filePath)
{
try
{
using StreamReader reader = File.OpenText(filePath);
return (Cache)serializer.Deserialize(reader, typeof(Cache));
}
catch (Exception exception)
{
Log.Error(exception, "An error occurred while deserializing the game Cache. Re-creating it.");
}
return null;
}
private static Dictionary<string, string> LoadPrefabDatabase(string fullFilename)
{
Dictionary<string, string> prefabFiles = new();
if (!File.Exists(fullFilename))
{
return null;
}
using FileStream input = File.OpenRead(fullFilename);
using BinaryReader binaryReader = new(input);
int num = binaryReader.ReadInt32();
for (int i = 0; i < num; i++)
{
string key = binaryReader.ReadString();
string value = binaryReader.ReadString();
prefabFiles[key] = value;
}
return prefabFiles;
}
private void LoadAddressableCatalog(Dictionary<string, string> prefabDatabase)
{
ContentCatalogData ccd = AddressablesJsonParser.FromString(File.ReadAllText(Path.Combine(aaRootPath, "catalog.json")));
Dictionary<string, string> classIdByPath = prefabDatabase.ToDictionary(m => m.Value, m => m.Key);
foreach (KeyValuePair<object, List<ResourceLocation>> entry in ccd.Resources)
{
if (entry.Key is string primaryKey && primaryKey.Length == 32 &&
classIdByPath.TryGetValue(entry.Value[0].PrimaryKey, out string classId))
{
classIdByRuntimeKey.TryAdd(primaryKey, classId);
}
}
foreach (KeyValuePair<string, string> prefabAddressable in prefabDatabase)
{
foreach (ResourceLocation resourceLocation in ccd.Resources[prefabAddressable.Value])
{
if (resourceLocation.ProviderId != "UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider")
{
continue;
}
List<ResourceLocation> resourceLocations = ccd.Resources[resourceLocation.Dependency];
if (!addressableCatalog.TryAdd(prefabAddressable.Key, resourceLocations.Select(x => x.InternalId).ToArray()))
{
throw new InvalidOperationException($"Couldn't add item to {nameof(addressableCatalog)}");
}
break;
}
}
}
/// <summary>
/// Gathers bundle paths by class id for prefab placeholder groups.
/// Also fills <see cref="RandomPossibilitiesByClassId"/>
/// </summary>
private ConcurrentDictionary<string, string[]> GetAllPrefabPlaceholdersGroupsFast()
{
ConcurrentDictionary<string, string[]> prefabPlaceholdersGroupPaths = new();
// First step is to find out about the hash of the types PrefabPlaceholdersGroup and SpawnRandom
// to be able to recognize them easily later on
byte[] prefabPlaceholdersGroupHash = [];
byte[] spawnRandomHash = [];
for (int aaIndex = 0; aaIndex < addressableCatalog.Count; aaIndex++)
{
KeyValuePair<string, string[]> keyValuePair = addressableCatalog.ElementAt(aaIndex);
BundleFileInstance bundleFile = am.LoadBundleFile(am.CleanBundlePath(keyValuePair.Value[0]));
AssetsFileInstance assetFileInstance = am.LoadAssetsFileFromBundle(bundleFile, 0);
foreach (AssetFileInfo monoScriptInfo in assetFileInstance.file.GetAssetsOfType(AssetClassID.MonoScript))
{
AssetTypeValueField monoScript = am.GetBaseField(assetFileInstance, monoScriptInfo);
switch (monoScript["m_Name"].AsString)
{
case "SpawnRandom":
spawnRandomHash = new byte[16];
for (int i = 0; i < 16; i++)
{
spawnRandomHash[i] = monoScript["m_PropertiesHash"][i].AsByte;
}
break;
case "PrefabPlaceholdersGroup":
prefabPlaceholdersGroupHash = new byte[16];
for (int i = 0; i < 16; i++)
{
prefabPlaceholdersGroupHash[i] = monoScript["m_PropertiesHash"][i].AsByte;
}
break;
}
}
if (prefabPlaceholdersGroupHash.Length > 0 && spawnRandomHash.Length > 0)
{
break;
}
}
// Now use the bundle paths and the hashes to find out which items from the catalog are important
// We fill prefabPlaceholdersGroupPaths and RandomPossibilitiesByClassId when we find objects with a SpawnRandom
Parallel.ForEach(addressableCatalog, (keyValuePair) =>
{
string[] assetPaths = keyValuePair.Value;
AssetsBundleManager bundleManagerInst = am.Clone();
AssetsFileInstance assetFileInstance = bundleManagerInst.LoadBundleWithDependencies(assetPaths);
foreach (TypeTreeType typeTreeType in assetFileInstance.file.Metadata.TypeTreeTypes)
{
if (typeTreeType.TypeId != (int)AssetClassID.MonoBehaviour)
{
continue;
}
if (typeTreeType.TypeHash.data.SequenceEqual(prefabPlaceholdersGroupHash))
{
prefabPlaceholdersGroupPaths.TryAdd(keyValuePair.Key, keyValuePair.Value);
break;
}
else if (typeTreeType.TypeHash.data.SequenceEqual(spawnRandomHash))
{
AssetsFileInstance assetFileInst = bundleManagerInst.LoadBundleWithDependencies(assetPaths);
GetPrefabGameObjectInfoFromBundle(bundleManagerInst, assetFileInst, out AssetFileInfo prefabGameObjectInfo);
AssetFileInfo spawnRandomInfo = bundleManagerInst.GetMonoBehaviourFromGameObject(assetFileInst, prefabGameObjectInfo, "SpawnRandom");
// See SpawnRandom.Start
AssetTypeValueField spawnRandom = bundleManagerInst.GetBaseField(assetFileInst, spawnRandomInfo);
List<string> classIds = [];
foreach (AssetTypeValueField assetReference in spawnRandom["assetReferences"])
{
classIds.Add(classIdByRuntimeKey[assetReference["m_AssetGUID"].AsString]);
}
RandomPossibilitiesByClassId.TryAdd(keyValuePair.Key, [.. classIds]);
break;
}
}
bundleManagerInst.UnloadAll();
});
return prefabPlaceholdersGroupPaths;
}
private ConcurrentDictionary<string, PrefabPlaceholdersGroupAsset> GetPrefabPlaceholderGroupAssetsByGroupClassId(ConcurrentDictionary<string, string[]> prefabPlaceholdersGroupPaths)
{
ConcurrentDictionary<string, PrefabPlaceholdersGroupAsset> prefabPlaceholderGroupsByGroupClassId = new();
Parallel.ForEach(prefabPlaceholdersGroupPaths, (keyValuePair) =>
{
AssetsBundleManager bundleManagerInst = am.Clone();
AssetsFileInstance assetFileInst = bundleManagerInst.LoadBundleWithDependencies(keyValuePair.Value);
PrefabPlaceholdersGroupAsset prefabPlaceholderGroup = GetAndCachePrefabPlaceholdersGroupOfBundle(bundleManagerInst, assetFileInst, keyValuePair.Key);
bundleManagerInst.UnloadAll();
if (!prefabPlaceholderGroupsByGroupClassId.TryAdd(keyValuePair.Key, prefabPlaceholderGroup))
{
throw new InvalidOperationException($"Couldn't add item to {nameof(prefabPlaceholderGroupsByGroupClassId)}");
}
});
return prefabPlaceholderGroupsByGroupClassId;
}
private static void GetPrefabGameObjectInfoFromBundle(AssetsBundleManager amInst, AssetsFileInstance assetFileInst, out AssetFileInfo prefabGameObjectInfo)
{
//Get the main asset with "m_Container" of the "AssetBundle-asset" inside the bundle
AssetFileInfo assetBundleInfo = assetFileInst.file.Metadata.GetAssetInfo(1);
AssetTypeValueField assetBundleValue = amInst.GetBaseField(assetFileInst, assetBundleInfo);
AssetTypeValueField assetBundleContainer = assetBundleValue["m_Container.Array"];
long rootAssetPathId = assetBundleContainer.Children[0][1]["asset.m_PathID"].AsLong;
prefabGameObjectInfo = assetFileInst.file.Metadata.GetAssetInfo(rootAssetPathId);
}
private PrefabPlaceholdersGroupAsset GetAndCachePrefabPlaceholdersGroupOfBundle(AssetsBundleManager amInst, AssetsFileInstance assetFileInst, string classId)
{
GetPrefabGameObjectInfoFromBundle(amInst, assetFileInst, out AssetFileInfo prefabGameObjectInfo);
return GetAndCachePrefabPlaceholdersGroupGroup(amInst, assetFileInst, prefabGameObjectInfo, classId);
}
private PrefabPlaceholdersGroupAsset GetAndCachePrefabPlaceholdersGroupGroup(AssetsBundleManager amInst, AssetsFileInstance assetFileInst, AssetFileInfo rootGameObjectInfo, string classId)
{
if (!string.IsNullOrEmpty(classId) && groupsByClassId.TryGetValue(classId, out PrefabPlaceholdersGroupAsset cachedGroup))
{
return cachedGroup;
}
AssetFileInfo prefabPlaceholdersGroupInfo = amInst.GetMonoBehaviourFromGameObject(assetFileInst, rootGameObjectInfo, "PrefabPlaceholdersGroup");
if (prefabPlaceholdersGroupInfo == null)
{
return default;
}
AssetTypeValueField prefabPlaceholdersGroupScript = amInst.GetBaseField(assetFileInst, prefabPlaceholdersGroupInfo);
List<AssetTypeValueField> prefabPlaceholdersOnGroup = prefabPlaceholdersGroupScript["prefabPlaceholders"].Children;
IPrefabAsset[] prefabPlaceholders = new IPrefabAsset[prefabPlaceholdersOnGroup.Count];
for (int index = 0; index < prefabPlaceholdersOnGroup.Count; index++)
{
AssetTypeValueField prefabPlaceholderPtr = prefabPlaceholdersOnGroup[index];
AssetTypeValueField prefabPlaceholder = amInst.GetExtAsset(assetFileInst, prefabPlaceholderPtr).baseField;
AssetTypeValueField gameObjectPtr = prefabPlaceholder["m_GameObject"];
AssetTypeValueField gameObjectField = amInst.GetExtAsset(assetFileInst, gameObjectPtr).baseField;
NitroxTransform transform = amInst.GetTransformFromGameObject(assetFileInst, gameObjectField);
IPrefabAsset asset = GetAndCacheAsset(amInst, prefabPlaceholder["prefabClassId"].AsString);
asset.Transform = transform;
prefabPlaceholders[index] = asset;
}
PrefabPlaceholdersGroupAsset prefabPlaceholdersGroup = new(classId, prefabPlaceholders);
AssetTypeValueField rootGameObjectField = amInst.GetBaseField(assetFileInst, rootGameObjectInfo);
NitroxTransform groupTransform = amInst.GetTransformFromGameObject(assetFileInst, rootGameObjectField);
prefabPlaceholdersGroup.Transform = groupTransform;
groupsByClassId[classId] = prefabPlaceholdersGroup;
return prefabPlaceholdersGroup;
}
private IPrefabAsset GetAndCacheAsset(AssetsBundleManager amInst, string classId)
{
if (string.IsNullOrEmpty(classId))
{
return default;
}
if (groupsByClassId.TryGetValue(classId, out PrefabPlaceholdersGroupAsset cachedGroup))
{
return cachedGroup;
}
else if (placeholdersByClassId.TryGetValue(classId, out PrefabPlaceholderAsset cachedPlaceholder))
{
return cachedPlaceholder;
}
if (!addressableCatalog.TryGetValue(classId, out string[] assetPaths))
{
Log.Error($"Couldn't get PrefabPlaceholder with classId: {classId}");
return default;
}
AssetsFileInstance assetFileInst = amInst.LoadBundleWithDependencies(assetPaths);
GetPrefabGameObjectInfoFromBundle(amInst, assetFileInst, out AssetFileInfo prefabGameObjectInfo);
AssetFileInfo placeholdersGroupInfo = amInst.GetMonoBehaviourFromGameObject(assetFileInst, prefabGameObjectInfo, "PrefabPlaceholdersGroup");
if (placeholdersGroupInfo != null)
{
PrefabPlaceholdersGroupAsset groupAsset = GetAndCachePrefabPlaceholdersGroupOfBundle(amInst, assetFileInst, classId);
groupsByClassId[classId] = groupAsset;
return groupAsset;
}
AssetFileInfo spawnRandomInfo = amInst.GetMonoBehaviourFromGameObject(assetFileInst, prefabGameObjectInfo, "SpawnRandom");
if (spawnRandomInfo != null)
{
// See SpawnRandom.Start
AssetTypeValueField spawnRandom = amInst.GetBaseField(assetFileInst, spawnRandomInfo);
List<string> classIds = new();
foreach (AssetTypeValueField assetReference in spawnRandom["assetReferences"])
{
classIds.Add(classIdByRuntimeKey[assetReference["m_AssetGUID"].AsString]);
}
return new PrefabPlaceholderRandomAsset(classIds);
}
AssetFileInfo databoxSpawnerInfo = amInst.GetMonoBehaviourFromGameObject(assetFileInst, prefabGameObjectInfo, "DataboxSpawner");
if (databoxSpawnerInfo != null)
{
// NB: This spawning should be cancelled if the techType is from a known tech
// But it doesn't matter if we still spawn it so we do so.
// See DataboxSpawner.Start
AssetTypeValueField databoxSpawner = amInst.GetBaseField(assetFileInst, databoxSpawnerInfo);
string runtimeKey = databoxSpawner["databoxPrefabReference"]["m_AssetGUID"].AsString;
PrefabPlaceholderAsset databoxAsset = new(classIdByRuntimeKey[runtimeKey]);
placeholdersByClassId[classId] = databoxAsset;
return databoxAsset;
}
AssetFileInfo entitySlotInfo = amInst.GetMonoBehaviourFromGameObject(assetFileInst, prefabGameObjectInfo, "EntitySlot");
NitroxEntitySlot? nitroxEntitySlot = null;
if (entitySlotInfo != null)
{
AssetTypeValueField entitySlot = amInst.GetBaseField(assetFileInst, entitySlotInfo);
string biomeType = ((BiomeType)entitySlot["biomeType"].AsInt).ToString();
List<string> allowedTypes = [];
foreach (AssetTypeValueField allowedType in entitySlot["allowedTypes"])
{
allowedTypes.Add(((EntitySlot.Type)allowedType.AsInt).ToString());
}
nitroxEntitySlot = new NitroxEntitySlot(biomeType, allowedTypes);
}
PrefabPlaceholderAsset prefabPlaceholderAsset = new(classId, nitroxEntitySlot);
placeholdersByClassId[classId] = prefabPlaceholderAsset;
return prefabPlaceholderAsset;
}
public void Dispose()
{
monoGen.Dispose();
am.UnloadAll(true);
}
record struct Cache(Dictionary<string, PrefabPlaceholdersGroupAsset> PrefabPlaceholdersGroupPaths, ConcurrentDictionary<string, string[]> RandomPossibilitiesByClassId);
}

View File

@@ -0,0 +1,51 @@
using AssetsTools.NET;
using AssetsTools.NET.Extra;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.Helper;
using NitroxServer_Subnautica.Resources.Parsers.Abstract;
using NitroxServer_Subnautica.Resources.Parsers.Helper;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
namespace NitroxServer_Subnautica.Resources.Parsers;
public class RandomStartParser : BundleFileParser<RandomStartGenerator>
{
public RandomStartParser() : base("essentials.unity_0ee8dd89ed55f05bc38a09cc77137d4e.bundle", 0) { }
public override RandomStartGenerator ParseFile()
{
AssetFileInfo assetFile = bundleFile.GetAssetInfo(assetsManager, "RandomStart", AssetClassID.Texture2D);
AssetTypeValueField textureValueField = assetsManager.GetBaseField(assetFileInst, assetFile);
TextureFile textureFile = TextureFile.ReadTextureFile(textureValueField);
byte[] texDat = textureFile.GetTextureData(assetFileInst);
assetsManager.UnloadAll();
if (texDat is not { Length: > 0 })
{
return null;
}
Image<Bgra32> texture = Image.LoadPixelData<Bgra32>(texDat, textureFile.m_Width, textureFile.m_Height);
texture.Mutate(x => x.Flip(FlipMode.Vertical));
return new RandomStartGenerator(new PixelProvider(texture));
}
private class PixelProvider : RandomStartGenerator.IPixelProvider
{
private readonly Image<Bgra32> texture;
public PixelProvider(Image<Bgra32> texture)
{
Validate.NotNull(texture);
this.texture = texture;
}
public byte GetRed(int x, int y) => texture[x, y].R;
public byte GetGreen(int x, int y) => texture[x, y].G;
public byte GetBlue(int x, int y) => texture[x, y].B;
}
}

View File

@@ -0,0 +1,37 @@
using System.Collections.Generic;
using AssetsTools.NET;
using AssetsTools.NET.Extra;
using NitroxServer_Subnautica.Resources.Parsers.Abstract;
using NitroxServer_Subnautica.Resources.Parsers.Helper;
using UWE;
namespace NitroxServer_Subnautica.Resources.Parsers;
public class WorldEntityInfoParser : ResourceFileParser<Dictionary<string, WorldEntityInfo>>
{
public override Dictionary<string, WorldEntityInfo> ParseFile()
{
Dictionary<string, WorldEntityInfo> worldEntitiesByClassId = new();
AssetFileInfo assetFileInfo = resourceFile.GetAssetInfo(assetsManager, "WorldEntityData", AssetClassID.MonoBehaviour);
AssetTypeValueField assetValue = assetsManager.GetBaseField(resourceInst, assetFileInfo);
foreach (AssetTypeValueField info in assetValue["infos"])
{
WorldEntityInfo entityData = new()
{
classId = info["classId"].AsString,
techType = (TechType)info["techType"].AsInt,
slotType = (EntitySlot.Type)info["slotType"].AsInt,
prefabZUp = info["prefabZUp"].AsBool,
cellLevel = (LargeWorldEntity.CellLevel)info["cellLevel"].AsInt,
localScale = info["localScale"].ToVector3()
};
worldEntitiesByClassId.Add(entityData.classId, entityData);
}
assetsManager.UnloadAll();
return worldEntitiesByClassId;
}
}

View File

@@ -0,0 +1,26 @@
using System.Collections.Generic;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.Helper;
using NitroxServer.Resources;
using UWE;
namespace NitroxServer_Subnautica.Resources;
public class ResourceAssets
{
public Dictionary<string, WorldEntityInfo> WorldEntitiesByClassId { get; init; } = new();
public string LootDistributionsJson { get; init; } = "";
public Dictionary<string, PrefabPlaceholdersGroupAsset> PrefabPlaceholdersGroupsByGroupClassId { get; init; } = new();
public Dictionary<string, string[]> RandomPossibilitiesByClassId { get; init; }
public RandomStartGenerator NitroxRandom { get; init; }
public static void ValidateMembers(ResourceAssets resourceAssets)
{
Validate.NotNull(resourceAssets);
Validate.IsTrue(resourceAssets.WorldEntitiesByClassId.Count > 0);
Validate.IsTrue(resourceAssets.LootDistributionsJson != "");
Validate.IsTrue(resourceAssets.PrefabPlaceholdersGroupsByGroupClassId.Count > 0);
Validate.IsTrue(resourceAssets.RandomPossibilitiesByClassId.Count > 0);
Validate.NotNull(resourceAssets.NitroxRandom);
}
}

View File

@@ -0,0 +1,62 @@
using System.IO;
using NitroxModel;
using NitroxModel.Helper;
using NitroxServer_Subnautica.Resources.Parsers;
namespace NitroxServer_Subnautica.Resources;
public static class ResourceAssetsParser
{
private static ResourceAssets resourceAssets;
public static ResourceAssets Parse()
{
if (resourceAssets != null)
{
return resourceAssets;
}
using (PrefabPlaceholderGroupsParser prefabPlaceholderGroupsParser = new())
{
resourceAssets = new ResourceAssets
{
WorldEntitiesByClassId = new WorldEntityInfoParser().ParseFile(),
LootDistributionsJson = new EntityDistributionsParser().ParseFile(),
PrefabPlaceholdersGroupsByGroupClassId = prefabPlaceholderGroupsParser.ParseFile(),
NitroxRandom = new RandomStartParser().ParseFile(),
RandomPossibilitiesByClassId = new(prefabPlaceholderGroupsParser.RandomPossibilitiesByClassId)
};
}
AssetParser.Dispose();
ResourceAssets.ValidateMembers(resourceAssets);
return resourceAssets;
}
public static string FindDirectoryContainingResourceAssets()
{
string subnauticaPath = NitroxUser.GamePath;
if (string.IsNullOrEmpty(subnauticaPath))
{
throw new DirectoryNotFoundException("Could not locate Subnautica installation directory for resource parsing.");
}
if (File.Exists(Path.Combine(subnauticaPath, GameInfo.Subnautica.DataFolder, "resources.assets")))
{
return Path.Combine(subnauticaPath, GameInfo.Subnautica.DataFolder);
}
if (File.Exists(Path.Combine("..", "resources.assets"))) // SubServer => Subnautica/Subnautica_Data/SubServer
{
return Path.GetFullPath(Path.Combine(".."));
}
if (File.Exists(Path.Combine("..", GameInfo.Subnautica.DataFolder, "resources.assets"))) // SubServer => Subnautica/SubServer
{
return Path.GetFullPath(Path.Combine("..", GameInfo.Subnautica.DataFolder));
}
if (File.Exists("resources.assets")) // SubServer/* => Subnautica/Subnautica_Data/
{
return Directory.GetCurrentDirectory();
}
throw new FileNotFoundException("Make sure resources.assets is in current or parent directory and readable.");
}
}

View File

@@ -0,0 +1,11 @@
using NitroxServer.Serialization;
namespace NitroxServer_Subnautica.Serialization
{
class SubnauticaServerJsonSerializer : ServerJsonSerializer
{
public SubnauticaServerJsonSerializer()
{
}
}
}

View File

@@ -0,0 +1,30 @@
using NitroxModel.DataStructures.Unity;
using NitroxModel_Subnautica.DataStructures.Surrogates;
using NitroxServer.Serialization;
using UnityEngine;
namespace NitroxServer_Subnautica.Serialization
{
class SubnauticaServerProtoBufSerializer : ServerProtoBufSerializer
{
public SubnauticaServerProtoBufSerializer(params string[] assemblies) : base(assemblies)
{
RegisterHardCodedTypes();
}
// Register here all hard coded types, that come from NitroxModel-Subnautica or NitroxServer-Subnautica
private void RegisterHardCodedTypes()
{
Model.Add(typeof(Light), true);
Model.Add(typeof(BoxCollider), true);
Model.Add(typeof(SphereCollider), true);
Model.Add(typeof(MeshCollider), true);
Model.Add(typeof(Vector3), false).SetSurrogate(typeof(Vector3Surrogate));
Model.Add(typeof(NitroxVector3), false).SetSurrogate(typeof(Vector3Surrogate));
Model.Add(typeof(Quaternion), false).SetSurrogate(typeof(QuaternionSurrogate));
Model.Add(typeof(NitroxQuaternion), false).SetSurrogate(typeof(QuaternionSurrogate));
Model.Add(typeof(Transform), false).SetSurrogate(typeof(NitroxTransform));
Model.Add(typeof(GameObject), false).SetSurrogate(typeof(NitroxServer.UnityStubs.GameObject));
}
}
}

View File

@@ -0,0 +1,67 @@
using Autofac;
using NitroxModel;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.GameLogic.FMOD;
using NitroxModel.Helper;
using NitroxModel_Subnautica.DataStructures.GameLogic.Entities;
using NitroxModel_Subnautica.Helper;
using NitroxServer;
using NitroxServer.GameLogic;
using NitroxServer.GameLogic.Entities;
using NitroxServer.GameLogic.Entities.Spawning;
using NitroxServer.Resources;
using NitroxServer.Serialization;
using NitroxServer_Subnautica.GameLogic;
using NitroxServer_Subnautica.GameLogic.Entities;
using NitroxServer_Subnautica.GameLogic.Entities.Spawning;
using NitroxServer_Subnautica.Resources;
using NitroxServer_Subnautica.Serialization;
namespace NitroxServer_Subnautica
{
public class SubnauticaServerAutoFacRegistrar : ServerAutoFacRegistrar
{
public override void RegisterDependencies(ContainerBuilder containerBuilder)
{
base.RegisterDependencies(containerBuilder);
containerBuilder.RegisterType<SimulationWhitelist>()
.As<ISimulationWhitelist>()
.SingleInstance();
containerBuilder.Register(c => new SubnauticaServerProtoBufSerializer(
"Assembly-CSharp",
"Assembly-CSharp-firstpass",
"NitroxModel",
"NitroxModel-Subnautica"))
.As<ServerProtoBufSerializer, IServerSerializer>()
.SingleInstance();
containerBuilder.Register(c => new SubnauticaServerJsonSerializer())
.As<ServerJsonSerializer, IServerSerializer>()
.SingleInstance();
containerBuilder.RegisterType<SubnauticaEntitySpawnPointFactory>().As<EntitySpawnPointFactory>().SingleInstance();
ResourceAssets resourceAssets = ResourceAssetsParser.Parse();
containerBuilder.Register(c => resourceAssets).SingleInstance();
containerBuilder.Register(c => resourceAssets.WorldEntitiesByClassId).SingleInstance();
containerBuilder.Register(c => resourceAssets.PrefabPlaceholdersGroupsByGroupClassId).SingleInstance();
containerBuilder.Register(c => resourceAssets.NitroxRandom).SingleInstance();
containerBuilder.RegisterType<SubnauticaUweWorldEntityFactory>().As<IUweWorldEntityFactory>().SingleInstance();
SubnauticaUwePrefabFactory prefabFactory = new SubnauticaUwePrefabFactory(resourceAssets.LootDistributionsJson);
containerBuilder.Register(c => prefabFactory).As<IUwePrefabFactory>().SingleInstance();
containerBuilder.RegisterType<SubnauticaEntityBootstrapperManager>()
.As<IEntityBootstrapperManager>()
.SingleInstance();
containerBuilder.RegisterType<SubnauticaMap>().As<IMap>().InstancePerLifetimeScope();
containerBuilder.RegisterType<EntityRegistry>().AsSelf().InstancePerLifetimeScope();
containerBuilder.RegisterType<SubnauticaWorldModifier>().As<IWorldModifier>().InstancePerLifetimeScope();
containerBuilder.Register(c => FMODWhitelist.Load(GameInfo.Subnautica)).InstancePerLifetimeScope();
containerBuilder.Register(_ => new RandomSpawnSpoofer(resourceAssets.RandomPossibilitiesByClassId))
.SingleInstance();
}
}
}