using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.Serialization; using NitroxModel.DataStructures; using NitroxModel.DataStructures.GameLogic; using NitroxModel.DataStructures.Unity; using NitroxServer.GameLogic.Entities.Spawning; using NitroxServer.UnityStubs; using ProtoBufNet; namespace NitroxServer.Serialization { /** * Parses the files in build18 in the format of batch-cells-x-y-z-slot-type.bin * These files contain serialized GameObjects with EntitySlot components. These * represent areas that entities (creatures, objects) can spawn within the world. * This class consolidates the gameObject, entitySlot, and cellHeader data to * create EntitySpawnPoint objects. */ public class BatchCellsParser { private readonly EntitySpawnPointFactory entitySpawnPointFactory; private readonly ServerProtoBufSerializer serializer; private readonly Dictionary surrogateTypes; public BatchCellsParser(EntitySpawnPointFactory entitySpawnPointFactory, ServerProtoBufSerializer serializer) { this.entitySpawnPointFactory = entitySpawnPointFactory; this.serializer = serializer; surrogateTypes = new Dictionary { { "UnityEngine.Transform", typeof(NitroxTransform) }, { "UnityEngine.Vector3", typeof(NitroxVector3) }, { "UnityEngine.Quaternion", typeof(NitroxQuaternion) } }; } public List ParseBatchData(NitroxInt3 batchId) { List spawnPoints = new List(); ParseFile(batchId, "CellsCache", "baked-", "", spawnPoints); return spawnPoints; } public void ParseFile(NitroxInt3 batchId, string pathPrefix, string prefix, string suffix, List spawnPoints) { string subnauticaPath = NitroxUser.GamePath; if (string.IsNullOrEmpty(subnauticaPath)) { return; } string path = Path.Combine(subnauticaPath, GameInfo.Subnautica.DataFolder, "StreamingAssets", "SNUnmanagedData", "Build18"); string fileName = Path.Combine(path, pathPrefix, $"{prefix}batch-cells-{batchId.X}-{batchId.Y}-{batchId.Z}{suffix}.bin"); if (!File.Exists(fileName)) { return; } ParseCacheCells(batchId, fileName, spawnPoints); } /** * It is suspected that 'cache' is a misnomer carried over from when UWE was actually doing procedurally * generated worlds. In the final release, this 'cache' has simply been baked into a final version that * we can parse. */ private void ParseCacheCells(NitroxInt3 batchId, string fileName, List spawnPoints) { using Stream stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); CellsFileHeader cellsFileHeader = serializer.Deserialize(stream); for (int cellCounter = 0; cellCounter < cellsFileHeader.NumCells; cellCounter++) { CellHeaderEx cellHeader = serializer.Deserialize(stream); byte[] serialData = new byte[cellHeader.DataLength]; stream.ReadStreamExactly(serialData, serialData.Length); ParseGameObjectsWithHeader(serialData, batchId, cellHeader.CellId, cellHeader.Level, spawnPoints, out bool wasLegacy); if (!wasLegacy) { byte[] legacyData = new byte[cellHeader.LegacyDataLength]; stream.ReadStreamExactly(legacyData, legacyData.Length); ParseGameObjectsWithHeader(legacyData, batchId, cellHeader.CellId, cellHeader.Level, spawnPoints, out _); byte[] waiterData = new byte[cellHeader.WaiterDataLength]; stream.ReadStreamExactly(waiterData, waiterData.Length); ParseGameObjectsFromStream(new MemoryStream(waiterData), batchId, cellHeader.CellId, cellHeader.Level, spawnPoints); } } } private void ParseGameObjectsWithHeader(byte[] data, NitroxInt3 batchId, NitroxInt3 cellId, int level, List spawnPoints, out bool wasLegacy) { wasLegacy = false; if (data.Length == 0) { return; } using Stream stream = new MemoryStream(data); StreamHeader header = serializer.Deserialize(stream); if (ReferenceEquals(header, null)) { return; } ParseGameObjectsFromStream(stream, batchId, cellId, level, spawnPoints); wasLegacy = header.Version < 9; } private void ParseGameObjectsFromStream(Stream stream, NitroxInt3 batchId, NitroxInt3 cellId, int level, List spawnPoints) { LoopHeader gameObjectCount = serializer.Deserialize(stream); for (int goCounter = 0; goCounter < gameObjectCount.Count; goCounter++) { GameObject gameObject = DeserializeGameObject(stream); DeserializeComponents(stream, gameObject); // If it is an "Empty" GameObject, we need it to have serialized components if (!gameObject.CreateEmptyObject || gameObject.SerializedComponents.Count > 0) { AbsoluteEntityCell absoluteEntityCell = new AbsoluteEntityCell(batchId, cellId, level); NitroxTransform transform = gameObject.GetComponent(); spawnPoints.AddRange(entitySpawnPointFactory.From(absoluteEntityCell, transform, gameObject)); } } } private GameObject DeserializeGameObject(Stream stream) { return new(serializer.Deserialize(stream)); } private void DeserializeComponents(Stream stream, GameObject gameObject) { gameObject.SerializedComponents.Clear(); LoopHeader components = serializer.Deserialize(stream); for (int componentCounter = 0; componentCounter < components.Count; componentCounter++) { ComponentHeader componentHeader = serializer.Deserialize(stream); if (!surrogateTypes.TryGetValue(componentHeader.TypeName, out Type type)) { type = AppDomain.CurrentDomain.GetAssemblies() .Select(a => a.GetType(componentHeader.TypeName)) .FirstOrDefault(t => t != null); } Validate.NotNull(type, $"No type or surrogate found for {componentHeader.TypeName}!"); #if NET5_0_OR_GREATER object component = System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject(type); #else object component = System.Runtime.Serialization.FormatterServices.GetUninitializedObject(type); #endif long startPosition = stream.Position; serializer.Deserialize(stream, component, type); gameObject.AddComponent(component, type); // SerializedComponents only matter if this is an "Empty" GameObject if (gameObject.CreateEmptyObject && !type.Name.Equals(nameof(NitroxTransform)) && !type.Name.Equals("LargeWorldEntity")) { byte[] data = new byte[(int)(stream.Position - startPosition)]; stream.Position = startPosition; stream.ReadStreamExactly(data, data.Length); SerializedComponent serializedComponent = new(componentHeader.TypeName, componentHeader.IsEnabled, data); gameObject.SerializedComponents.Add(serializedComponent); } } } } [ProtoContract] public class CellsFileHeader { public override string ToString() { return string.Format("(version={0}, numCells={1})", Version, NumCells); } [ProtoMember(1)] public int Version; [ProtoMember(2)] public int NumCells; } [ProtoContract] public class CellHeader { public override string ToString() { return $"(cellId={CellId}, level={Level})"; } [ProtoMember(1)] public NitroxInt3 CellId; [ProtoMember(2)] public int Level; } [ProtoContract] public class CellHeaderEx { public override string ToString() { return string.Format("(cellId={0}, level={1}, dataLength={2}, legacyDataLength={3}, waiterDataLength={4})", new object[] { CellId, Level, DataLength, LegacyDataLength, WaiterDataLength }); } [ProtoMember(1)] public NitroxInt3 CellId; [ProtoMember(2)] public int Level; [ProtoMember(3)] public int DataLength; [ProtoMember(4)] public int LegacyDataLength; [ProtoMember(5)] public int WaiterDataLength; // There's no point in spawning allowSpawnRestrictions as SpawnRestrictionEnforcer doesn't load any restrictions } [ProtoContract] public class StreamHeader { [ProtoMember(1)] public int Signature { get; set; } [ProtoMember(2)] public int Version { get; set; } public void Reset() { Signature = 0; Version = 0; } public override string ToString() { return string.Format("(UniqueIdentifier={0}, Version={1})", Signature, Version); } } [ProtoContract] public class LoopHeader { [ProtoMember(1)] public int Count { get; set; } public void Reset() { Count = 0; } public override string ToString() { return string.Format("(Count={0})", Count); } } [ProtoContract] public class GameObjectData { [ProtoMember(1)] public bool CreateEmptyObject { get; set; } [ProtoMember(2)] public bool IsActive { get; set; } [ProtoMember(3)] public int Layer { get; set; } [ProtoMember(4)] public string Tag { get; set; } [ProtoMember(6)] public string Id { get; set; } [ProtoMember(7)] public string ClassId { get; set; } [ProtoMember(8)] public string Parent { get; set; } [ProtoMember(9)] public bool OverridePrefab { get; set; } [ProtoMember(10)] public bool MergeObject { get; set; } public void Reset() { CreateEmptyObject = false; IsActive = false; Layer = 0; Tag = null; Id = null; ClassId = null; Parent = null; OverridePrefab = false; MergeObject = false; } public override string ToString() { return string.Format("(CreateEmptyObject={0}, IsActive={1}, Layer={2}, Tag={3}, Id={4}, ClassId={5}, Parent={6}, OverridePrefab={7}, MergeObject={8})", new object[] { CreateEmptyObject, IsActive, Layer, Tag, Id, ClassId, Parent, OverridePrefab, MergeObject }); } } [ProtoContract] public class ComponentHeader { [ProtoMember(1)] public string TypeName { get; set; } [ProtoMember(2)] public bool IsEnabled { get; set; } public void Reset() { TypeName = null; IsEnabled = false; } public override string ToString() { return string.Format("(TypeName={0}, IsEnabled={1})", TypeName, IsEnabled); } } }