Files
Nitrox/NitroxServer/Serialization/BatchCellsParser.cs
2025-07-06 00:23:46 +02:00

428 lines
13 KiB
C#

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<string, Type> surrogateTypes;
public BatchCellsParser(EntitySpawnPointFactory entitySpawnPointFactory, ServerProtoBufSerializer serializer)
{
this.entitySpawnPointFactory = entitySpawnPointFactory;
this.serializer = serializer;
surrogateTypes = new Dictionary<string, Type>
{
{ "UnityEngine.Transform", typeof(NitroxTransform) },
{ "UnityEngine.Vector3", typeof(NitroxVector3) },
{ "UnityEngine.Quaternion", typeof(NitroxQuaternion) }
};
}
public List<EntitySpawnPoint> ParseBatchData(NitroxInt3 batchId)
{
List<EntitySpawnPoint> spawnPoints = new List<EntitySpawnPoint>();
ParseFile(batchId, "CellsCache", "baked-", "", spawnPoints);
return spawnPoints;
}
public void ParseFile(NitroxInt3 batchId, string pathPrefix, string prefix, string suffix, List<EntitySpawnPoint> 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<EntitySpawnPoint> spawnPoints)
{
using Stream stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
CellsFileHeader cellsFileHeader = serializer.Deserialize<CellsFileHeader>(stream);
for (int cellCounter = 0; cellCounter < cellsFileHeader.NumCells; cellCounter++)
{
CellHeaderEx cellHeader = serializer.Deserialize<CellHeaderEx>(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<EntitySpawnPoint> spawnPoints, out bool wasLegacy)
{
wasLegacy = false;
if (data.Length == 0)
{
return;
}
using Stream stream = new MemoryStream(data);
StreamHeader header = serializer.Deserialize<StreamHeader>(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<EntitySpawnPoint> spawnPoints)
{
LoopHeader gameObjectCount = serializer.Deserialize<LoopHeader>(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<NitroxTransform>();
spawnPoints.AddRange(entitySpawnPointFactory.From(absoluteEntityCell, transform, gameObject));
}
}
}
private GameObject DeserializeGameObject(Stream stream)
{
return new(serializer.Deserialize<GameObjectData>(stream));
}
private void DeserializeComponents(Stream stream, GameObject gameObject)
{
gameObject.SerializedComponents.Clear();
LoopHeader components = serializer.Deserialize<LoopHeader>(stream);
for (int componentCounter = 0; componentCounter < components.Count; componentCounter++)
{
ComponentHeader componentHeader = serializer.Deserialize<ComponentHeader>(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);
}
}
}