first commit
This commit is contained in:
427
NitroxServer/Serialization/BatchCellsParser.cs
Normal file
427
NitroxServer/Serialization/BatchCellsParser.cs
Normal file
@@ -0,0 +1,427 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
14
NitroxServer/Serialization/IServerSerializer.cs
Normal file
14
NitroxServer/Serialization/IServerSerializer.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.IO;
|
||||
|
||||
namespace NitroxServer.Serialization;
|
||||
|
||||
public interface IServerSerializer
|
||||
{
|
||||
string FileEnding { get; }
|
||||
|
||||
void Serialize(Stream stream, object o);
|
||||
void Serialize(string filePath, object o);
|
||||
|
||||
T Deserialize<T>(Stream stream);
|
||||
T Deserialize<T>(string filePath);
|
||||
}
|
21
NitroxServer/Serialization/Json/AttributeContractResolver.cs
Normal file
21
NitroxServer/Serialization/Json/AttributeContractResolver.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace NitroxServer.Serialization.Json;
|
||||
|
||||
public class AttributeContractResolver : DefaultContractResolver
|
||||
{
|
||||
//IDictionary to JsonArray
|
||||
protected override JsonContract CreateContract(Type objectType)
|
||||
{
|
||||
if (objectType.GetInterfaces().Any(i => i == typeof(IDictionary) || i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IDictionary<,>)))
|
||||
{
|
||||
return base.CreateArrayContract(objectType);
|
||||
}
|
||||
|
||||
return base.CreateContract(objectType);
|
||||
}
|
||||
}
|
19
NitroxServer/Serialization/Json/NitroxIdConverter.cs
Normal file
19
NitroxServer/Serialization/Json/NitroxIdConverter.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using NitroxModel.DataStructures;
|
||||
|
||||
namespace NitroxServer.Serialization.Json
|
||||
{
|
||||
public class NitroxIdConverter : JsonConverter<NitroxId>
|
||||
{
|
||||
public override void WriteJson(JsonWriter writer, NitroxId value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteValue(value.ToString());
|
||||
}
|
||||
|
||||
public override NitroxId ReadJson(JsonReader reader, Type objectType, NitroxId existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
return reader.Value == null ? null : new NitroxId((string)reader.Value);
|
||||
}
|
||||
}
|
||||
}
|
18
NitroxServer/Serialization/Json/TechTypeConverter.cs
Normal file
18
NitroxServer/Serialization/Json/TechTypeConverter.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using NitroxModel.DataStructures.GameLogic;
|
||||
|
||||
namespace NitroxServer.Serialization.Json
|
||||
{
|
||||
public class TechTypeConverter : JsonConverter<NitroxTechType>
|
||||
{
|
||||
public override void WriteJson(JsonWriter writer, NitroxTechType value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteValue(value.Name);
|
||||
}
|
||||
public override NitroxTechType ReadJson(JsonReader reader, Type objectType, NitroxTechType existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
return reader.Value == null ? null : new NitroxTechType((string)reader.Value);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,48 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace NitroxServer.Serialization.SaveDataUpgrades
|
||||
{
|
||||
public static class NewtonsoftExtensions
|
||||
{
|
||||
public static void Rename(this JToken token, string newName)
|
||||
{
|
||||
if (token == null)
|
||||
{
|
||||
throw new ArgumentNullException("token", "Cannot rename a null token");
|
||||
}
|
||||
|
||||
JProperty property;
|
||||
|
||||
if (token.Type == JTokenType.Property)
|
||||
{
|
||||
if (token.Parent == null)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot rename a property with no parent");
|
||||
}
|
||||
|
||||
property = (JProperty)token;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (token.Parent == null || token.Parent.Type != JTokenType.Property)
|
||||
{
|
||||
throw new InvalidOperationException("This token's parent is not a JProperty; cannot rename");
|
||||
}
|
||||
|
||||
property = (JProperty)token.Parent;
|
||||
}
|
||||
|
||||
// Note: to avoid triggering a clone of the existing property's value,
|
||||
// we need to save a reference to it and then null out property.Value
|
||||
// before adding the value to the new JProperty.
|
||||
// Thanks to @dbc for the suggestion.
|
||||
|
||||
JToken? existingValue = property.Value;
|
||||
property.Value = null!;
|
||||
JProperty? newProperty = new JProperty(newName, existingValue);
|
||||
property.Replace(newProperty);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NitroxServer.Serialization.Json;
|
||||
|
||||
namespace NitroxServer.Serialization.Upgrade
|
||||
{
|
||||
public abstract class SaveDataUpgrade
|
||||
{
|
||||
private readonly JsonConverter[] converters = { new NitroxIdConverter(), new TechTypeConverter(), new VersionConverter(), new KeyValuePairConverter(), new StringEnumConverter() };
|
||||
|
||||
public abstract Version TargetVersion { get; }
|
||||
|
||||
public static readonly Version MinimumSaveVersion = new(1, 8, 0, 0);
|
||||
|
||||
public void UpgradeSaveFiles(string saveDir, string fileEnding)
|
||||
{
|
||||
Log.Info($"┌── Executing {GetType().Name}");
|
||||
string baseDataPath = Path.Combine(saveDir, $"BaseData{fileEnding}");
|
||||
string playerDataPath = Path.Combine(saveDir, $"PlayerData{fileEnding}");
|
||||
string worldDataPath = Path.Combine(saveDir, $"WorldData{fileEnding}");
|
||||
string entityDataPath = Path.Combine(saveDir, $"EntityData{fileEnding}");
|
||||
|
||||
Log.Info("├── Parsing raw json");
|
||||
JObject baseData = JObject.Parse(File.ReadAllText(baseDataPath));
|
||||
JObject playerData = JObject.Parse(File.ReadAllText(playerDataPath));
|
||||
JObject worldData = JObject.Parse(File.ReadAllText(worldDataPath));
|
||||
JObject entityData = JObject.Parse(File.ReadAllText(entityDataPath));
|
||||
|
||||
Log.Info("├── Applying upgrade scripts");
|
||||
UpgradeBaseData(baseData);
|
||||
UpgradePlayerData(playerData);
|
||||
UpgradeWorldData(worldData);
|
||||
UpgradeEntityData(entityData);
|
||||
|
||||
Log.Info("└── Saving to disk");
|
||||
File.WriteAllText(baseDataPath, baseData.ToString(Formatting.None, converters));
|
||||
File.WriteAllText(playerDataPath, playerData.ToString(Formatting.None, converters));
|
||||
File.WriteAllText(worldDataPath, worldData.ToString(Formatting.None, converters));
|
||||
File.WriteAllText(entityDataPath, entityData.ToString(Formatting.None, converters));
|
||||
}
|
||||
|
||||
protected virtual void UpgradeBaseData(JObject data) { }
|
||||
protected virtual void UpgradePlayerData(JObject data) { }
|
||||
protected virtual void UpgradeWorldData(JObject data) { }
|
||||
protected virtual void UpgradeEntityData(JObject data) { }
|
||||
}
|
||||
}
|
22
NitroxServer/Serialization/SaveDataUpgrades/Upgrade_V1500.cs
Normal file
22
NitroxServer/Serialization/SaveDataUpgrades/Upgrade_V1500.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NitroxServer.Serialization.Upgrade;
|
||||
|
||||
namespace NitroxServer.Serialization.SaveDataUpgrades
|
||||
{
|
||||
public sealed class Upgrade_V1500 : SaveDataUpgrade
|
||||
{
|
||||
public override Version TargetVersion { get; } = new Version(1, 5, 0, 0);
|
||||
|
||||
protected override void UpgradeWorldData(JObject data)
|
||||
{
|
||||
data["GameData"]["StoryTiming"] = data["StoryTimingData"];
|
||||
data.Property("StoryTimingData")?.Remove();
|
||||
data["Seed"] = "TCCBIBZXAB"; //Default seed so life pod should stay the same
|
||||
data["InventoryData"]["Modules"] = new JArray();
|
||||
|
||||
Log.Warn("Plants will still be counted as normal items with no growth progression. Re adding them to a container should fix this.");
|
||||
Log.Warn("The precursor incubator may be unpowered and hatching progress will be reset");
|
||||
}
|
||||
}
|
||||
}
|
37
NitroxServer/Serialization/SaveDataUpgrades/Upgrade_V1600.cs
Normal file
37
NitroxServer/Serialization/SaveDataUpgrades/Upgrade_V1600.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NitroxServer.Serialization.Upgrade;
|
||||
|
||||
namespace NitroxServer.Serialization.SaveDataUpgrades
|
||||
{
|
||||
public class Upgrade_V1600 : SaveDataUpgrade
|
||||
{
|
||||
public override Version TargetVersion { get; } = new Version(1, 6, 0, 0);
|
||||
|
||||
protected override void UpgradeWorldData(JObject data)
|
||||
{
|
||||
List<string> cleanUnlockedTechTypes = data["GameData"]["PDAState"]["UnlockedTechTypes"].ToObject<List<string>>().Distinct().ToList();
|
||||
List<string> cleanKnownTechTypes = data["GameData"]["PDAState"]["KnownTechTypes"].ToObject<List<string>>().Distinct().ToList();
|
||||
List<string> cleanEncyclopediaEntries = data["GameData"]["PDAState"]["EncyclopediaEntries"].ToObject<List<string>>().Distinct().ToList();
|
||||
data["GameData"]["PDAState"]["UnlockedTechTypes"] = new JArray(cleanUnlockedTechTypes);
|
||||
data["GameData"]["PDAState"]["KnownTechTypes"] = new JArray(cleanKnownTechTypes);
|
||||
data["GameData"]["PDAState"]["EncyclopediaEntries"] = new JArray(cleanEncyclopediaEntries);
|
||||
|
||||
List<JToken> cleanPdaLog = new List<JToken>();
|
||||
List<JToken> pdaLog = data["GameData"]["PDAState"]["PdaLog"].ToObject<List<JToken>>();
|
||||
foreach (JToken pdaLogEntry in pdaLog)
|
||||
{
|
||||
string Key = pdaLogEntry["Key"].ToString();
|
||||
if (cleanPdaLog.All(entry => entry["Key"].ToString() != Key))
|
||||
{
|
||||
cleanPdaLog.Add(pdaLogEntry);
|
||||
}
|
||||
}
|
||||
data["GameData"]["PDAState"]["PdaLog"] = new JArray(cleanPdaLog);
|
||||
|
||||
data.Property("ServerStartTime")?.Remove();
|
||||
}
|
||||
}
|
||||
}
|
29
NitroxServer/Serialization/SaveDataUpgrades/Upgrade_V1601.cs
Normal file
29
NitroxServer/Serialization/SaveDataUpgrades/Upgrade_V1601.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxServer.Serialization.Upgrade;
|
||||
|
||||
namespace NitroxServer.Serialization.SaveDataUpgrades
|
||||
{
|
||||
public class Upgrade_V1601 : SaveDataUpgrade
|
||||
{
|
||||
public override Version TargetVersion { get; } = new Version(1, 6, 0, 1);
|
||||
|
||||
protected override void UpgradeWorldData(JObject data)
|
||||
{
|
||||
List<string> modules = new();
|
||||
foreach (JToken moduleEntry in data["InventoryData"]["Modules"])
|
||||
{
|
||||
JToken itemId = moduleEntry["ItemId"];
|
||||
if (modules.Contains(itemId.ToString()))
|
||||
{
|
||||
itemId = new NitroxId().ToString();
|
||||
// this line is enough to modify the original data
|
||||
moduleEntry["ItemId"] = itemId;
|
||||
}
|
||||
modules.Add(itemId.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
64
NitroxServer/Serialization/ServerJsonSerializer.cs
Normal file
64
NitroxServer/Serialization/ServerJsonSerializer.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using NitroxModel.Platforms.OS.Shared;
|
||||
using NitroxServer.Serialization.Json;
|
||||
|
||||
namespace NitroxServer.Serialization;
|
||||
|
||||
public class ServerJsonSerializer : IServerSerializer
|
||||
{
|
||||
public const string FILE_ENDING = ".json";
|
||||
|
||||
private readonly JsonSerializer serializer;
|
||||
|
||||
public ServerJsonSerializer()
|
||||
{
|
||||
serializer = new JsonSerializer();
|
||||
|
||||
serializer.Error += delegate (object _, Newtonsoft.Json.Serialization.ErrorEventArgs e)
|
||||
{
|
||||
Log.Error(e.ErrorContext.Error, "Json serialization error: ");
|
||||
};
|
||||
|
||||
serializer.TypeNameHandling = TypeNameHandling.Auto;
|
||||
serializer.ContractResolver = new AttributeContractResolver();
|
||||
serializer.Converters.Add(new NitroxIdConverter());
|
||||
serializer.Converters.Add(new TechTypeConverter());
|
||||
serializer.Converters.Add(new VersionConverter());
|
||||
serializer.Converters.Add(new KeyValuePairConverter());
|
||||
serializer.Converters.Add(new StringEnumConverter());
|
||||
}
|
||||
|
||||
public string FileEnding => FILE_ENDING;
|
||||
|
||||
public void Serialize(Stream stream, object o)
|
||||
{
|
||||
stream.Position = 0;
|
||||
using JsonTextWriter writer = new(new StreamWriter(stream));
|
||||
serializer.Serialize(writer, o);
|
||||
}
|
||||
|
||||
public void Serialize(string filePath, object o)
|
||||
{
|
||||
string tmpPath = Path.ChangeExtension(filePath, ".tmp");
|
||||
using (StreamWriter stream = File.CreateText(tmpPath))
|
||||
{
|
||||
serializer.Serialize(stream, o);
|
||||
}
|
||||
FileSystem.Instance.ReplaceFile(tmpPath, filePath);
|
||||
}
|
||||
|
||||
public T Deserialize<T>(Stream stream)
|
||||
{
|
||||
stream.Position = 0;
|
||||
using JsonTextReader reader = new(new StreamReader(stream));
|
||||
return serializer.Deserialize<T>(reader);
|
||||
}
|
||||
|
||||
public T Deserialize<T>(string filePath)
|
||||
{
|
||||
using StreamReader reader = File.OpenText(filePath);
|
||||
return (T)serializer.Deserialize(reader, typeof(T));
|
||||
}
|
||||
}
|
122
NitroxServer/Serialization/ServerProtoBufSerializer.cs
Normal file
122
NitroxServer/Serialization/ServerProtoBufSerializer.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization;
|
||||
using NitroxModel.Platforms.OS.Shared;
|
||||
using ProtoBufNet;
|
||||
using ProtoBufNet.Meta;
|
||||
|
||||
namespace NitroxServer.Serialization;
|
||||
|
||||
public class ServerProtoBufSerializer : IServerSerializer
|
||||
{
|
||||
public const string FILE_ENDING = ".nitrox";
|
||||
|
||||
protected RuntimeTypeModel Model { get; } = TypeModel.Create();
|
||||
|
||||
public ServerProtoBufSerializer(params string[] assemblies)
|
||||
{
|
||||
foreach (string assembly in assemblies)
|
||||
{
|
||||
RegisterAssemblyClasses(assembly);
|
||||
}
|
||||
}
|
||||
|
||||
public string FileEnding => FILE_ENDING;
|
||||
|
||||
public void Serialize(Stream stream, object o)
|
||||
{
|
||||
Model.SerializeWithLengthPrefix(stream, o, o.GetType(), PrefixStyle.Base128, 0);
|
||||
}
|
||||
|
||||
public void Serialize(string filePath, object o)
|
||||
{
|
||||
string tmpPath = Path.ChangeExtension(filePath, ".tmp");
|
||||
using (Stream stream = File.OpenWrite(tmpPath))
|
||||
{
|
||||
Serialize(stream, o);
|
||||
}
|
||||
|
||||
FileSystem.Instance.ReplaceFile(tmpPath, filePath);
|
||||
}
|
||||
|
||||
public T Deserialize<T>(Stream stream)
|
||||
{
|
||||
T t = Activator.CreateInstance<T>();
|
||||
Model.DeserializeWithLengthPrefix(stream, t, typeof(T), PrefixStyle.Base128, 0);
|
||||
return t;
|
||||
}
|
||||
|
||||
public T Deserialize<T>(string filePath)
|
||||
{
|
||||
using Stream stream = File.OpenRead(filePath);
|
||||
return Deserialize<T>(stream);
|
||||
}
|
||||
|
||||
public void Deserialize(Stream stream, object o, Type t)
|
||||
{
|
||||
Model.DeserializeWithLengthPrefix(stream, o, t, PrefixStyle.Base128, 0);
|
||||
}
|
||||
|
||||
private void RegisterAssemblyClasses(string assemblyName)
|
||||
{
|
||||
bool HasNitroxDataContract(Type type) => type.GetCustomAttributes(typeof(DataContractAttribute), false).Length > 0;
|
||||
bool HasNitroxProtoBuf(Type type) => type.GetCustomAttributes(typeof(ProtoContractAttribute), false).Length > 0;
|
||||
|
||||
foreach (Type type in Assembly.Load(assemblyName).GetTypes())
|
||||
{
|
||||
try
|
||||
{
|
||||
if (HasNitroxDataContract(type) || HasNitroxProtoBuf(type))
|
||||
{
|
||||
// As of the latest protobuf update they will automatically register detected attributes.
|
||||
Model.Add(type, true);
|
||||
}
|
||||
else if (HasUweProtoContract(type))
|
||||
{
|
||||
Model.Add(type, true);
|
||||
|
||||
ManuallyRegisterUweProtoMembers(type.GetMembers(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance), type);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, $"ServerProtoBufSerializer has thrown an error registering the type: {type} from {assemblyName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasUweProtoContract(Type type)
|
||||
{
|
||||
foreach (object o in type.GetCustomAttributes(false))
|
||||
{
|
||||
if (o.GetType().ToString().Contains(nameof(ProtoContractAttribute)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ManuallyRegisterUweProtoMembers(MemberInfo[] info, Type type)
|
||||
{
|
||||
foreach (MemberInfo property in info)
|
||||
{
|
||||
if (property.DeclaringType != type)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (object customAttribute in property.GetCustomAttributes(false))
|
||||
{
|
||||
Type attributeType = customAttribute.GetType();
|
||||
if (attributeType.ToString().Contains(nameof(ProtoMemberAttribute)))
|
||||
{
|
||||
int tag = (int)attributeType.GetProperty(nameof(ProtoMemberAttribute.Tag), BindingFlags.Public | BindingFlags.Instance).GetValue(customAttribute, Array.Empty<object>());
|
||||
Model[type].Add(tag, property.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
47
NitroxServer/Serialization/World/PersistedWorldData.cs
Normal file
47
NitroxServer/Serialization/World/PersistedWorldData.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using System.Runtime.Serialization;
|
||||
using NitroxServer.GameLogic.Bases;
|
||||
using NitroxServer.GameLogic.Entities;
|
||||
using NitroxServer.GameLogic.Players;
|
||||
|
||||
namespace NitroxServer.Serialization.World
|
||||
{
|
||||
[DataContract]
|
||||
public class PersistedWorldData
|
||||
{
|
||||
[DataMember(Order = 1)]
|
||||
public WorldData WorldData { get; set; }
|
||||
|
||||
[DataMember(Order = 2)]
|
||||
public PlayerData PlayerData { get; set; }
|
||||
|
||||
[DataMember(Order = 3)]
|
||||
public GlobalRootData GlobalRootData { get; set; }
|
||||
|
||||
[DataMember(Order = 4)]
|
||||
public EntityData EntityData { get; set; }
|
||||
|
||||
public static PersistedWorldData From(World world)
|
||||
{
|
||||
return new PersistedWorldData
|
||||
{
|
||||
WorldData = new()
|
||||
{
|
||||
ParsedBatchCells = world.BatchEntitySpawner.SerializableParsedBatches,
|
||||
GameData = GameData.From(world.GameData.PDAState, world.GameData.StoryGoals, world.ScheduleKeeper, world.StoryManager, world.TimeKeeper),
|
||||
Seed = world.Seed,
|
||||
},
|
||||
PlayerData = PlayerData.From(world.PlayerManager.GetAllPlayers()),
|
||||
GlobalRootData = GlobalRootData.From(world.WorldEntityManager.GetPersistentGlobalRootEntities()),
|
||||
EntityData = EntityData.From(world.EntityRegistry.GetAllEntities(exceptGlobalRoot: true))
|
||||
};
|
||||
}
|
||||
|
||||
public bool IsValid()
|
||||
{
|
||||
return WorldData.IsValid() &&
|
||||
PlayerData != null &&
|
||||
GlobalRootData != null &&
|
||||
EntityData != null;
|
||||
}
|
||||
}
|
||||
}
|
43
NitroxServer/Serialization/World/SaveFileVersion.cs
Normal file
43
NitroxServer/Serialization/World/SaveFileVersion.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace NitroxServer.Serialization.World;
|
||||
|
||||
[DataContract]
|
||||
public class SaveFileVersion
|
||||
{
|
||||
[DataMember(Order = 1)]
|
||||
public readonly int Major;
|
||||
|
||||
[DataMember(Order = 2)]
|
||||
public readonly int Minor;
|
||||
|
||||
[DataMember(Order = 3)]
|
||||
public readonly int Build;
|
||||
|
||||
[DataMember(Order = 4)]
|
||||
public readonly int Revision;
|
||||
|
||||
public Version Version => new(Major, Minor, Build, Revision);
|
||||
|
||||
public SaveFileVersion()
|
||||
{
|
||||
Major = NitroxEnvironment.Version.Major;
|
||||
Minor = NitroxEnvironment.Version.Minor;
|
||||
Build = NitroxEnvironment.Version.Build;
|
||||
Revision = NitroxEnvironment.Version.Revision;
|
||||
}
|
||||
|
||||
public SaveFileVersion(Version version)
|
||||
{
|
||||
Major = version.Major;
|
||||
Minor = version.Minor;
|
||||
Build = version.Build;
|
||||
Revision = version.Revision;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Major}.{Minor}.{Build}.{Revision}";
|
||||
}
|
||||
}
|
20
NitroxServer/Serialization/World/VersionMismatchException.cs
Normal file
20
NitroxServer/Serialization/World/VersionMismatchException.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace NitroxServer.Serialization.World
|
||||
{
|
||||
[Serializable]
|
||||
public class VersionMismatchException : Exception
|
||||
{
|
||||
public VersionMismatchException() { }
|
||||
|
||||
public VersionMismatchException(string message) : base(message) { }
|
||||
|
||||
public VersionMismatchException(string message, Exception inner) : base(message, inner) { }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return base.Message;
|
||||
}
|
||||
}
|
||||
}
|
28
NitroxServer/Serialization/World/World.cs
Normal file
28
NitroxServer/Serialization/World/World.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using NitroxModel.DataStructures.GameLogic;
|
||||
using NitroxModel.Server;
|
||||
using NitroxServer.GameLogic;
|
||||
using NitroxServer.GameLogic.Bases;
|
||||
using NitroxServer.GameLogic.Entities;
|
||||
using NitroxServer.GameLogic.Entities.Spawning;
|
||||
|
||||
namespace NitroxServer.Serialization.World
|
||||
{
|
||||
public class World
|
||||
{
|
||||
public PlayerManager PlayerManager { get; set; }
|
||||
public ScheduleKeeper ScheduleKeeper { get; set; }
|
||||
public TimeKeeper TimeKeeper { get; set; }
|
||||
public SimulationOwnershipData SimulationOwnershipData { get; set; }
|
||||
public EscapePodManager EscapePodManager { get; set; }
|
||||
public BatchEntitySpawner BatchEntitySpawner { get; set; }
|
||||
public EntitySimulation EntitySimulation { get; set; }
|
||||
public EntityRegistry EntityRegistry { get; set; }
|
||||
public WorldEntityManager WorldEntityManager { get; set; }
|
||||
public BuildingManager BuildingManager { get; set; }
|
||||
public StoryManager StoryManager { get; set; }
|
||||
public GameData GameData { get; set; }
|
||||
public SessionSettings SessionSettings { get; set; }
|
||||
public NitroxGameMode GameMode { get; set; }
|
||||
public string Seed { get; set; }
|
||||
}
|
||||
}
|
27
NitroxServer/Serialization/World/WorldData.cs
Normal file
27
NitroxServer/Serialization/World/WorldData.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Serialization;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxServer.GameLogic.Bases;
|
||||
|
||||
namespace NitroxServer.Serialization.World
|
||||
{
|
||||
[DataContract]
|
||||
public class WorldData
|
||||
{
|
||||
[DataMember(Order = 1)]
|
||||
public List<NitroxInt3> ParsedBatchCells { get; set; } = [];
|
||||
|
||||
[DataMember(Order = 2)]
|
||||
public GameData GameData { get; set; }
|
||||
|
||||
[DataMember(Order = 3)]
|
||||
public string Seed { get; set; }
|
||||
|
||||
public bool IsValid()
|
||||
{
|
||||
return ParsedBatchCells != null &&
|
||||
GameData != null &&
|
||||
Seed != null;
|
||||
}
|
||||
}
|
||||
}
|
356
NitroxServer/Serialization/World/WorldPersistence.cs
Normal file
356
NitroxServer/Serialization/World/WorldPersistence.cs
Normal file
@@ -0,0 +1,356 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NitroxModel.Core;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.DataStructures.GameLogic;
|
||||
using NitroxModel.DataStructures.GameLogic.Entities;
|
||||
using NitroxModel.DataStructures.Util;
|
||||
using NitroxModel.Networking;
|
||||
using NitroxModel.Platforms.OS.Shared;
|
||||
using NitroxModel.Serialization;
|
||||
using NitroxModel.Server;
|
||||
using NitroxServer.GameLogic;
|
||||
using NitroxServer.GameLogic.Bases;
|
||||
using NitroxServer.GameLogic.Entities;
|
||||
using NitroxServer.GameLogic.Entities.Spawning;
|
||||
using NitroxServer.GameLogic.Players;
|
||||
using NitroxServer.GameLogic.Unlockables;
|
||||
using NitroxServer.Helper;
|
||||
using NitroxServer.Resources;
|
||||
using NitroxServer.Serialization.Upgrade;
|
||||
|
||||
namespace NitroxServer.Serialization.World;
|
||||
|
||||
public class WorldPersistence
|
||||
{
|
||||
public const string BACKUP_DATE_TIME_FORMAT = "yyyy-MM-dd HH.mm.ss";
|
||||
public IServerSerializer Serializer { get; private set; }
|
||||
private string FileEnding => Serializer?.FileEnding ?? "";
|
||||
|
||||
private readonly ServerProtoBufSerializer protoBufSerializer;
|
||||
private readonly ServerJsonSerializer jsonSerializer;
|
||||
private readonly SubnauticaServerConfig config;
|
||||
private readonly RandomStartGenerator randomStart;
|
||||
private readonly IWorldModifier worldModifier;
|
||||
private readonly SaveDataUpgrade[] upgrades;
|
||||
private readonly RandomSpawnSpoofer randomSpawnSpoofer;
|
||||
private readonly NtpSyncer ntpSyncer;
|
||||
|
||||
public WorldPersistence(
|
||||
ServerProtoBufSerializer protoBufSerializer,
|
||||
ServerJsonSerializer jsonSerializer,
|
||||
SubnauticaServerConfig config,
|
||||
RandomStartGenerator randomStart,
|
||||
IWorldModifier worldModifier,
|
||||
SaveDataUpgrade[] upgrades,
|
||||
RandomSpawnSpoofer randomSpawnSpoofer,
|
||||
NtpSyncer ntpSyncer
|
||||
)
|
||||
{
|
||||
this.protoBufSerializer = protoBufSerializer;
|
||||
this.jsonSerializer = jsonSerializer;
|
||||
this.config = config;
|
||||
this.randomStart = randomStart;
|
||||
this.worldModifier = worldModifier;
|
||||
this.upgrades = upgrades;
|
||||
this.randomSpawnSpoofer = randomSpawnSpoofer;
|
||||
this.ntpSyncer = ntpSyncer;
|
||||
|
||||
UpdateSerializer(config.SerializerMode);
|
||||
}
|
||||
|
||||
public bool Save(World world, string saveDir) => Save(PersistedWorldData.From(world), saveDir);
|
||||
|
||||
public void BackUp(string saveDir)
|
||||
{
|
||||
if (config.MaxBackups < 1)
|
||||
{
|
||||
Log.Info($"No backup was made (\"{nameof(config.MaxBackups)}\" is equal to 0)");
|
||||
return;
|
||||
}
|
||||
string backupDir = Path.Combine(saveDir, "Backups");
|
||||
string tempOutDir = Path.Combine(backupDir, $"Backup - {DateTime.Now.ToString(BACKUP_DATE_TIME_FORMAT)}");
|
||||
Directory.CreateDirectory(backupDir);
|
||||
|
||||
try
|
||||
{
|
||||
// Prepare backup location
|
||||
Directory.CreateDirectory(tempOutDir);
|
||||
string newZipFile = $"{tempOutDir}.zip";
|
||||
if (File.Exists(newZipFile))
|
||||
{
|
||||
File.Delete(newZipFile);
|
||||
}
|
||||
foreach (string file in Directory.GetFiles(saveDir))
|
||||
{
|
||||
File.Copy(file, Path.Combine(tempOutDir, Path.GetFileName(file)));
|
||||
}
|
||||
|
||||
FileSystem.Instance.ZipFilesInDirectory(tempOutDir, newZipFile);
|
||||
Directory.Delete(tempOutDir, true);
|
||||
Log.Info("World backed up");
|
||||
|
||||
// Prune old backups
|
||||
FileInfo[] backups = Directory.EnumerateFiles(backupDir)
|
||||
.Select(f => new FileInfo(f))
|
||||
.Where(f => f is { Extension: ".zip" } info && info.Name.Contains("Backup - "))
|
||||
.OrderBy(f => File.GetCreationTime(f.FullName))
|
||||
.ToArray();
|
||||
if (backups.Length > config.MaxBackups)
|
||||
{
|
||||
int numBackupsToDelete = backups.Length - Math.Max(1, config.MaxBackups);
|
||||
for (int i = 0; i < numBackupsToDelete; i++)
|
||||
{
|
||||
backups[i].Delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error while backing up world");
|
||||
if (Directory.Exists(tempOutDir))
|
||||
{
|
||||
Directory.Delete(tempOutDir, true); // Delete the outZip folder that is sometimes left
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public World Load(string saveName)
|
||||
{
|
||||
Optional<World> fileLoadedWorld = LoadFromFile(Path.Combine(KeyValueStore.Instance.GetSavesFolderDir(), saveName));
|
||||
if (fileLoadedWorld.HasValue)
|
||||
{
|
||||
return fileLoadedWorld.Value;
|
||||
}
|
||||
|
||||
return CreateFreshWorld();
|
||||
}
|
||||
|
||||
public World CreateWorld(PersistedWorldData pWorldData, NitroxGameMode gameMode)
|
||||
{
|
||||
string seed = pWorldData.WorldData.Seed;
|
||||
if (string.IsNullOrWhiteSpace(seed))
|
||||
{
|
||||
#if DEBUG
|
||||
seed = "TCCBIBZXAB";
|
||||
#else
|
||||
seed = StringHelper.GenerateRandomString(10);
|
||||
#endif
|
||||
}
|
||||
// Initialized only once, just like UnityEngine.Random
|
||||
XORRandom.InitSeed(seed.GetHashCode());
|
||||
|
||||
Log.Info($"Loading world with seed {seed}");
|
||||
|
||||
EntityRegistry entityRegistry = NitroxServiceLocator.LocateService<EntityRegistry>();
|
||||
entityRegistry.AddEntities(pWorldData.EntityData.Entities);
|
||||
entityRegistry.AddEntitiesIgnoringDuplicate(pWorldData.GlobalRootData.Entities.OfType<Entity>().ToList());
|
||||
|
||||
World world = new()
|
||||
{
|
||||
SimulationOwnershipData = new SimulationOwnershipData(),
|
||||
PlayerManager = new PlayerManager(pWorldData.PlayerData.GetPlayers(), config),
|
||||
EscapePodManager = new EscapePodManager(entityRegistry, randomStart, seed),
|
||||
EntityRegistry = entityRegistry,
|
||||
GameData = pWorldData.WorldData.GameData,
|
||||
GameMode = gameMode,
|
||||
Seed = seed,
|
||||
SessionSettings = new()
|
||||
};
|
||||
|
||||
world.TimeKeeper = new(world.PlayerManager, ntpSyncer, pWorldData.WorldData.GameData.StoryTiming.ElapsedSeconds, pWorldData.WorldData.GameData.StoryTiming.RealTimeElapsed);
|
||||
world.StoryManager = new StoryManager(world.PlayerManager, pWorldData.WorldData.GameData.PDAState, pWorldData.WorldData.GameData.StoryGoals, world.TimeKeeper, seed, pWorldData.WorldData.GameData.StoryTiming.AuroraCountdownTime,
|
||||
pWorldData.WorldData.GameData.StoryTiming.AuroraWarningTime, pWorldData.WorldData.GameData.StoryTiming.AuroraRealExplosionTime);
|
||||
world.ScheduleKeeper = new ScheduleKeeper(pWorldData.WorldData.GameData.PDAState, pWorldData.WorldData.GameData.StoryGoals, world.TimeKeeper, world.PlayerManager);
|
||||
|
||||
world.BatchEntitySpawner = new BatchEntitySpawner(
|
||||
NitroxServiceLocator.LocateService<EntitySpawnPointFactory>(),
|
||||
NitroxServiceLocator.LocateService<IUweWorldEntityFactory>(),
|
||||
NitroxServiceLocator.LocateService<IUwePrefabFactory>(),
|
||||
pWorldData.WorldData.ParsedBatchCells,
|
||||
protoBufSerializer,
|
||||
NitroxServiceLocator.LocateService<IEntityBootstrapperManager>(),
|
||||
NitroxServiceLocator.LocateService<Dictionary<string, PrefabPlaceholdersGroupAsset>>(),
|
||||
pWorldData.WorldData.GameData.PDAState,
|
||||
randomSpawnSpoofer,
|
||||
world.Seed
|
||||
);
|
||||
|
||||
world.WorldEntityManager = new WorldEntityManager(world.EntityRegistry, world.BatchEntitySpawner, world.PlayerManager);
|
||||
|
||||
world.BuildingManager = new BuildingManager(world.EntityRegistry, world.WorldEntityManager, config);
|
||||
|
||||
ISimulationWhitelist simulationWhitelist = NitroxServiceLocator.LocateService<ISimulationWhitelist>();
|
||||
world.EntitySimulation = new EntitySimulation(world.EntityRegistry, world.WorldEntityManager, world.SimulationOwnershipData, world.PlayerManager, simulationWhitelist);
|
||||
|
||||
return world;
|
||||
}
|
||||
|
||||
internal void UpdateSerializer(IServerSerializer serverSerializer)
|
||||
{
|
||||
Validate.NotNull(serverSerializer, "Serializer cannot be null");
|
||||
Serializer = serverSerializer;
|
||||
}
|
||||
|
||||
internal void UpdateSerializer(ServerSerializerMode mode) => Serializer = mode == ServerSerializerMode.PROTOBUF ? protoBufSerializer : jsonSerializer;
|
||||
|
||||
internal bool Save(PersistedWorldData persistedData, string saveDir)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(saveDir))
|
||||
{
|
||||
Directory.CreateDirectory(saveDir);
|
||||
}
|
||||
|
||||
Serializer.Serialize(Path.Combine(saveDir, $"Version{FileEnding}"), new SaveFileVersion());
|
||||
Serializer.Serialize(Path.Combine(saveDir, $"PlayerData{FileEnding}"), persistedData.PlayerData);
|
||||
Serializer.Serialize(Path.Combine(saveDir, $"WorldData{FileEnding}"), persistedData.WorldData);
|
||||
Serializer.Serialize(Path.Combine(saveDir, $"GlobalRootData{FileEnding}"), persistedData.GlobalRootData);
|
||||
Serializer.Serialize(Path.Combine(saveDir, $"EntityData{FileEnding}"), persistedData.EntityData);
|
||||
|
||||
using (config.Update(saveDir))
|
||||
{
|
||||
config.Seed = persistedData.WorldData.Seed;
|
||||
}
|
||||
|
||||
Log.Info("World state saved");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Could not save world :");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal Optional<World> LoadFromFile(string saveDir)
|
||||
{
|
||||
if (!Directory.Exists(saveDir) || !File.Exists(Path.Combine(saveDir, $"Version{FileEnding}")))
|
||||
{
|
||||
Log.Warn("No previous save file found, creating a new one");
|
||||
return Optional.Empty;
|
||||
}
|
||||
|
||||
UpgradeSave(saveDir);
|
||||
|
||||
PersistedWorldData persistedData = LoadDataFromPath(saveDir);
|
||||
|
||||
if (persistedData == null)
|
||||
{
|
||||
return Optional.Empty;
|
||||
}
|
||||
|
||||
World world = CreateWorld(persistedData, config.GameMode);
|
||||
|
||||
return Optional.Of(world);
|
||||
}
|
||||
|
||||
internal PersistedWorldData LoadDataFromPath(string saveDir)
|
||||
{
|
||||
try
|
||||
{
|
||||
PersistedWorldData persistedData = new()
|
||||
{
|
||||
PlayerData = Serializer.Deserialize<PlayerData>(Path.Combine(saveDir, $"PlayerData{FileEnding}")),
|
||||
WorldData = Serializer.Deserialize<WorldData>(Path.Combine(saveDir, $"WorldData{FileEnding}")),
|
||||
GlobalRootData = Serializer.Deserialize<GlobalRootData>(Path.Combine(saveDir, $"GlobalRootData{FileEnding}")),
|
||||
EntityData = Serializer.Deserialize<EntityData>(Path.Combine(saveDir, $"EntityData{FileEnding}"))
|
||||
};
|
||||
|
||||
if (!persistedData.IsValid())
|
||||
{
|
||||
throw new InvalidDataException("Save files are not valid");
|
||||
}
|
||||
|
||||
return persistedData;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Check if the world was newly created using the world manager
|
||||
if (new FileInfo(Path.Combine(saveDir, $"Version{FileEnding}")).Length > 0)
|
||||
{
|
||||
// Give error saying that world could not be used, and to restore a backup
|
||||
Log.Error($"Could not load world, please restore one of your backups to continue using this world. : {ex.GetType()} {ex.Message}");
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private World CreateFreshWorld()
|
||||
{
|
||||
PersistedWorldData pWorldData = new()
|
||||
{
|
||||
EntityData = EntityData.From(new List<Entity>()),
|
||||
PlayerData = PlayerData.From(new List<Player>()),
|
||||
WorldData = new WorldData
|
||||
{
|
||||
GameData = new GameData
|
||||
{
|
||||
PDAState = new PDAStateData(),
|
||||
StoryGoals = new StoryGoalData(),
|
||||
StoryTiming = new StoryTimingData()
|
||||
},
|
||||
ParsedBatchCells = new List<NitroxInt3>(),
|
||||
Seed = config.Seed
|
||||
},
|
||||
GlobalRootData = new GlobalRootData()
|
||||
};
|
||||
|
||||
World newWorld = CreateWorld(pWorldData, config.GameMode);
|
||||
worldModifier.ModifyWorld(newWorld);
|
||||
|
||||
return newWorld;
|
||||
}
|
||||
|
||||
private void UpgradeSave(string saveDir)
|
||||
{
|
||||
SaveFileVersion saveFileVersion;
|
||||
|
||||
try
|
||||
{
|
||||
saveFileVersion = Serializer.Deserialize<SaveFileVersion>(Path.Combine(saveDir, $"Version{FileEnding}"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, $"Error while upgrading save file. \"Version{FileEnding}\" couldn't be read.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (saveFileVersion == null || saveFileVersion.Version == NitroxEnvironment.Version)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.SerializerMode == ServerSerializerMode.PROTOBUF)
|
||||
{
|
||||
Log.Info("Can't upgrade while using ProtoBuf as serializer");
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (SaveDataUpgrade upgrade in upgrades)
|
||||
{
|
||||
if (upgrade.TargetVersion > saveFileVersion.Version)
|
||||
{
|
||||
upgrade.UpgradeSaveFiles(saveDir, FileEnding);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error while upgrading save file.");
|
||||
return;
|
||||
}
|
||||
|
||||
Serializer.Serialize(Path.Combine(saveDir, $"Version{FileEnding}"), new SaveFileVersion());
|
||||
Log.Info($"Save file was upgraded to {NitroxEnvironment.Version}");
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user