first commit
This commit is contained in:
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