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,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;
}
}
}

View 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}";
}
}

View 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;
}
}
}

View 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; }
}
}

View 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;
}
}
}

View 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}");
}
}
}