diff --git a/JotunnLib/Documentation/tutorials/config.md b/JotunnLib/Documentation/tutorials/config.md index ea5f794f8..04fe60c52 100644 --- a/JotunnLib/Documentation/tutorials/config.md +++ b/JotunnLib/Documentation/tutorials/config.md @@ -67,6 +67,23 @@ Local settings will be overriden by the servers values as long as the client is Changing the configs at runtime will sync the changes to all clients connected to the server. +## Admin Only Strictness + +Usually, the `IsAdminOnly` flag is enforcing players to be admin on the server to change the configuration. +However, without having Jotunn installed on the server the admin status cannot be detected reliably. + +To change this behaviour, the `SynchronizationMode` can be set to `AdminOnlyStrictness.IfOnServer`. +This means `IsAdminOnly` configs are only synced and enforced if the server has Jotunn installed. +If not installed on the server, all players are free to change any config values. +This can useful for mods that want to behave like client-only mods on vanilla servers but still sync configs on modded servers. +```cs +[SynchronizationMode(AdminOnlyStrictness.IfOnServer)] +internal class TestMod : BaseUnityPlugin +{ +... +} +``` + ## Synced admin status Upon connection to a server, Jötunn checks the admin status of the connecting player on that server, given that Jötunn is installed on both sides. diff --git a/JotunnLib/Extensions/PluginExtensions.cs b/JotunnLib/Extensions/PluginExtensions.cs new file mode 100644 index 000000000..122ec148e --- /dev/null +++ b/JotunnLib/Extensions/PluginExtensions.cs @@ -0,0 +1,26 @@ +using System; +using System.Linq; +using BepInEx; +using Jotunn.Utils; + +namespace Jotunn.Extensions +{ + internal static class PluginExtensions + { + internal static NetworkCompatibilityAttribute GetNetworkCompatibilityAttribute(this BaseUnityPlugin plugin) + { + return plugin.GetType() + .GetCustomAttributes(typeof(NetworkCompatibilityAttribute), true) + .Cast() + .FirstOrDefault(); + } + + internal static SynchronizationModeAttribute GetSynchronizationModeAttribute(this BaseUnityPlugin plugin) + { + return plugin.GetType() + .GetCustomAttributes(typeof(SynchronizationModeAttribute), true) + .Cast() + .FirstOrDefault(); + } + } +} diff --git a/JotunnLib/Managers/SynchronizationManager.cs b/JotunnLib/Managers/SynchronizationManager.cs index 8d488b538..4c2433cdf 100644 --- a/JotunnLib/Managers/SynchronizationManager.cs +++ b/JotunnLib/Managers/SynchronizationManager.cs @@ -7,6 +7,7 @@ using BepInEx.Configuration; using HarmonyLib; using Jotunn.Entities; +using Jotunn.Extensions; using Jotunn.Utils; using UnityEngine; using UnityEngine.SceneManagement; @@ -464,6 +465,10 @@ private void InvokeOnAdminStatusChanged() OnAdminStatusChanged?.SafeInvoke(); } + /// + /// Gets an IEnumerable of all default and custom config files that associated with plugins that have Jotunn as a dependency. + /// + /// private IEnumerable GetConfigFiles() { var loadedPlugins = BepInExUtils.GetDependentPlugins(true); @@ -479,11 +484,71 @@ private IEnumerable GetConfigFiles() } } + /// + /// Checks if AdminOnly config entries should be locked based the AdminOnlyStrictness value for the plugin that the + /// config file is attached to (including custom config files) and whether the plugin is installed on the server or not. + /// + /// + /// + private bool ShouldManageConfig(ConfigFile config) + { + if (!GetPluginGUID(config, out var pluginGUID)) + { + return false; + } + + if (!BepInExUtils.GetDependentPlugins().TryGetValue(pluginGUID, out var plugin)) + { + return false; + } + + return ShouldManageConfig(plugin); + } + + /// + /// Checks if AdminOnly config entries should be locked based the AdminOnlyStrictness value for the plugin + /// and whether the plugin is installed on the server or not. + /// + /// + /// + private bool ShouldManageConfig(BaseUnityPlugin plugin) + { + if (ModCompatibility.IsModuleOnServer(plugin)) + { + return true; + } + + // Current behaviour is that AdminOnly config entries are always locked + // if Jotunn is not on the server. So if SynchronizationModeAttribute has + // not been set for the mod then return true to mimic current behaviour and + // maintain backwards compatibility. + SynchronizationModeAttribute syncMode = plugin.GetSynchronizationModeAttribute(); + return syncMode == null || syncMode.ShouldAlwaysEnforceAdminOnly(); + } + private static string GetFileIdentifier(ConfigFile config) { return config.ConfigFilePath.Replace(BepInEx.Paths.ConfigPath, "").Replace("\\", "/").Trim('/'); } + /// + /// Gets the corresponding Plugin GUID for a config file (works for custom config files) + /// and returns a boolean indicating success or failure. + /// + /// + /// + private bool GetPluginGUID(ConfigFile config, out string pluginGUID) + { + var configFileIdentifier = GetFileIdentifier(config); + return GetPluginGUID(configFileIdentifier, out pluginGUID); + } + + /// + /// Gets the corresponding Plugin GUID for a config file identifier (works for custom config files) + /// and returns a boolean indicating success or failure. + /// + /// + /// private bool GetPluginGUID(string configFileIdentifier, out string pluginGUID) { if (IsDefaultModConfig(configFileIdentifier, out pluginGUID)) @@ -556,6 +621,11 @@ private void LockConfigurationEntries() { foreach (var config in GetConfigFiles()) { + if (!ShouldManageConfig(config)) + { + continue; + } + foreach (var configDefinition in config.Keys) { var configEntry = config[configDefinition.Section, configDefinition.Key]; @@ -808,6 +878,11 @@ private void SetToDefaultConfigEntries() { foreach (var config in GetConfigFiles()) { + if (!ShouldManageConfig(config)) + { + continue; + } + foreach (var configDefinition in config.Keys) { var configEntry = config[configDefinition.Section, configDefinition.Key]; diff --git a/JotunnLib/Utils/ModCompatibility/ModCompatibility.cs b/JotunnLib/Utils/ModCompatibility/ModCompatibility.cs index 4b5e6f7e1..5cc2cf649 100644 --- a/JotunnLib/Utils/ModCompatibility/ModCompatibility.cs +++ b/JotunnLib/Utils/ModCompatibility/ModCompatibility.cs @@ -1,10 +1,11 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Text; +using BepInEx; +using BepInEx.Bootstrap; using HarmonyLib; +using Jotunn.Extensions; using Jotunn.Managers; using UnityEngine; using UnityEngine.UI; @@ -20,7 +21,7 @@ public static class ModCompatibility /// /// Stores the last server message. /// - private static ZPackage LastServerVersion; + private static ServerVersionData LastServerVersionData = new ServerVersionData(); private static readonly Dictionary ClientVersions = new Dictionary(); @@ -30,11 +31,56 @@ internal static void Init() Main.Harmony.PatchAll(typeof(ModCompatibility)); } + /// + /// Check if a mod is installed and loaded on the server.
+ /// Can be called from both client and server. + ///
+ /// BepInEx mod to check + /// true if the mod is loaded on the server.
false if the mod is not loaded on the server or no server connection is established
+ public static bool IsModuleOnServer(BaseUnityPlugin plugin) + { + return IsModuleOnServer(plugin.Info.Metadata.GUID); + } + + /// + /// Check if a mod is installed and loaded on the server.
+ /// Can be called from both client and server. + ///
+ /// BepInEx mod GUID to check + /// true if the mod is loaded on the server.
false if the mod is not loaded on the server or no server connection is established
+ public static bool IsModuleOnServer(string modGUID) + { + if (ZNet.instance) + { + if (ZNet.instance.IsClientInstance()) + { + return LastServerVersionData.IsValid() && LastServerVersionData.moduleGUIDs.Contains(modGUID); + } + + if (ZNet.instance.IsServer()) + { + return Chainloader.PluginInfos.ContainsKey(modGUID); + } + } + + return false; + } + + /// + /// Check if Jotunn is installed and loaded on the server.
+ /// Can be called from both client and server. + ///
+ /// true if Jotunn is loaded on the server.
false if Jotunn is not loaded on the server or no server connection is established
+ public static bool IsJotunnOnServer() + { + return IsModuleOnServer(Main.ModGuid); + } + [HarmonyPatch(typeof(ZNet), nameof(ZNet.OnNewConnection)), HarmonyPrefix, HarmonyPriority(Priority.First)] private static void ZNet_OnNewConnection(ZNet __instance, ZNetPeer peer) { // clear the previous connection, if existing - LastServerVersion = null; + LastServerVersionData.Reset(); // Register our RPC very early peer.m_rpc.Register(nameof(RPC_Jotunn_ReceiveVersionData), RPC_Jotunn_ReceiveVersionData); @@ -58,7 +104,7 @@ private static void ZNet_RPC_ServerHandshake(ZNet __instance, ZRpc rpc) [HarmonyPatch(typeof(FejdStartup), nameof(FejdStartup.ShowConnectError)), HarmonyPostfix, HarmonyPriority(Priority.Last)] private static void FejdStartup_ShowConnectError(FejdStartup __instance) { - if (LastServerVersion != null && ZNet.m_connectionStatus == ZNet.ConnectionStatus.ErrorVersion) + if (LastServerVersionData.IsValid() && ZNet.m_connectionStatus == ZNet.ConnectionStatus.ErrorVersion) { string failedConnectionText = __instance.m_connectionFailedError.text; __instance.StartCoroutine(ShowModCompatibilityErrorMessage(failedConnectionText)); @@ -73,13 +119,13 @@ private static bool ZNet_SendPeerInfo(ZNet __instance, ZRpc rpc, string password if (ZNet.instance.IsClientInstance()) { // If there was no server version response, Jötunn is not installed. Cancel if we have mandatory mods - if (LastServerVersion == null && GetEnforcableMods().Any(x => x.IsNeededOnServer())) + if (!LastServerVersionData.IsValid() && GetEnforcableMods().Any(x => x.IsNeededOnServer())) { - string missingMods = string.Join(Environment.NewLine, GetEnforcableMods().Where(x => x.IsNeededOnServer()).Select(x => x.name)); + string missingMods = string.Join(Environment.NewLine, GetEnforcableMods().Where(x => x.IsNeededOnServer()).Select(x => x.ModName)); Logger.LogWarning("Jötunn is not installed on the server. Client has mandatory mods, cancelling connection. " + "Mods that need to be installed on the server:" + Environment.NewLine + missingMods); rpc.Invoke("Disconnect"); - LastServerVersion = new ModuleVersionData(new List()).ToZPackage(); + LastServerVersionData = new ServerVersionData(new List()); ZNet.m_connectionStatus = ZNet.ConnectionStatus.ErrorVersion; return false; } @@ -103,7 +149,7 @@ private static bool ZNet_RPC_PeerInfo(ZNet __instance, ZRpc rpc, ZPackage pkg) // There is a mod, which needs to be client side too // Lets disconnect the vanilla client with Incompatible Version message - string missingMods = string.Join(Environment.NewLine, GetEnforcableMods().Where(x => x.IsNeededOnClient()).Select(x => x.name)); + string missingMods = string.Join(Environment.NewLine, GetEnforcableMods().Where(x => x.IsNeededOnClient()).Select(x => x.ModName)); Logger.LogWarning("Jötunn is not installed on the client. Server has mandatory mods, cancelling connection. " + "Mods that need to be installed on the client:" + Environment.NewLine + missingMods); rpc.Invoke("Error", (int)ZNet.ConnectionStatus.ErrorVersion); @@ -154,10 +200,16 @@ private static void RPC_Jotunn_ReceiveVersionData(ZRpc sender, ZPackage data) } else { - LastServerVersion = data; + LastServerVersionData = new ServerVersionData(data); } } - + + /// + /// Compares version data on server and client. + /// + /// + /// + /// internal static bool CompareVersionData(ModuleVersionData serverData, ModuleVersionData clientData) { if (ReferenceEquals(serverData, clientData)) @@ -167,25 +219,39 @@ internal static bool CompareVersionData(ModuleVersionData serverData, ModuleVers bool result = true; + // Check for supported ModModule data layout + if (!clientData.IsSupportedDataLayout) + { + Logger.LogWarning($"Jotunn version on client is higher than server version: {Main.Version}"); + result = false; + } + if (!serverData.IsSupportedDataLayout) + { + Logger.LogWarning($"Jotunn version on server is higher than client version: {Main.Version}"); + result = false; + } + // Check server enforced mods foreach (var serverModule in FindNotInstalledMods(serverData, clientData)) { - Logger.LogWarning($"Missing mod on client: {serverModule.name}"); + Logger.LogWarning($"Missing mod on client: {serverModule.ModName}"); result = false; } // Check client enforced mods foreach (var clientModule in FindAdditionalMods(serverData, clientData)) { - Logger.LogWarning($"Client loaded additional mod: {clientModule.name}"); + Logger.LogWarning($"Client loaded additional mod: {clientModule.ModName}"); result = false; } + bool legacyDataLayout = Mathf.Min(serverData.ModModuleDataLayout, clientData.ModModuleDataLayout) == ModModule.LegacyDataLayoutVersion; + // Check versions foreach (var serverModule in FindLowerVersionMods(serverData, clientData).Union(FindHigherVersionMods(serverData, clientData))) { - var clientModule = clientData.FindModule(serverModule.name); - Logger.LogWarning($"Mod version mismatch {serverModule.name}: Server {serverModule.version}, Client {clientModule.version}"); + var clientModule = clientData.FindModule(serverModule, legacyDataLayout); + Logger.LogWarning($"Mod version mismatch {serverModule.ModName}: Server {serverModule.Version}, Client {clientModule.Version}"); result = false; } @@ -230,7 +296,7 @@ private static CompatibilityWindow LoadCompatWindow() private static IEnumerator ShowModCompatibilityErrorMessage(string failedConnectionText) { var compatWindow = LoadCompatWindow(); - var remote = new ModuleVersionData(LastServerVersion); + var remote = LastServerVersionData.moduleVersionData; var local = new ModuleVersionData(GetEnforcableMods().ToList()); // print issues to console @@ -257,7 +323,7 @@ private static IEnumerator ShowModCompatibilityErrorMessage(string failedConnect compatWindow.scrollRect.verticalNormalizedPosition = 1f; // Reset the last server version - LastServerVersion = null; + LastServerVersionData.Reset(); } private static void OpenLogFile() @@ -286,6 +352,28 @@ private static string CreateErrorMessage(ModuleVersionData serverData, ModuleVer CreateFurtherStepsMessage(); } + private static string CreateModModuleLayoutErrorMessage(ModuleVersionData serverData, ModuleVersionData clientData) + { + + if (!clientData.IsSupportedDataLayout) + { + return ColoredLine(Color.red, $"Jotunn version on client is higher than server version: {Main.Version}"); + } + + if (!serverData.IsSupportedDataLayout) + { + return ColoredLine(Color.red, $"Jotunn version on server is higher than client version: {Main.Version}"); + } + + if (serverData.ModModuleDataLayout != clientData.ModModuleDataLayout) + { + return ColoredLine(Color.red, "Jotunn versions on server and client are not compatible."); + } + + return string.Empty; + } + + private static string CreateVanillaVersionErrorMessage(ModuleVersionData serverData, ModuleVersionData clientData) { if (serverData.NetworkVersion <= 0 || clientData.NetworkVersion <= 0) @@ -323,7 +411,7 @@ private static string CreateNotInstalledErrorMessage(ModuleVersionData serverDat return ColoredLine(Color.red, "$mod_compat_header_missing_mods") + ColoredLine(GUIManager.Instance.ValheimOrange, $"$mod_compat_missing_mods_description") + - string.Join("", matchingServerMods.Select(serverModule => ColoredLine(Color.white, "$mod_compat_missing_mod", $"{serverModule.name}", $"{serverModule.version}"))) + + string.Join("", matchingServerMods.Select(serverModule => ColoredLine(Color.white, "$mod_compat_missing_mod", $"{serverModule.ModName}", $"{serverModule.Version}"))) + Environment.NewLine; } @@ -337,7 +425,7 @@ private static string CreateLowerVersionErrorMessage(ModuleVersionData serverDat } return ColoredLine(Color.red, "$mod_compat_header_update_needed") + - string.Join("", matchingServerMods.Select((serverModule) => ColoredLine(Color.white, "$mod_compat_mod_update", serverModule.name, serverModule.GetVersionString()))) + + string.Join("", matchingServerMods.Select((serverModule) => ColoredLine(Color.white, "$mod_compat_mod_update", serverModule.ModName, serverModule.GetVersionString()))) + Environment.NewLine; } @@ -351,7 +439,7 @@ private static string CreateHigherVersionErrorMessage(ModuleVersionData serverDa } return ColoredLine(Color.red, "$mod_compat_header_downgrade_needed") + - string.Join("", matchingServerMods.Select(serverModule => ColoredLine(Color.white, "$mod_compat_mod_downgrade", serverModule.name, serverModule.GetVersionString()))) + + string.Join("", matchingServerMods.Select(serverModule => ColoredLine(Color.white, "$mod_compat_mod_downgrade", serverModule.ModName, serverModule.GetVersionString()))) + Environment.NewLine; } @@ -366,7 +454,7 @@ private static string CreateAdditionalModsErrorMessage(ModuleVersionData serverD return ColoredLine(Color.red, "$mod_compat_header_additional_mods") + ColoredLine(GUIManager.Instance.ValheimOrange, "$mod_compat_additional_mods_description") + - string.Join("", matchingClientMods.Select(clientModule => ColoredLine(Color.white, "$mod_compat_additional_mod", clientModule.name, $"{clientModule.version}"))) + + string.Join("", matchingClientMods.Select(clientModule => ColoredLine(Color.white, "$mod_compat_additional_mod", clientModule.ModName, $"{clientModule.Version}"))) + Environment.NewLine; } @@ -397,7 +485,7 @@ private static List FindLowerVersionMods(ModuleVersionData serverData { return FindMods(serverData, clientData, (serverModule, clientModule) => { - return clientModule != null && ModModule.IsLowerVersion(serverModule, clientModule, serverModule.versionStrictness); + return clientModule != null && ModModule.IsLowerVersion(serverModule, clientModule, serverModule.VersionStrictness); }).ToList(); } @@ -405,15 +493,17 @@ private static List FindHigherVersionMods(ModuleVersionData serverDat { return FindMods(serverData, clientData, (serverModule, clientModule) => { - return clientModule != null && ModModule.IsLowerVersion(clientModule, serverModule, serverModule.versionStrictness); + return clientModule != null && ModModule.IsLowerVersion(clientModule, serverModule, serverModule.VersionStrictness); }).ToList(); } private static IEnumerable FindMods(ModuleVersionData baseModules, ModuleVersionData additionalModules, Func predicate) { + bool legacyDataLayout = Mathf.Min(baseModules.ModModuleDataLayout, additionalModules.ModModuleDataLayout) == ModModule.LegacyDataLayoutVersion; + foreach (ModModule baseModule in baseModules.Modules) { - ModModule additionalModule = additionalModules.FindModule(baseModule.name); + ModModule additionalModule = additionalModules.FindModule(baseModule, legacyDataLayout); if (predicate(baseModule, additionalModule)) { @@ -430,10 +520,7 @@ internal static IEnumerable GetEnforcableMods() { foreach (var plugin in BepInExUtils.GetDependentPlugins(true).OrderBy(x => x.Key)) { - var networkCompatibilityAttribute = plugin.Value.GetType() - .GetCustomAttributes(typeof(NetworkCompatibilityAttribute), true) - .Cast() - .FirstOrDefault(); + var networkCompatibilityAttribute = plugin.Value.GetNetworkCompatibilityAttribute(); if (networkCompatibilityAttribute != null) { diff --git a/JotunnLib/Utils/ModCompatibility/ModModule.cs b/JotunnLib/Utils/ModCompatibility/ModModule.cs index 0ad9f0caa..794b54706 100644 --- a/JotunnLib/Utils/ModCompatibility/ModModule.cs +++ b/JotunnLib/Utils/ModCompatibility/ModModule.cs @@ -1,68 +1,170 @@ -using BepInEx; +using System; +using System.Collections.Generic; +using System.Linq; +using BepInEx; namespace Jotunn.Utils { internal class ModModule { - public string name; - public System.Version version; - public CompatibilityLevel compatibilityLevel; - public VersionStrictness versionStrictness; + public const int LegacyDataLayoutVersion = 0; + public const int CurrentDataLayoutVersion = 1; + public static readonly HashSet SupportedDataLayouts = new HashSet { LegacyDataLayoutVersion, CurrentDataLayoutVersion }; - public ModModule(string name, System.Version version, CompatibilityLevel compatibilityLevel, VersionStrictness versionStrictness) + /// + /// DataLayoutVersion indicates the version layout of data within the ZPkg. If equal to 0 then it is a legacy format. + /// + public int DataLayoutVersion { get; private set; } + + private string guid; + + /// + /// Identifier for mod based on DataLayoutVersion. + /// For legacy layout returns mod name, otherwise returns mod GUID. + /// + public string ModID + { + get + { + return DataLayoutVersion == LegacyDataLayoutVersion ? ModName : guid; + } + } + + /// + /// Friendly version of mod name. + /// + public string ModName { get; } + + /// + /// Version data for mod. + /// + public System.Version Version { get; } + + /// + /// Compatibility level of the mod. + /// + public CompatibilityLevel CompatibilityLevel { get; } + + /// + /// Version strictness level of the mod. + /// + public VersionStrictness VersionStrictness { get; } + + /// + /// Whether the data layout is a legacy format or not. + /// + public bool IsLegacyDataLayout { - this.name = name; - this.version = version; - this.compatibilityLevel = compatibilityLevel; - this.versionStrictness = versionStrictness; + get + { + return CurrentDataLayoutVersion == LegacyDataLayoutVersion; + } } - public ModModule(ZPackage pkg) + public ModModule(string guid, string name, System.Version version, CompatibilityLevel compatibilityLevel, VersionStrictness versionStrictness) { - name = pkg.ReadString(); - int major = pkg.ReadInt(); - int minor = pkg.ReadInt(); - int build = pkg.ReadInt(); - version = build >= 0 ? new System.Version(major, minor, build) : new System.Version(major, minor); - compatibilityLevel = (CompatibilityLevel)pkg.ReadInt(); - versionStrictness = (VersionStrictness)pkg.ReadInt(); + this.DataLayoutVersion = CurrentDataLayoutVersion; + this.guid = guid; + this.ModName = name; + this.Version = version; + this.CompatibilityLevel = compatibilityLevel; + this.VersionStrictness = versionStrictness; } - public void WriteToPackage(ZPackage pkg) + public ModModule(ZPackage pkg, bool legacy) { - pkg.Write(name); - pkg.Write(version.Major); - pkg.Write(version.Minor); - pkg.Write(version.Build); - pkg.Write((int)compatibilityLevel); - pkg.Write((int)versionStrictness); + if (legacy) + { + DataLayoutVersion = LegacyDataLayoutVersion; + ModName = pkg.ReadString(); + int major = pkg.ReadInt(); + int minor = pkg.ReadInt(); + int build = pkg.ReadInt(); + Version = build >= 0 ? new System.Version(major, minor, build) : new System.Version(major, minor); + CompatibilityLevel = (CompatibilityLevel)pkg.ReadInt(); + VersionStrictness = (VersionStrictness)pkg.ReadInt(); + return; + } + + // Handle deserialization based on dataLayoutVersion + DataLayoutVersion = pkg.ReadInt(); + + if (!this.IsSupportedDataLayout()) + { + // Data from a newer version of Jotunn has been received and cannot be read. + throw new NotSupportedException($"{DataLayoutVersion} is not a supported data layout version."); + } + + if (DataLayoutVersion == 1) + { + guid = pkg.ReadString(); + ModName = pkg.ReadString(); + int major = pkg.ReadInt(); + int minor = pkg.ReadInt(); + int build = pkg.ReadInt(); + Version = build >= 0 ? new System.Version(major, minor, build) : new System.Version(major, minor); + CompatibilityLevel = (CompatibilityLevel)pkg.ReadInt(); + VersionStrictness = (VersionStrictness)pkg.ReadInt(); + } + } + + /// + /// Write to ZPkg + /// + /// + /// + public void WriteToPackage(ZPackage pkg, bool legacy) + { + if (legacy) + { + pkg.Write(ModName); + pkg.Write(Version.Major); + pkg.Write(Version.Minor); + pkg.Write(Version.Build); + pkg.Write((int)CompatibilityLevel); + pkg.Write((int)VersionStrictness); + return; + } + + pkg.Write(DataLayoutVersion); + pkg.Write(guid); + pkg.Write(ModName); + pkg.Write(Version.Major); + pkg.Write(Version.Minor); + pkg.Write(Version.Build); + pkg.Write((int)CompatibilityLevel); + pkg.Write((int)VersionStrictness); } public ModModule(BepInPlugin plugin, NetworkCompatibilityAttribute networkAttribute) { - this.name = plugin.Name; - this.version = plugin.Version; - this.compatibilityLevel = networkAttribute.EnforceModOnClients; - this.versionStrictness = networkAttribute.EnforceSameVersion; + this.DataLayoutVersion = CurrentDataLayoutVersion; + this.guid = plugin.GUID; + this.ModName = plugin.Name; + this.Version = plugin.Version; + this.CompatibilityLevel = networkAttribute.EnforceModOnClients; + this.VersionStrictness = networkAttribute.EnforceSameVersion; } public ModModule(BepInPlugin plugin) { - this.name = plugin.Name; - this.version = plugin.Version; - this.compatibilityLevel = CompatibilityLevel.NotEnforced; - this.versionStrictness = VersionStrictness.None; + this.DataLayoutVersion = CurrentDataLayoutVersion; + this.guid = plugin.GUID; + this.ModName = plugin.Name; + this.Version = plugin.Version; + this.CompatibilityLevel = CompatibilityLevel.NotEnforced; + this.VersionStrictness = VersionStrictness.None; } public string GetVersionString() { - if (version.Build >= 0) + if (Version.Build >= 0) { - return $"{version.Major}.{version.Minor}.{version.Build}"; + return $"{Version.Major}.{Version.Minor}.{Version.Build}"; } else { - return $"{version.Major}.{version.Minor}"; + return $"{Version.Major}.{Version.Minor}"; } } @@ -72,7 +174,7 @@ public string GetVersionString() /// public bool IsNeededOnServer() { - return compatibilityLevel == CompatibilityLevel.EveryoneMustHaveMod || compatibilityLevel == CompatibilityLevel.ServerMustHaveMod; + return CompatibilityLevel == CompatibilityLevel.EveryoneMustHaveMod || CompatibilityLevel == CompatibilityLevel.ServerMustHaveMod; } /// @@ -81,7 +183,7 @@ public bool IsNeededOnServer() /// public bool IsNeededOnClient() { - return compatibilityLevel == CompatibilityLevel.EveryoneMustHaveMod || compatibilityLevel == CompatibilityLevel.ClientMustHaveMod; + return CompatibilityLevel == CompatibilityLevel.EveryoneMustHaveMod || CompatibilityLevel == CompatibilityLevel.ClientMustHaveMod; } /// @@ -91,7 +193,7 @@ public bool IsNeededOnClient() public bool IsNotEnforced() { #pragma warning disable CS0618 // Type or member is obsolete - return compatibilityLevel == CompatibilityLevel.NotEnforced || compatibilityLevel == CompatibilityLevel.NoNeedForSync; + return CompatibilityLevel == CompatibilityLevel.NotEnforced || CompatibilityLevel == CompatibilityLevel.NoNeedForSync; #pragma warning restore CS0618 // Type or member is obsolete } @@ -102,10 +204,20 @@ public bool IsNotEnforced() public bool OnlyVersionCheck() { #pragma warning disable CS0618 // Type or member is obsolete - return compatibilityLevel == CompatibilityLevel.OnlySyncWhenInstalled || compatibilityLevel == CompatibilityLevel.VersionCheckOnly; + return CompatibilityLevel == CompatibilityLevel.OnlySyncWhenInstalled || CompatibilityLevel == CompatibilityLevel.VersionCheckOnly; #pragma warning restore CS0618 // Type or member is obsolete } + /// + /// Module is formatted as in one of the supported data layout versions. + /// Should return false is data was received from a newer version of Jotunn. + /// + /// + public bool IsSupportedDataLayout() + { + return SupportedDataLayouts.Contains(DataLayoutVersion); + } + /// /// Checks if the compare module has a lower version then the other base module /// @@ -120,12 +232,12 @@ public static bool IsLowerVersion(ModModule baseModule, ModModule compareModule, return false; } - bool majorSmaller = compareModule.version.Major < baseModule.version.Major; - bool minorSmaller = compareModule.version.Minor < baseModule.version.Minor; - bool patchSmaller = compareModule.version.Build < baseModule.version.Build; + bool majorSmaller = compareModule.Version.Major < baseModule.Version.Major; + bool minorSmaller = compareModule.Version.Minor < baseModule.Version.Minor; + bool patchSmaller = compareModule.Version.Build < baseModule.Version.Build; - bool majorEqual = compareModule.version.Major == baseModule.version.Major; - bool minorEqual = compareModule.version.Minor == baseModule.version.Minor; + bool majorEqual = compareModule.Version.Major == baseModule.Version.Major; + bool minorEqual = compareModule.Version.Minor == baseModule.Version.Minor; if (strictness >= VersionStrictness.Major && majorSmaller) { diff --git a/JotunnLib/Utils/ModCompatibility/ModuleVersionData.cs b/JotunnLib/Utils/ModCompatibility/ModuleVersionData.cs index d0f79229b..997693d3e 100644 --- a/JotunnLib/Utils/ModCompatibility/ModuleVersionData.cs +++ b/JotunnLib/Utils/ModCompatibility/ModuleVersionData.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -24,6 +24,20 @@ internal class ModuleVersionData public uint NetworkVersion { get; internal set; } + public int ModModuleDataLayout { get; private set; } + + /// + /// Whether all the ModModule instances were formatted in a supported + /// data layout and all instances had the same data layout. + /// + public bool IsSupportedDataLayout + { + get + { + return ModModule.SupportedDataLayouts.Contains(ModModuleDataLayout); + } + } + /// /// Create from module data /// @@ -34,6 +48,7 @@ internal ModuleVersionData(List versionData) VersionString = GetVersionString(); NetworkVersion = GameVersions.NetworkVersion; Modules = new List(versionData); + ModModuleDataLayout = GetModModuleDataLayoutVersion(Modules); } internal ModuleVersionData(System.Version valheimVersion, List versionData) @@ -42,6 +57,7 @@ internal ModuleVersionData(System.Version valheimVersion, List versio VersionString = GetVersionString(); NetworkVersion = GameVersions.NetworkVersion; Modules = new List(versionData); + ModModuleDataLayout = GetModModuleDataLayoutVersion(Modules); } /// @@ -56,12 +72,12 @@ internal ModuleVersionData(ZPackage pkg) pkg.SetPos(0); ValheimVersion = new System.Version(pkg.ReadInt(), pkg.ReadInt(), pkg.ReadInt()); - var numberOfModules = pkg.ReadInt(); - - while (numberOfModules > 0) + // Read encoded ModModules in legacy format + var numberOfLegacyModules = pkg.ReadInt(); + while (numberOfLegacyModules > 0) { - Modules.Add(new ModModule(pkg)); - numberOfModules--; + Modules.Add(new ModModule(pkg, legacy: true)); + numberOfLegacyModules--; } if (pkg.m_reader.BaseStream.Position != pkg.m_reader.BaseStream.Length) @@ -73,6 +89,40 @@ internal ModuleVersionData(ZPackage pkg) { NetworkVersion = pkg.ReadUInt(); } + + // Get current data layout ModModules if present + if (pkg.m_reader.BaseStream.Position != pkg.m_reader.BaseStream.Length) + { + var modModules = new List(); + + // Read and store the relevant ModModules + var numberOfModules = pkg.ReadInt(); + var currentPos = pkg.GetPos(); + + while (numberOfModules > 0) + { + try + { + var modModule = new ModModule(pkg, legacy: false); + modModules.Add(modModule); + numberOfModules--; + } + catch (NotSupportedException ex) + { + pkg.SetPos(currentPos); // get data layout version + this.ModModuleDataLayout = pkg.ReadInt(); + Logger.LogError($"Could not parse unsupported data layout version {ModModuleDataLayout} from zPackage"); + Logger.LogError(ex.Message); + + // abort reading as the start of the next ModModule is unknown + break; + } + } + + // overwrite Modules to replace legacy ModModule formatted data with current ModModule formatted data + Modules = modModules; + ModModuleDataLayout = GetModModuleDataLayoutVersion(Modules); + } } catch (Exception ex) { @@ -96,12 +146,18 @@ public ZPackage ToZPackage() foreach (var module in Modules) { - module.WriteToPackage(pkg); + module.WriteToPackage(pkg, legacy: true); } pkg.Write(VersionString); pkg.Write(NetworkVersion); + pkg.Write(Modules.Count); + foreach (var module in Modules) + { + module.WriteToPackage(pkg, legacy: false); + } + return pkg; } @@ -130,7 +186,7 @@ public override string ToString() foreach (var mod in Modules) { - sb.AppendLine($"{mod.name} {mod.GetVersionString()} {mod.compatibilityLevel} {mod.versionStrictness}"); + sb.AppendLine($"{mod.ModName} {mod.GetVersionString()} {mod.CompatibilityLevel} {mod.VersionStrictness}"); } return sb.ToString(); @@ -159,20 +215,25 @@ public string ToString(bool showEnforce) foreach (var mod in Modules) { - sb.AppendLine($"{mod.name} {mod.GetVersionString()}" + (showEnforce ? $" {mod.compatibilityLevel} {mod.versionStrictness}" : "")); + sb.AppendLine($"{mod.ModName} {mod.GetVersionString()}" + (showEnforce ? $" {mod.CompatibilityLevel} {mod.VersionStrictness}" : "")); } return sb.ToString(); } - public ModModule FindModule(string name) + public ModModule FindModule(ModModule modModule, bool legacyDataLayout) { - return Modules.FirstOrDefault(x => x.name == name); + if (legacyDataLayout) + { + return Modules.FirstOrDefault(x => x.ModName == modModule.ModName); + } + + return Modules.FirstOrDefault(x => x.ModID == modModule.ModID); } - public bool HasModule(string name) + public bool HasModule(ModModule modModule, bool legacyDataLayout) { - return FindModule(name) != null; + return FindModule(modModule, legacyDataLayout) != null; } private static string GetVersionString() @@ -180,5 +241,15 @@ private static string GetVersionString() // ServerCharacters replaces the version string on the server but not client and does it's own checks afterwards return Version.GetVersionString().Replace("-ServerCharacters", ""); } + + private static int GetModModuleDataLayoutVersion(List modules) + { + // Handle tracking ModModule data layouts + if (modules.Any(x => x.DataLayoutVersion != modules.FirstOrDefault().DataLayoutVersion)) + { + throw new NotSupportedException("DataVersionLayout is not the same for all ModModule instances."); + } + return modules.FirstOrDefault().DataLayoutVersion; + } } } diff --git a/JotunnLib/Utils/ModCompatibility/NetworkCompatibilityAttribute.cs b/JotunnLib/Utils/ModCompatibility/NetworkCompatibilityAttribute.cs index 090f80626..ee7eba6ff 100644 --- a/JotunnLib/Utils/ModCompatibility/NetworkCompatibilityAttribute.cs +++ b/JotunnLib/Utils/ModCompatibility/NetworkCompatibilityAttribute.cs @@ -14,27 +14,33 @@ public enum CompatibilityLevel /// [Obsolete("Use NotEnforced instead")] NoNeedForSync = 0, + /// /// Mod is checked only if the client and server have loaded it and ignores if just one side has it. /// [Obsolete("Use VersionCheckOnly")] OnlySyncWhenInstalled = 1, + /// /// Mod must be loaded on server and client. Version checking depends on the VersionStrictness. /// EveryoneMustHaveMod = 2, + /// /// If mod is installed on the server, every client has to have it. VersionStrictness does apply when both sides have it. /// ClientMustHaveMod = 3, + /// /// If mod is installed on the client, the server has to have it. VersionStrictness does apply when both sides have it. /// ServerMustHaveMod = 4, + /// /// Version check is performed when both server and client have the mod, no check if the mod is actually installed. /// VersionCheckOnly = 5, + /// /// Mod is not checked at all, VersionsStrictness does not apply. /// @@ -51,14 +57,17 @@ public enum VersionStrictness : int /// No version check is done /// None = 0, + /// /// Mod must have the same Major version /// Major = 1, + /// /// Mods must have the same Minor version /// Minor = 2, + /// /// Mods must have the same Patch version /// @@ -66,16 +75,14 @@ public enum VersionStrictness : int } /// - /// Mod compatibility attribute
- ///
- /// PLEASE READ
- /// Example usage:
- /// If your mod adds its own RPCs, EnforceModOnClients is likely a must (otherwise clients would just discard the messages from the server), same version you do have to determine, if your sent data changed
- /// If your mod adds items, you always should enforce mods on client and same version (there could be nasty side effects with different versions of an item)
- /// If your mod is just GUI changes (for example bigger inventory, additional equip slots) there is no need to set this attribute + /// Mod compatibility attribute
+ ///
+ /// If your mod adds its own RPCs, EnforceModOnClients is likely a must (otherwise clients would just discard the messages from the server), same version you do have to determine, if your sent data changed.
+ /// If your mod adds items, you always should enforce mods on client and same version (there could be nasty side effects with different versions of an item).
+ /// If your mod is just GUI changes (for example bigger inventory, additional equip slots) there is no need to set this attribute ///
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Assembly)] - public class NetworkCompatibilityAttribute: Attribute + public class NetworkCompatibilityAttribute : Attribute { /// /// Compatibility Level diff --git a/JotunnLib/Utils/ModCompatibility/ServerVersionData.cs b/JotunnLib/Utils/ModCompatibility/ServerVersionData.cs new file mode 100644 index 000000000..f631a4d39 --- /dev/null +++ b/JotunnLib/Utils/ModCompatibility/ServerVersionData.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Jotunn.Utils +{ + internal class ServerVersionData + { + public ModuleVersionData moduleVersionData { get; set; } + public HashSet moduleGUIDs { get; set; } + + /// + /// Create empty ServerData + /// + internal ServerVersionData() { } + + /// + /// Create from module data + /// + /// + internal ServerVersionData(List versionData) + { + moduleVersionData = new ModuleVersionData(versionData); + moduleGUIDs = new HashSet(moduleVersionData.Modules.Where(x => x.ModID != null).Select(x => x.ModID).ToList()); + } + + internal ServerVersionData(System.Version valheimVersion, List versionData) + { + moduleVersionData = new ModuleVersionData(valheimVersion, versionData); + moduleGUIDs = new HashSet(moduleVersionData.Modules.Where(x => x.ModID != null).Select(x => x.ModID).ToList()); + } + + /// + /// Create from ZPackage + /// + /// + internal ServerVersionData(ZPackage pkg) + { + moduleVersionData = new ModuleVersionData(pkg); + moduleGUIDs = new HashSet(moduleVersionData.Modules.Where(x => x.ModID != null).Select(x => x.ModID).ToList()); + } + + internal bool IsValid() + { + return moduleVersionData != null && moduleGUIDs != null; + } + + internal void Reset() + { + moduleVersionData = null; + moduleGUIDs = null; + } + } +} diff --git a/JotunnLib/Utils/ModCompatibility/SynchronizationModeAttribute.cs b/JotunnLib/Utils/ModCompatibility/SynchronizationModeAttribute.cs new file mode 100644 index 000000000..7df3742f5 --- /dev/null +++ b/JotunnLib/Utils/ModCompatibility/SynchronizationModeAttribute.cs @@ -0,0 +1,54 @@ +using System; + + +namespace Jotunn.Utils +{ + /// + /// Enum used for telling whether AdminOnly settings for Config Entries should always be enforced + /// or if they should only be enforced when the mod is installed on the server. + /// + public enum AdminOnlyStrictness : int + { + /// + /// AdminOnly is always enforced for Config Entries even if the mod is not installed on the server. + /// This means that AdminOnly configs cannot be edited in multiplayer if the mod is not on the server. + /// + Always = 0, + + /// + /// AdminOnly is only enforced for Config Entries if the mod is installed on the server. + /// + IfOnServer = 1, + } + + /// + /// This attribute is used to determine how Jotunn should enforce synchronization of Config Entries.
+ /// Only relevant for Config Entries that have the applied. + ///
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Assembly)] + public class SynchronizationModeAttribute : Attribute + { + /// + /// AdminOnly ConfigEntry Strictness + /// + public AdminOnlyStrictness EnforceAdminOnly { get; set; } + + /// + /// Synchronization mode Attribute + /// + /// + public SynchronizationModeAttribute(AdminOnlyStrictness enforceAdminOnly) + { + EnforceAdminOnly = enforceAdminOnly; + } + + /// + /// Check if AdminOnly Config Entries should always be locked if player is not an admin. + /// + /// + public bool ShouldAlwaysEnforceAdminOnly() + { + return EnforceAdminOnly == AdminOnlyStrictness.Always; + } + } +} diff --git a/JotunnTests/CompatibilityLevelTest.cs b/JotunnTests/CompatibilityLevelTest.cs index 8c595484c..f9df47c62 100644 --- a/JotunnTests/CompatibilityLevelTest.cs +++ b/JotunnTests/CompatibilityLevelTest.cs @@ -47,7 +47,7 @@ public void ClientHasMod_ServerDoesNot(CompatibilityLevel compatibilityLevel, bo { clientVersionData.Modules = new List { - new ModModule("TestMod", v_1_0_0, compatibilityLevel, VersionStrictness.Minor) + new ModModule("TestMod", "TestMod", v_1_0_0, compatibilityLevel, VersionStrictness.Minor) }; Assert.Equal(expected, ModCompatibility.CompareVersionData(serverVersionData, clientVersionData)); } @@ -66,7 +66,7 @@ public void ServerHasMod_ClientDoesNot(CompatibilityLevel compatibilityLevel, bo { serverVersionData.Modules = new List { - new ModModule("TestMod", v_1_0_0, compatibilityLevel, VersionStrictness.Minor) + new ModModule("TestMod", "TestMod", v_1_0_0, compatibilityLevel, VersionStrictness.Minor) }; Assert.Equal(expected, ModCompatibility.CompareVersionData(serverVersionData, clientVersionData)); } @@ -122,35 +122,35 @@ public void ModVersionCompare_NoneStrictness() [Fact] public void OnlyLowerOrHigherVersion_Minor() { - var moduleA = new ModModule("", v_1_1_0, CompatibilityLevel.VersionCheckOnly, VersionStrictness.Minor); - var moduleB = new ModModule("", v_2_0_0, CompatibilityLevel.VersionCheckOnly, VersionStrictness.Minor); - Assert.False(ModModule.IsLowerVersion(moduleA, moduleB, moduleA.versionStrictness)); - Assert.True(ModModule.IsLowerVersion(moduleB, moduleA, moduleA.versionStrictness)); + var moduleA = new ModModule("", "", v_1_1_0, CompatibilityLevel.VersionCheckOnly, VersionStrictness.Minor); + var moduleB = new ModModule("", "", v_2_0_0, CompatibilityLevel.VersionCheckOnly, VersionStrictness.Minor); + Assert.False(ModModule.IsLowerVersion(moduleA, moduleB, moduleA.VersionStrictness)); + Assert.True(ModModule.IsLowerVersion(moduleB, moduleA, moduleA.VersionStrictness)); } [Fact] public void OnlyLowerOrHigherVersion_Patch() { - var moduleA = new ModModule("", v_1_1_1, CompatibilityLevel.VersionCheckOnly, VersionStrictness.Patch); - var moduleB = new ModModule("", v_2_2_0, CompatibilityLevel.VersionCheckOnly, VersionStrictness.Patch); - Assert.False(ModModule.IsLowerVersion(moduleA, moduleB, moduleA.versionStrictness)); - Assert.True(ModModule.IsLowerVersion(moduleB, moduleA, moduleA.versionStrictness)); - - var moduleC = new ModModule("", v_1_1_1, CompatibilityLevel.VersionCheckOnly, VersionStrictness.Patch); - var moduleD = new ModModule("", v_2_1_0, CompatibilityLevel.VersionCheckOnly, VersionStrictness.Patch); - Assert.False(ModModule.IsLowerVersion(moduleC, moduleD, moduleC.versionStrictness)); - Assert.True(ModModule.IsLowerVersion(moduleD, moduleC, moduleC.versionStrictness)); + var moduleA = new ModModule("", "", v_1_1_1, CompatibilityLevel.VersionCheckOnly, VersionStrictness.Patch); + var moduleB = new ModModule("", "", v_2_2_0, CompatibilityLevel.VersionCheckOnly, VersionStrictness.Patch); + Assert.False(ModModule.IsLowerVersion(moduleA, moduleB, moduleA.VersionStrictness)); + Assert.True(ModModule.IsLowerVersion(moduleB, moduleA, moduleA.VersionStrictness)); + + var moduleC = new ModModule("", "", v_1_1_1, CompatibilityLevel.VersionCheckOnly, VersionStrictness.Patch); + var moduleD = new ModModule("", "", v_2_1_0, CompatibilityLevel.VersionCheckOnly, VersionStrictness.Patch); + Assert.False(ModModule.IsLowerVersion(moduleC, moduleD, moduleC.VersionStrictness)); + Assert.True(ModModule.IsLowerVersion(moduleD, moduleC, moduleC.VersionStrictness)); } private void TestVersionCompare(System.Version v1, System.Version v2, CompatibilityLevel level, VersionStrictness strictness, bool expected) { - serverVersionData.Modules = new List { new ModModule("TestMod", v1, level, strictness) }; - clientVersionData.Modules = new List { new ModModule("TestMod", v2, level, strictness) }; + serverVersionData.Modules = new List { new ModModule("TestMod", "TestMod", v1, level, strictness) }; + clientVersionData.Modules = new List { new ModModule("TestMod", "TestMod", v2, level, strictness) }; Assert.Equal(expected, ModCompatibility.CompareVersionData(serverVersionData, clientVersionData)); - serverVersionData.Modules = new List { new ModModule("TestMod", v2, level, strictness) }; - clientVersionData.Modules = new List { new ModModule("TestMod", v1, level, strictness) }; + serverVersionData.Modules = new List { new ModModule("TestMod", "TestMod", v2, level, strictness) }; + clientVersionData.Modules = new List { new ModModule("TestMod", "TestMod", v1, level, strictness) }; Assert.Equal(expected, ModCompatibility.CompareVersionData(serverVersionData, clientVersionData)); } } diff --git a/JotunnTests/CompatibilityZPackageTest.cs b/JotunnTests/CompatibilityZPackageTest.cs index 9ea2cc525..71f9b277b 100644 --- a/JotunnTests/CompatibilityZPackageTest.cs +++ b/JotunnTests/CompatibilityZPackageTest.cs @@ -17,17 +17,18 @@ public CompatibilityZPackageTest() [Fact] public void ModModuleZPackage() { - ModModule module = new ModModule("TestMod", v_1_0_5, CompatibilityLevel.EveryoneMustHaveMod, VersionStrictness.Minor); + ModModule module = new ModModule("TestMod", "TestMod", v_1_0_5, CompatibilityLevel.EveryoneMustHaveMod, VersionStrictness.Minor); ZPackage pkg = new ZPackage(); - module.WriteToPackage(pkg); + module.WriteToPackage(pkg, legacy: false); pkg.SetPos(0); - ModModule result = new ModModule(pkg); + ModModule result = new ModModule(pkg, legacy: false); - Assert.Equal(module.name, result.name); - Assert.Equal(module.version, result.version); - Assert.Equal(module.compatibilityLevel, result.compatibilityLevel); - Assert.Equal(module.versionStrictness, result.versionStrictness); + Assert.Equal(module.ModID, result.ModID); + Assert.Equal(module.ModName, result.ModName); + Assert.Equal(module.Version, result.Version); + Assert.Equal(module.CompatibilityLevel, result.CompatibilityLevel); + Assert.Equal(module.VersionStrictness, result.VersionStrictness); } [Fact] @@ -35,7 +36,7 @@ public void ModuleVersionDataZPackage() { moduleData.ValheimVersion = v_1_0_5; moduleData.VersionString = "1.2.3-Test"; - moduleData.Modules = new List { new ModModule("TestMod", v_1_0_5, CompatibilityLevel.EveryoneMustHaveMod, VersionStrictness.Minor) }; + moduleData.Modules = new List { new ModModule("TestMod", "TestMod", v_1_0_5, CompatibilityLevel.EveryoneMustHaveMod, VersionStrictness.Minor) }; ZPackage pkg = moduleData.ToZPackage(); pkg.SetPos(0); @@ -52,7 +53,7 @@ public void ModuleVersionDataZPackage_OldVersion() { moduleData.ValheimVersion = v_1_0_5; moduleData.VersionString = "1.2.3-Test"; - moduleData.Modules = new List { new ModModule("TestMod", v_1_0_5, CompatibilityLevel.EveryoneMustHaveMod, VersionStrictness.Minor) }; + moduleData.Modules = new List { new ModModule("TestMod", "TestMod", v_1_0_5, CompatibilityLevel.EveryoneMustHaveMod, VersionStrictness.Minor) }; var pkg = new ZPackage(); pkg.Write(moduleData.ValheimVersion.Major); @@ -63,7 +64,7 @@ public void ModuleVersionDataZPackage_OldVersion() foreach (var module in moduleData.Modules) { - module.WriteToPackage(pkg); + module.WriteToPackage(pkg, legacy: true); } pkg.SetPos(0);