first commit
This commit is contained in:
21
NitroxModel/Serialization/IProperties.cs
Normal file
21
NitroxModel/Serialization/IProperties.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace NitroxModel.Serialization;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Class, Inherited = false)]
|
||||
public sealed class PropertyDescriptionAttribute : DescriptionAttribute
|
||||
{
|
||||
public PropertyDescriptionAttribute(string desc) : base(desc)
|
||||
{
|
||||
}
|
||||
|
||||
public PropertyDescriptionAttribute(string desc, Type type)
|
||||
{
|
||||
if (type.IsEnum)
|
||||
{
|
||||
desc += $" {string.Join(", ", type.GetEnumNames())}";
|
||||
DescriptionValue = desc;
|
||||
}
|
||||
}
|
||||
}
|
265
NitroxModel/Serialization/NitroxConfig.cs
Normal file
265
NitroxModel/Serialization/NitroxConfig.cs
Normal file
@@ -0,0 +1,265 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace NitroxModel.Serialization;
|
||||
|
||||
public abstract class NitroxConfig<T> where T : NitroxConfig<T>, new()
|
||||
{
|
||||
private static readonly Dictionary<string, object> unserializedMembersWarnOnceCache = [];
|
||||
private static readonly Dictionary<string, MemberInfo> typeCache = [];
|
||||
|
||||
private readonly char[] newlineChars = Environment.NewLine.ToCharArray();
|
||||
private readonly object locker = new();
|
||||
|
||||
public abstract string FileName { get; }
|
||||
|
||||
public static T Load(string saveDir)
|
||||
{
|
||||
T config = new();
|
||||
config.Deserialize(saveDir);
|
||||
return config;
|
||||
}
|
||||
|
||||
public void Deserialize(string saveDir)
|
||||
{
|
||||
if (!File.Exists(Path.Combine(saveDir, FileName)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (locker)
|
||||
{
|
||||
Type type = GetType();
|
||||
Dictionary<string, MemberInfo> typeCachedDict = GetTypeCacheDictionary();
|
||||
using StreamReader reader = new(new FileStream(Path.Combine(saveDir, FileName), FileMode.Open, FileAccess.Read, FileShare.Read), Encoding.UTF8);
|
||||
|
||||
HashSet<MemberInfo> unserializedMembers = new(typeCachedDict.Values);
|
||||
char[] lineSeparator = { '=' };
|
||||
int lineNum = 0;
|
||||
string readLine;
|
||||
|
||||
while ((readLine = reader.ReadLine()) != null)
|
||||
{
|
||||
lineNum++;
|
||||
if (readLine.Length < 1 || readLine[0] == '#')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (readLine.Contains('='))
|
||||
{
|
||||
string[] keyValuePair = readLine.Split(lineSeparator, 2);
|
||||
// Ignore case for property names in file.
|
||||
if (!typeCachedDict.TryGetValue(keyValuePair[0].ToLowerInvariant(), out MemberInfo member))
|
||||
{
|
||||
Log.Warn($"Property or field {keyValuePair[0]} does not exist on type {type.FullName}!");
|
||||
continue;
|
||||
}
|
||||
|
||||
unserializedMembers.Remove(member); // This member was serialized in the file
|
||||
|
||||
if (!NitroxConfig<T>.SetMemberValue(this, member, keyValuePair[1]))
|
||||
{
|
||||
(Type type, object value) logData = member switch
|
||||
{
|
||||
FieldInfo field => (field.FieldType, field.GetValue(this)),
|
||||
PropertyInfo prop => (prop.PropertyType, prop.GetValue(this)),
|
||||
_ => (typeof(string), "")
|
||||
};
|
||||
Log.Warn($@"Property ""({logData.type.Name}) {member.Name}"" has an invalid value {NitroxConfig<T>.StringifyValue(keyValuePair[1])} on line {lineNum}. Using default value: {NitroxConfig<T>.StringifyValue(logData.value)}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"Incorrect format detected on line {lineNum} in {Path.GetFullPath(Path.Combine(saveDir, FileName))}:{Environment.NewLine}{readLine}");
|
||||
}
|
||||
}
|
||||
|
||||
if (unserializedMembers.Count != 0)
|
||||
{
|
||||
string[] unserializedProps = unserializedMembers
|
||||
.Select(m =>
|
||||
{
|
||||
object value = null;
|
||||
if (m is FieldInfo field)
|
||||
{
|
||||
value = field.GetValue(this);
|
||||
}
|
||||
else if (m is PropertyInfo prop)
|
||||
{
|
||||
value = prop.GetValue(this);
|
||||
}
|
||||
|
||||
if (unserializedMembersWarnOnceCache.TryGetValue(m.Name, out object cachedValue))
|
||||
{
|
||||
if (Equals(value, cachedValue))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
unserializedMembersWarnOnceCache[m.Name] = value;
|
||||
return $" - {m.Name}: {value}";
|
||||
})
|
||||
.Where(i => i != null)
|
||||
.ToArray();
|
||||
if (unserializedProps.Length > 0)
|
||||
{
|
||||
Log.Warn($"{FileName} is using default values for the missing properties:{Environment.NewLine}{string.Join(Environment.NewLine, unserializedProps)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Serialize(string saveDir)
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
Type type = GetType();
|
||||
Dictionary<string, MemberInfo> typeCachedDict = GetTypeCacheDictionary();
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(saveDir);
|
||||
using StreamWriter stream = new(new FileStream(Path.Combine(saveDir, FileName), FileMode.Create, FileAccess.Write), Encoding.UTF8);
|
||||
WritePropertyDescription(type, stream);
|
||||
|
||||
foreach (string name in typeCachedDict.Keys)
|
||||
{
|
||||
MemberInfo member = typeCachedDict[name];
|
||||
|
||||
FieldInfo field = member as FieldInfo;
|
||||
if (field != null)
|
||||
{
|
||||
WritePropertyDescription(member, stream);
|
||||
NitroxConfig<T>.WriteProperty(field, field.GetValue(this), stream);
|
||||
}
|
||||
|
||||
PropertyInfo property = member as PropertyInfo;
|
||||
if (property != null)
|
||||
{
|
||||
WritePropertyDescription(member, stream);
|
||||
NitroxConfig<T>.WriteProperty(property, property.GetValue(this), stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
Log.Error($"Config file {FileName} exists but is a hidden file and cannot be modified, config file will not be updated. Please make file accessible");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures updates are properly persisted to the backing config file without overwriting user edits.
|
||||
/// </summary>
|
||||
public UpdateDiposable Update(string saveDir)
|
||||
{
|
||||
return new UpdateDiposable(this, saveDir);
|
||||
}
|
||||
|
||||
private static Dictionary<string, MemberInfo> GetTypeCacheDictionary()
|
||||
{
|
||||
Type type = typeof(T);
|
||||
if (typeCache.Count == 0)
|
||||
{
|
||||
IEnumerable<MemberInfo> members = type.GetFields()
|
||||
.Where(f => f.Attributes != FieldAttributes.NotSerialized)
|
||||
.Concat(type.GetProperties()
|
||||
.Where(p => p.CanWrite)
|
||||
.Cast<MemberInfo>());
|
||||
|
||||
try
|
||||
{
|
||||
foreach (MemberInfo member in members)
|
||||
{
|
||||
typeCache.Add(member.Name.ToLowerInvariant(), member);
|
||||
}
|
||||
}
|
||||
catch (ArgumentException e)
|
||||
{
|
||||
Log.Error(e, $"Type {type.FullName} has properties that require case-sensitivity to be unique which is unsuitable for .properties format.");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
return typeCache;
|
||||
}
|
||||
|
||||
private static string StringifyValue(object value) => value switch
|
||||
{
|
||||
string _ => $@"""{value}""",
|
||||
null => @"""""",
|
||||
_ => value.ToString()
|
||||
};
|
||||
|
||||
private static bool SetMemberValue(NitroxConfig<T> instance, MemberInfo member, string valueFromFile)
|
||||
{
|
||||
object ConvertFromStringOrDefault(Type typeOfValue, out bool isDefault, object defaultValue = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
object newValue = TypeDescriptor.GetConverter(typeOfValue).ConvertFrom(null!, CultureInfo.InvariantCulture, valueFromFile);
|
||||
isDefault = false;
|
||||
return newValue;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
isDefault = true;
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
bool usedDefault;
|
||||
switch (member)
|
||||
{
|
||||
case FieldInfo field:
|
||||
field.SetValue(instance, ConvertFromStringOrDefault(field.FieldType, out usedDefault, field.GetValue(instance)));
|
||||
return !usedDefault;
|
||||
case PropertyInfo prop:
|
||||
prop.SetValue(instance, ConvertFromStringOrDefault(prop.PropertyType, out usedDefault, prop.GetValue(instance)));
|
||||
return !usedDefault;
|
||||
default:
|
||||
throw new Exception($"Serialized member must be field or property: {member}.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteProperty<TMember>(TMember member, object value, StreamWriter stream) where TMember : MemberInfo
|
||||
{
|
||||
stream.Write(member.Name);
|
||||
stream.Write('=');
|
||||
stream.WriteLine(Convert.ToString(value, CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
private void WritePropertyDescription(MemberInfo member, StreamWriter stream)
|
||||
{
|
||||
PropertyDescriptionAttribute attribute = member.GetCustomAttribute<PropertyDescriptionAttribute>();
|
||||
if (attribute != null)
|
||||
{
|
||||
foreach (string line in attribute.Description.Split(newlineChars))
|
||||
{
|
||||
stream.Write("# ");
|
||||
stream.WriteLine(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct UpdateDiposable : IDisposable
|
||||
{
|
||||
private string SaveDir { get; }
|
||||
private NitroxConfig<T> Config { get; }
|
||||
|
||||
public UpdateDiposable(NitroxConfig<T> config, string saveDir)
|
||||
{
|
||||
config.Deserialize(saveDir);
|
||||
SaveDir = saveDir;
|
||||
Config = config;
|
||||
}
|
||||
|
||||
public void Dispose() => Config.Serialize(SaveDir);
|
||||
}
|
||||
}
|
160
NitroxModel/Serialization/ServerList.cs
Normal file
160
NitroxModel/Serialization/ServerList.cs
Normal file
@@ -0,0 +1,160 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using NitroxModel.Helper;
|
||||
|
||||
namespace NitroxModel.Serialization;
|
||||
|
||||
public class ServerList
|
||||
{
|
||||
private const string SERVERS_FILE_NAME = "servers";
|
||||
public const int DEFAULT_PORT = 11000;
|
||||
private static ServerList instance;
|
||||
private readonly List<Entry> entries = new();
|
||||
public static ServerList Instance => instance ??= Refresh();
|
||||
|
||||
private static ServerList Default
|
||||
{
|
||||
get
|
||||
{
|
||||
ServerList list = new();
|
||||
list.Add(new Entry("Your server", "127.0.0.1", DEFAULT_PORT));
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
public static string DefaultFile => Path.Combine(NitroxUser.AppDataPath, SERVERS_FILE_NAME);
|
||||
|
||||
public IEnumerable<Entry> Entries => entries;
|
||||
|
||||
public static ServerList Refresh()
|
||||
{
|
||||
return instance = From();
|
||||
}
|
||||
|
||||
public static ServerList From()
|
||||
{
|
||||
// Create file if it doesn't exist yet.
|
||||
string file = DefaultFile;
|
||||
if (!File.Exists(file))
|
||||
{
|
||||
instance = Default;
|
||||
instance.Save();
|
||||
return instance;
|
||||
}
|
||||
|
||||
ServerList list = new();
|
||||
foreach (string line in File.ReadAllLines(file))
|
||||
{
|
||||
Entry entry = Entry.FromLine(line);
|
||||
if (entry == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
list.entries.Add(entry);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public void Add(Entry entry)
|
||||
{
|
||||
entries.Add(entry);
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
string file = DefaultFile;
|
||||
|
||||
using StreamWriter writer = new(new FileStream(file, FileMode.Create, FileAccess.Write));
|
||||
foreach (Entry entry in entries)
|
||||
{
|
||||
if (entry.Persist)
|
||||
{
|
||||
writer.WriteLine(entry.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveAt(int index)
|
||||
{
|
||||
if (index < 0 || index >= entries.Count)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(index));
|
||||
}
|
||||
|
||||
entries.RemoveAt(index);
|
||||
}
|
||||
|
||||
public class Entry
|
||||
{
|
||||
public string Name { get; }
|
||||
public string Address { get; }
|
||||
public int Port { get; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, entry will be saved to storage.
|
||||
/// </summary>
|
||||
public bool Persist { get; }
|
||||
|
||||
public Entry(string name, string address, int port, bool persist = true)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new ArgumentException("name in ServerList.Entry constructor can't be null or whitespace");
|
||||
}
|
||||
Validate.NotNull(address);
|
||||
|
||||
Name = name.Trim();
|
||||
Address = address.Trim();
|
||||
Port = port;
|
||||
Persist = persist;
|
||||
}
|
||||
|
||||
public Entry(string name, IPAddress address, int port, bool persist = true) : this(name, address.ToString(), port, persist)
|
||||
{
|
||||
}
|
||||
|
||||
public static Entry FromLine(string line)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
string[] parts = line.Split('|');
|
||||
int port;
|
||||
string address;
|
||||
switch (parts.Length)
|
||||
{
|
||||
case 2:
|
||||
// Split from address as format "hostname:port".
|
||||
string[] addressSplit = parts[1].Split(':');
|
||||
address = addressSplit[0];
|
||||
if (!int.TryParse(addressSplit.ElementAtOrDefault(1), out port))
|
||||
{
|
||||
port = DEFAULT_PORT;
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
address = parts[1].Trim();
|
||||
if (!int.TryParse(parts[2], out port))
|
||||
{
|
||||
port = DEFAULT_PORT;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Exception($"Expected server entry to have 2 or 3 parts: {line}");
|
||||
}
|
||||
|
||||
string name = parts[0].Trim();
|
||||
return new Entry(name, address, port);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Name}|{Address}|{Port}";
|
||||
}
|
||||
}
|
||||
}
|
130
NitroxModel/Serialization/SubnauticaServerConfig.cs
Normal file
130
NitroxModel/Serialization/SubnauticaServerConfig.cs
Normal file
@@ -0,0 +1,130 @@
|
||||
using NitroxModel.DataStructures.GameLogic;
|
||||
using NitroxModel.Helper;
|
||||
using NitroxModel.Server;
|
||||
|
||||
namespace NitroxModel.Serialization
|
||||
{
|
||||
[PropertyDescription("Server settings can be changed here")]
|
||||
public class SubnauticaServerConfig : NitroxConfig<SubnauticaServerConfig>
|
||||
{
|
||||
private int maxConnectionsSetting = 100;
|
||||
|
||||
private int initialSyncTimeoutSetting = 300000;
|
||||
|
||||
[PropertyDescription("Set to true to Cache entities for the whole map on next run. \nWARNING! Will make server load take longer on the cache run but players will gain a performance boost when entering new areas.")]
|
||||
public bool CreateFullEntityCache { get; set; } = false;
|
||||
|
||||
private int saveIntervalSetting = 120000;
|
||||
|
||||
private int maxBackupsSetting = 10;
|
||||
|
||||
private string postSaveCommandPath = string.Empty;
|
||||
|
||||
public override string FileName => "server.cfg";
|
||||
|
||||
[PropertyDescription("Leave blank for a random spawn position")]
|
||||
public string Seed { get; set; }
|
||||
|
||||
public int ServerPort { get; set; } = ServerList.DEFAULT_PORT;
|
||||
|
||||
[PropertyDescription("Prevents players from losing items on death")]
|
||||
public bool KeepInventoryOnDeath { get; set; } = false;
|
||||
|
||||
[PropertyDescription("Measured in milliseconds")]
|
||||
public int SaveInterval
|
||||
{
|
||||
get => saveIntervalSetting;
|
||||
|
||||
set
|
||||
{
|
||||
Validate.IsTrue(value >= 1000, "SaveInterval must be greater than 1000");
|
||||
saveIntervalSetting = value;
|
||||
}
|
||||
}
|
||||
|
||||
public int MaxBackups
|
||||
{
|
||||
get => maxBackupsSetting;
|
||||
|
||||
set
|
||||
{
|
||||
Validate.IsTrue(value >= 0, "MaxBackups must be greater than or equal to 0");
|
||||
maxBackupsSetting = value;
|
||||
}
|
||||
}
|
||||
|
||||
[PropertyDescription("Command to run following a successful world save (e.g. .exe, .bat, or PowerShell script). ")]
|
||||
public string PostSaveCommandPath
|
||||
{
|
||||
get => postSaveCommandPath;
|
||||
set => postSaveCommandPath = value?.Trim('"').Trim();
|
||||
}
|
||||
|
||||
public int MaxConnections
|
||||
{
|
||||
get => maxConnectionsSetting;
|
||||
|
||||
set
|
||||
{
|
||||
Validate.IsTrue(value > 0, "MaxConnections must be greater than 0");
|
||||
maxConnectionsSetting = value;
|
||||
}
|
||||
}
|
||||
|
||||
public int InitialSyncTimeout
|
||||
{
|
||||
get => initialSyncTimeoutSetting;
|
||||
|
||||
set
|
||||
{
|
||||
Validate.IsTrue(value > 30000, "InitialSyncTimeout must be greater than 30 seconds");
|
||||
initialSyncTimeoutSetting = value;
|
||||
}
|
||||
}
|
||||
|
||||
public bool DisableConsole { get; set; }
|
||||
|
||||
public bool DisableAutoSave { get; set; }
|
||||
|
||||
public bool DisableAutoBackup { get; set; }
|
||||
|
||||
public string ServerPassword { get; set; } = string.Empty;
|
||||
|
||||
public string AdminPassword { get; set; } = StringHelper.GenerateRandomString(12);
|
||||
|
||||
[PropertyDescription("Possible values:", typeof(NitroxGameMode))]
|
||||
public NitroxGameMode GameMode { get; set; } = NitroxGameMode.SURVIVAL;
|
||||
|
||||
[PropertyDescription("Possible values:", typeof(ServerSerializerMode))]
|
||||
public ServerSerializerMode SerializerMode { get; set; } = ServerSerializerMode.JSON;
|
||||
|
||||
[PropertyDescription("Possible values:", typeof(Perms))]
|
||||
public Perms DefaultPlayerPerm { get; set; } = Perms.PLAYER;
|
||||
|
||||
[PropertyDescription("\nDefault player stats below here")]
|
||||
public float DefaultOxygenValue { get; set; } = 45;
|
||||
|
||||
public float DefaultMaxOxygenValue { get; set; } = 45;
|
||||
public float DefaultHealthValue { get; set; } = 80;
|
||||
public float DefaultHungerValue { get; set; } = 50.5f;
|
||||
public float DefaultThirstValue { get; set; } = 90.5f;
|
||||
|
||||
[PropertyDescription("Recommended to keep at 0.1f which is the default starting value. If set to 0 then new players are cured by default.")]
|
||||
public float DefaultInfectionValue { get; set; } = 0.1f;
|
||||
|
||||
public PlayerStatsData DefaultPlayerStats => new(DefaultOxygenValue, DefaultMaxOxygenValue, DefaultHealthValue, DefaultHungerValue, DefaultThirstValue, DefaultInfectionValue);
|
||||
[PropertyDescription("If set to true, the server will try to open port on your router via UPnP")]
|
||||
public bool AutoPortForward { get; set; } = true;
|
||||
[PropertyDescription("Determines whether the server will listen for and reply to LAN discovery requests.")]
|
||||
public bool LANDiscoveryEnabled { get; set; } = true;
|
||||
|
||||
[PropertyDescription("When true, will reject any build actions detected as desynced")]
|
||||
public bool SafeBuilding { get; set; } = true;
|
||||
|
||||
[PropertyDescription("When true and started in launcher, will use launcher UI as opposed to external window")]
|
||||
public bool IsEmbedded { get; set; } = true;
|
||||
|
||||
[PropertyDescription("Activates/Deactivates Player versus Player damage/interactions")]
|
||||
public bool PvPEnabled { get; set; } = true;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user