Skip to content

Commit

Permalink
Added save file upgradability (SubnauticaNitrox#1370)
Browse files Browse the repository at this point in the history
* Added save file upgradability

* Changed config value setting with commands

* Moved waiting  to server

* Moved from string to JObject in SaveDataUpgrade
  • Loading branch information
Jannify authored Jan 21, 2021
1 parent 97188cb commit 3963b27
Show file tree
Hide file tree
Showing 18 changed files with 317 additions and 105 deletions.
3 changes: 3 additions & 0 deletions NitroxModel/Core/NitroxEnvironment.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
using System;
using System.Reflection;

namespace NitroxModel.Helper
{
public static class NitroxEnvironment
{
public static readonly Version Version = Assembly.GetAssembly(typeof(NitroxEnvironment)).GetName().Version;

public enum Types
{
NORMAL,
Expand Down
9 changes: 4 additions & 5 deletions NitroxServer/ConsoleCommands/AutosaveCommand.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.Serialization;
using NitroxModel.Server;
using NitroxServer.ConsoleCommands.Abstract;
using NitroxServer.ConsoleCommands.Abstract.Type;
Expand All @@ -8,18 +9,15 @@ namespace NitroxServer.ConsoleCommands
{
internal class AutoSaveCommand : Command
{
private readonly ServerConfig serverConfig;

public AutoSaveCommand(ServerConfig serverConfig) : base("autosave", Perms.ADMIN, "Toggles the map autosave")
public AutoSaveCommand() : base("autosave", Perms.ADMIN, "Toggles the map autosave")
{
this.serverConfig = serverConfig;
AddParameter(new TypeBoolean("on/off", true));
}

protected override void Execute(CallArgs args)
{
bool toggle = args.Get<bool>(0);

ServerConfig serverConfig = NitroxConfig.Deserialize<ServerConfig>();
if (toggle)
{
serverConfig.DisableAutoSave = false;
Expand All @@ -32,6 +30,7 @@ protected override void Execute(CallArgs args)
Server.Instance.DisablePeriodicSaving();
SendMessage(args.Sender, "Disabled periodical saving");
}
NitroxConfig.Serialize(serverConfig);
}
}
}
10 changes: 5 additions & 5 deletions NitroxServer/ConsoleCommands/ChangeAdminPasswordCommand.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.Logger;
using NitroxModel.Serialization;
using NitroxModel.Server;
using NitroxServer.ConsoleCommands.Abstract;
using NitroxServer.ConsoleCommands.Abstract.Type;
Expand All @@ -9,22 +10,21 @@ namespace NitroxServer.ConsoleCommands
{
internal class ChangeAdminPasswordCommand : Command
{
private readonly ServerConfig serverConfig;

public ChangeAdminPasswordCommand(ServerConfig serverConfig) : base("changeadminpassword", Perms.ADMIN, "Changes admin password")
public ChangeAdminPasswordCommand() : base("changeadminpassword", Perms.ADMIN, "Changes admin password")
{
this.serverConfig = serverConfig;
AddParameter(new TypeString("password", true));
}

protected override void Execute(CallArgs args)
{
string newPassword = args.Get(0);

ServerConfig serverConfig = NitroxConfig.Deserialize<ServerConfig>();
serverConfig.AdminPassword = newPassword;
NitroxConfig.Serialize(serverConfig);

Log.InfoSensitive("Admin password changed to {password} by {playername}", newPassword, args.SenderName);
SendMessageToPlayer(args.Sender, "Admin password changed");
SendMessageToPlayer(args.Sender, "Admin password changed. In order to take effect pls restart the server.");
}
}
}
10 changes: 5 additions & 5 deletions NitroxServer/ConsoleCommands/ChangeServerPasswordCommand.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.Logger;
using NitroxModel.Serialization;
using NitroxModel.Server;
using NitroxServer.ConsoleCommands.Abstract;
using NitroxServer.ConsoleCommands.Abstract.Type;
Expand All @@ -9,22 +10,21 @@ namespace NitroxServer.ConsoleCommands
{
internal class ChangeServerPasswordCommand : Command
{
private readonly ServerConfig serverConfig;

public ChangeServerPasswordCommand(ServerConfig serverConfig) : base("changeserverpassword", Perms.ADMIN, "Changes server password. Clear it without argument")
public ChangeServerPasswordCommand() : base("changeserverpassword", Perms.ADMIN, "Changes server password. Clear it without argument")
{
this.serverConfig = serverConfig;
AddParameter(new TypeString("password", false));
}

protected override void Execute(CallArgs args)
{
string password = args.Get(0) ?? string.Empty;

ServerConfig serverConfig = NitroxConfig.Deserialize<ServerConfig>();
serverConfig.ServerPassword = password;
NitroxConfig.Serialize(serverConfig);

Log.InfoSensitive("Server password changed to {password} by {playername}", password, args.SenderName);
SendMessageToPlayer(args.Sender, "Server password changed");
SendMessageToPlayer(args.Sender, "Server password changed. In order to take effect pls restart the server.");
}
}
}
33 changes: 33 additions & 0 deletions NitroxServer/ConsoleCommands/SwapSerializerCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.Serialization;
using NitroxModel.Server;
using NitroxServer.ConsoleCommands.Abstract;
using NitroxServer.Serialization;
using NitroxServer.Serialization.World;

namespace NitroxServer.ConsoleCommands
{
internal sealed class SwapSerializerCommand : Command
{
private readonly WorldPersistence worldPersistence;
private readonly ServerProtoBufSerializer protoBufSerializer;
private readonly ServerJsonSerializer jsonSerializer;

public SwapSerializerCommand(WorldPersistence worldPersistence, ServerProtoBufSerializer protoBufSerializer, ServerJsonSerializer jsonSerializer) : base("swapSerializer", Perms.CONSOLE, "Swaps the world data serializer")
{
this.worldPersistence = worldPersistence;
this.protoBufSerializer = protoBufSerializer;
this.jsonSerializer = jsonSerializer;
}

protected override void Execute(CallArgs args)
{
ServerConfig serverConfig = NitroxConfig.Deserialize<ServerConfig>();
serverConfig.SerializerMode = serverConfig.SerializerMode == ServerSerializerMode.PROTOBUF ? ServerSerializerMode.JSON : ServerSerializerMode.PROTOBUF;
NitroxConfig.Serialize(serverConfig);

worldPersistence.UpdateSerializer(serverConfig.SerializerMode == ServerSerializerMode.PROTOBUF ? (IServerSerializer)protoBufSerializer : jsonSerializer);
SendMessage(args.Sender, $"Swapped to {serverConfig.SerializerMode}");
}
}
}
59 changes: 59 additions & 0 deletions NitroxServer/ConsoleCommands/UpgradeCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System.IO;
using System.Threading;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.Helper;
using NitroxModel.Server;
using NitroxServer.ConsoleCommands.Abstract;
using NitroxServer.Serialization;
using NitroxServer.Serialization.Upgrade;
using NitroxServer.Serialization.World;

namespace NitroxServer.ConsoleCommands
{
internal sealed class UpgradeCommand : Command
{
private readonly WorldPersistence worldPersistence;
private readonly ServerConfig serverConfig;
private readonly SaveDataUpgrade[] upgrades;
private readonly Server server;

public UpgradeCommand(WorldPersistence worldPersistence, ServerConfig serverConfig, SaveDataUpgrade[] upgrades, Server server) : base("upgrade", Perms.CONSOLE, "Upgrades the save file to the next version")
{
this.worldPersistence = worldPersistence;
this.serverConfig = serverConfig;
this.upgrades = upgrades;
this.server = server;
}

protected override void Execute(CallArgs args)
{
server.DisablePeriodicSaving();
string saveDir = serverConfig.SaveName;
string fileEnding = worldPersistence.SaveDataSerializer.GetFileEnding();
SaveFileVersion saveFileVersion = worldPersistence.SaveDataSerializer.Deserialize<SaveFileVersion>(Path.Combine(saveDir, "Version" + fileEnding));


if (saveFileVersion.Version == NitroxEnvironment.Version)
{
SendMessage(args.Sender, "Save files are already at the newest version");
}
else if (serverConfig.SerializerMode == ServerSerializerMode.PROTOBUF)
{
SendMessage(args.Sender, "Can't upgrade while using ProtoBuf as serializer");
}
else
{
foreach (SaveDataUpgrade upgrade in upgrades)
{
if (upgrade.TargetVersion > saveFileVersion.Version)
{
upgrade.UpgradeData(saveDir, fileEnding);
}
}
worldPersistence.SaveDataSerializer.Serialize(Path.Combine(saveDir, "Version" + fileEnding), new SaveFileVersion());
SendMessage(args.Sender, $"Save file was upgraded to {NitroxEnvironment.Version}");
server.StopAndWait(false);
}
}
}
}
2 changes: 0 additions & 2 deletions NitroxServer/GameLogic/Bases/BaseData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ namespace NitroxServer.GameLogic.Bases
[ProtoContract, JsonObject(MemberSerialization.OptIn)]
public class BaseData
{
public const short VERSION = 2;

[JsonProperty, ProtoMember(1)]
public List<BasePiece> PartiallyConstructedPieces = new List<BasePiece>();

Expand Down
2 changes: 0 additions & 2 deletions NitroxServer/GameLogic/Players/PlayerData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ namespace NitroxServer.GameLogic.Players
[ProtoContract, JsonObject(MemberSerialization.OptIn)]
public class PlayerData
{
public const short VERSION = 2;

[JsonProperty, ProtoMember(1)]
public List<PersistedPlayerData> Players = new List<PersistedPlayerData>();

Expand Down
8 changes: 6 additions & 2 deletions NitroxServer/NitroxServer.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
Expand Down Expand Up @@ -147,7 +147,9 @@
<Compile Include="ConsoleCommands\DirectoryCommand.cs" />
<Compile Include="ConsoleCommands\HelpCommand.cs" />
<Compile Include="ConsoleCommands\RestartCommand.cs" />
<Compile Include="ConsoleCommands\SwapSerializerCommand.cs" />
<Compile Include="ConsoleCommands\TeleportCommand.cs" />
<Compile Include="ConsoleCommands\UpgradeCommand.cs" />
<Compile Include="ConsoleCommands\WarpCommand.cs" />
<Compile Include="ConsoleCommands\WhoisCommand.cs" />
<Compile Include="ConsoleCommands\KickCommand.cs" />
Expand All @@ -172,6 +174,8 @@
<Compile Include="Serialization\JsonConverter\AttributeContractResolver.cs" />
<Compile Include="Serialization\JsonConverter\NitroxIdConverter.cs" />
<Compile Include="Serialization\JsonConverter\TechTypeConverter.cs" />
<Compile Include="Serialization\SaveDataUpgrades\NewtonsoftExtensions.cs" />
<Compile Include="Serialization\SaveDataUpgrades\SaveDataUpgrade.cs" />
<Compile Include="Serialization\ServerConfig.cs" />
<Compile Include="Serialization\Resources\Datastructures\AssetIdentifier.cs" />
<Compile Include="Serialization\Resources\Datastructures\GameObjectAsset.cs" />
Expand Down Expand Up @@ -214,7 +218,7 @@
<Compile Include="Serialization\World\World.cs" />
<Compile Include="Serialization\World\WorldData.cs" />
<Compile Include="Serialization\World\WorldPersistence.cs" />
<Compile Include="Serialization\World\SaveFileVersions.cs" />
<Compile Include="Serialization\World\SaveFileVersion.cs" />
<Compile Include="Serialization\World\VersionMismatchException.cs" />
<Compile Include="Server.cs" />
<Compile Include="ServerAutoFacRegistrar.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
56 changes: 56 additions & 0 deletions NitroxServer/Serialization/SaveDataUpgrades/SaveDataUpgrade.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System;
using System.IO;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
using NitroxModel.DataStructures.JsonConverter;
using NitroxModel.Logger;

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 void UpgradeData(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);

try
{
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));
}
catch (Exception ex)
{
Log.Error(ex, $"Error while upgrading save file with {GetType().Name}");
}
}

protected virtual void UpgradeBaseData(JObject data) { }
protected virtual void UpgradePlayerData(JObject data) { }
protected virtual void UpgradeWorldData(JObject data) { }
protected virtual void UpgradeEntityData(JObject data) { }
}
}
4 changes: 4 additions & 0 deletions NitroxServer/Serialization/ServerJsonSerializer.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.IO;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using NitroxModel.DataStructures.JsonConverter;
using NitroxModel.Logger;

Expand All @@ -22,6 +23,9 @@ public ServerJsonSerializer()
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 GetFileEnding() => ".json";
Expand Down
Loading

0 comments on commit 3963b27

Please sign in to comment.