From 1cd91c735f1810ea7fac613f227b8df6fa8d985c Mon Sep 17 00:00:00 2001 From: jneb802 <73610029+jneb802@users.noreply.github.com> Date: Sun, 18 Oct 2026 14:54:45 -0700 Subject: [PATCH 1/7] Add temporary Endgame tablet compatibility for Epic Loot --- PraetorisClient.csproj | 1 + Source/Core/Plugin.cs | 3 + Source/EpicLoot/EndgameCompatibility.cs | 252 ++++++++++++++++++ .../EndgameCompatibility.csproj | 9 + tests/EndgameCompatibility/Program.cs | 104 ++++++++ tests/EndgameCompatibility/README.md | 100 +++++++ 6 files changed, 469 insertions(+) create mode 100644 Source/EpicLoot/EndgameCompatibility.cs create mode 100644 tests/EndgameCompatibility/EndgameCompatibility.csproj create mode 100644 tests/EndgameCompatibility/Program.cs create mode 100644 tests/EndgameCompatibility/README.md diff --git a/PraetorisClient.csproj b/PraetorisClient.csproj index 82643f9..5b6fa25 100644 --- a/PraetorisClient.csproj +++ b/PraetorisClient.csproj @@ -49,6 +49,7 @@ + PraetorisClient.GuideImages.%(Filename)%(Extension) diff --git a/Source/Core/Plugin.cs b/Source/Core/Plugin.cs index e2378a1..a61e65e 100644 --- a/Source/Core/Plugin.cs +++ b/Source/Core/Plugin.cs @@ -22,6 +22,7 @@ namespace PraetorisClient { [BepInPlugin(ModGUID, ModName, ModVersion)] + [BepInDependency(EndgameCompatibility.EndgameGuid, BepInDependency.DependencyFlags.SoftDependency)] [BepInDependency(Jotunn.Main.ModGuid)] [BepInDependency(EpicLootApiBridge.PluginGuid, BepInDependency.DependencyFlags.SoftDependency)] [BepInDependency("org.tristan.rcon", BepInDependency.DependencyFlags.SoftDependency)] @@ -166,6 +167,7 @@ public void Awake() FrameTimeMonitor.Initialize(); RpcTraceTelemetry.Initialize(); ApplyHarmonyPatches(epicLootLoaded); + EndgameCompatibility.Initialize(); CraftyBoxesWardGuard.TryApply(_harmony); PublicBasicPickables.TryApply(_harmony); ShippingPortDepartures.Initialize(_harmony); @@ -226,6 +228,7 @@ private void Update() private void OnDestroy() { + EndgameCompatibility.Shutdown(); GuardStoneFeature.GuardStoneMapPins.Clear(); GuardStoneFeature.SpawnIslandWards.ClearSurvey(); PrefabManager.OnPrefabsRegistered -= BossSpawnMessageSuppression.Refresh; diff --git a/Source/EpicLoot/EndgameCompatibility.cs b/Source/EpicLoot/EndgameCompatibility.cs new file mode 100644 index 0000000..12ff842 --- /dev/null +++ b/Source/EpicLoot/EndgameCompatibility.cs @@ -0,0 +1,252 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System.Security.Cryptography; +using BepInEx.Bootstrap; +using HarmonyLib; +using UnityEngine; + +namespace PraetorisClient; + +// Temporary bridge for Epic Loot PRs #45 and #47. No replacement EpicLoot.dll or new save format. +internal static class EndgameCompatibility +{ + internal const string EndgameGuid = "warpalicious.ValheimEndgame"; + private const string Owner = "warpalicious.PraetorisClient.EndgameCompatibility"; + // Production Season 8 8.0.46. Epic Loot forks can share a version; match the actual binary. + private const string LegacyHash = "802790e0cbd8a6f951627619a488b84e6a406e8770e2687b2fff8b63d2adf9ea"; + private static readonly Harmony Patches = new(Owner); + private static FieldInfo socketCount = null!; + private static FieldInfo craftedItem = null!; + private static MethodInfo socketWeights = null!; + private static Type rarityType = null!; + + internal static bool IsTabletName(string? name) + { + if (name == "ve_tablet") return true; + const string prefix = "ve_tablet_tier"; + return name != null && name.Length == prefix.Length + 2 && + name.StartsWith(prefix, StringComparison.Ordinal) && + int.TryParse(name.Substring(prefix.Length), NumberStyles.None, CultureInfo.InvariantCulture, out int tier) && + tier >= 1 && tier <= 15; + } + + private static bool IsTablet(ItemDrop.ItemData? item) => IsTabletName(item?.m_dropPrefab?.name); + + internal static void Initialize() + { + if (!Chainloader.PluginInfos.ContainsKey(EndgameGuid) || + !Chainloader.PluginInfos.TryGetValue(EpicLootApiBridge.PluginGuid, out BepInEx.PluginInfo info)) return; + + Assembly assembly = info.Instance.GetType().Assembly; + try + { + if (HasNativeSupport(assembly)) + { + ApplyCraftResultFix(assembly); + PraetorisClientPlugin.Log.LogInfo("Endgame compatibility: native Epic Loot enchanting/socket support detected; temporary item patches skipped."); + return; + } + + using SHA256 sha = SHA256.Create(); + using System.IO.FileStream stream = System.IO.File.OpenRead(assembly.Location); + string hash = string.Concat(sha.ComputeHash(stream).Select(b => b.ToString("x2"))); + if (hash != LegacyHash) + { + PraetorisClientPlugin.Log.LogWarning("Endgame compatibility: unsupported Epic Loot binary. No temporary patches applied; tablet enchanting/socket rules are NOT provided."); + return; + } + + Type item = typeof(ItemDrop.ItemData); + rarityType = RequiredType(assembly, "EpicLoot.ItemRarity"); + socketCount = AccessTools.Field(RequiredType(assembly, "EpicLoot.MagicItem"), "SocketCount") + ?? throw new MissingFieldException("MagicItem.SocketCount"); + craftedItem = AccessTools.Field(RequiredType(assembly, "EpicLoot.Crafting.TransferMagicalEffects"), "CraftedItem") + ?? throw new MissingFieldException("TransferMagicalEffects.CraftedItem"); + socketWeights = RequiredMethod(assembly, "EpicLoot.LootRoller", "GetSocketCountsPerRarity", rarityType); + + // Manual registration: these methods must not join Praetoris Client's assembly-wide patch scan. + Patch(RequiredMethod(assembly, "EpicLoot.EpicLoot", "IsAllowedMagicItemType", item), postfix: nameof(AllowTablet)); + Patch(RequiredMethod(assembly, "EpicLoot.GatedItemType.ItemTypeClassifier", "ClassifyFromFields", item), postfix: nameof(ClassifyTablet)); + Patch(RequiredMethod(assembly, "EpicLoot.LootRoller", "RollMagicItem", rarityType, item, typeof(float), typeof(float)), transpiler: nameof(PreventRolledSockets)); + Patch(RequiredMethod(assembly, "EpicLoot.CraftingV2.EnchantingUIController", "GetEnchantInfo", item, rarityType), transpiler: nameof(HideSocketPreview)); + Patch(RequiredMethod(assembly, "EpicLoot.Crafting.TransferMagicalEffects", "TransferMagicToCraftedItem", typeof(Player)), transpiler: nameof(PreventTransferredSockets)); + Patch(RequiredMethod(assembly, "EpicLoot.ShardStones.ShardSlotChisel", "CanApply", item, item, typeof(int).MakeByRefType(), typeof(string).MakeByRefType()), prefix: nameof(PreventChisel)); + Patch(RequiredMethod(assembly, "EpicLoot.ShardStones.ShardSocketManager", "CanSocket", item, item, + typeof(string).MakeByRefType()), prefix: nameof(PreventPlacement)); + Patch(RequiredMethod(assembly, "EpicLoot.ShardStones.ShardSocketManager", "CanCoexist", item, item, + typeof(IEnumerable), typeof(string).MakeByRefType()), prefix: nameof(PreventPlacement)); + ApplyCraftResultFix(assembly); + PraetorisClientPlugin.Log.LogInfo("Endgame compatibility: applied tablet enchanting, socket restrictions, and craft result fix to verified Epic Loot 0.14.13."); + } + catch (Exception exception) + { + Shutdown(); + PraetorisClientPlugin.Log.LogError("Endgame compatibility could not initialize; all temporary patches removed: " + exception.GetBaseException().Message); + } + } + + internal static void Shutdown() => Patches.UnpatchSelf(); + + private static Type RequiredType(Assembly assembly, string name) => assembly.GetType(name, true)!; + private static MethodInfo RequiredMethod(Assembly assembly, string type, string name, params Type[] parameters) => + AccessTools.Method(RequiredType(assembly, type), name, parameters) ?? throw new MissingMethodException(type, name); + + private static void Patch(MethodInfo target, string? prefix = null, string? postfix = null, string? transpiler = null) => + Patches.Patch(target, Hook(prefix), Hook(postfix), Hook(transpiler)); + + private static HarmonyMethod? Hook(string? name) => name == null ? null : new HarmonyMethod(typeof(EndgameCompatibility), name); + + private static bool HasNativeSupport(Assembly assembly) + { + Type? config = assembly.GetType("EpicLoot.GatedItemType.ItemTypeInfo"); + Type? details = assembly.GetType("EpicLoot.GatedItemType.GatedItemDetails"); + Type? helper = assembly.GetType("EpicLoot.GatedItemType.GatedItemTypeHelper"); + MethodInfo? native = helper == null ? null : AccessTools.Method(helper, "IsSocketable", new[] { typeof(ItemDrop.ItemData) }); + if (config?.GetField("Enchantable")?.FieldType != typeof(bool) || config.GetField("Socketable")?.FieldType != typeof(bool) || + details?.GetProperty("Enchantable")?.PropertyType != typeof(bool) || details.GetProperty("Socketable")?.PropertyType != typeof(bool) || native == null) return false; + + // Fields alone can be present in an incomplete upstream build. Check each socket enforcement path. + (string Type, string Method)[] paths = + { + ("EpicLoot.LootRoller", "RollMagicItem"), + ("EpicLoot.CraftingV2.EnchantingUIController", "GetEnchantInfo"), + ("EpicLoot.Crafting.TransferMagicalEffects", "TransferMagicToCraftedItem"), + ("EpicLoot.ShardStones.ShardSlotChisel", "CanApply"), + ("EpicLoot.ShardStones.ShardSocketManager", "ResolvePlacementEffect") + }; + foreach ((string typeName, string methodName) in paths) + { + Type? type = assembly.GetType(typeName); + if (type == null || !AccessTools.GetDeclaredMethods(type).Where(m => m.Name == methodName) + .Any(m => PatchProcessor.GetOriginalInstructions(m).Any(i => i.Calls(native)))) return false; + } + MethodInfo allowed = RequiredMethod(assembly, "EpicLoot.EpicLoot", "IsAllowedMagicItemType", typeof(ItemDrop.ItemData)); + MethodInfo? enchantable = details.GetProperty("Enchantable")?.GetGetMethod(); + return enchantable != null && PatchProcessor.GetOriginalInstructions(allowed).Any(i => i.Calls(enchantable)); + } + + private static void AllowTablet(ItemDrop.ItemData __0, ref bool __result) + { + if (IsTablet(__0) && __0.m_shared.m_itemType != ItemDrop.ItemData.ItemType.Ammo && + __0.m_shared.m_itemType != ItemDrop.ItemData.ItemType.AmmoNonEquipable) __result = true; + } + + private static void ClassifyTablet(ItemDrop.ItemData __0, ref string __result) + { + if (IsTablet(__0)) __result = "Tablet"; + } + + private static int FilterSocketCount(int count, ItemDrop.ItemData item) => IsTablet(item) ? 0 : count; + + private static int TransferSocketCount(object magicItem) => + IsTablet(craftedItem.GetValue(null) as ItemDrop.ItemData) ? 0 : (int)socketCount.GetValue(magicItem); + + private static List> PreviewSocketWeights(int rarity, ItemDrop.ItemData item) => + IsTablet(item) ? new List>() : + (List>)socketWeights.Invoke(null, new[] { Enum.ToObject(rarityType, rarity) }); + + private static IEnumerable PreventRolledSockets(IEnumerable instructions) + { + int matches = 0; + foreach (CodeInstruction instruction in instructions) + { + if (instruction.opcode == OpCodes.Stfld && Equals(instruction.operand, socketCount)) + { + matches++; + CodeInstruction load = new(OpCodes.Ldarg_1); + load.labels.AddRange(instruction.labels); + instruction.labels.Clear(); + load.blocks.AddRange(instruction.blocks); + instruction.blocks.Clear(); + yield return load; + yield return new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(EndgameCompatibility), nameof(FilterSocketCount))); + } + yield return instruction; + } + if (matches != 1) throw new InvalidOperationException("Unexpected Epic Loot socket roll layout."); + } + + private static IEnumerable HideSocketPreview(IEnumerable instructions) + { + int matches = 0; + foreach (CodeInstruction instruction in instructions) + { + if (instruction.Calls(socketWeights)) + { + matches++; + CodeInstruction load = new(OpCodes.Ldarg_0); + load.labels.AddRange(instruction.labels); + instruction.labels.Clear(); + load.blocks.AddRange(instruction.blocks); + instruction.blocks.Clear(); + yield return load; + instruction.opcode = OpCodes.Call; + instruction.operand = AccessTools.Method(typeof(EndgameCompatibility), nameof(PreviewSocketWeights)); + } + yield return instruction; + } + if (matches != 1) throw new InvalidOperationException("Unexpected Epic Loot socket preview layout."); + } + + private static IEnumerable PreventTransferredSockets(IEnumerable instructions) + { + int matches = 0; + foreach (CodeInstruction instruction in instructions) + { + if (instruction.opcode == OpCodes.Ldfld && Equals(instruction.operand, socketCount)) + { + matches++; + instruction.opcode = OpCodes.Call; + instruction.operand = AccessTools.Method(typeof(EndgameCompatibility), nameof(TransferSocketCount)); + } + yield return instruction; + } + if (matches != 3) throw new InvalidOperationException("Unexpected Epic Loot socket transfer layout."); + } + + private static bool PreventChisel(ItemDrop.ItemData __1, ref int __2, ref string __3, ref bool __result) + { + if (!IsTablet(__1)) return true; + __2 = 0; + __3 = "$mod_epicloot_socket_unavailable"; + __result = false; + return false; + } + + private static bool PreventPlacement(ItemDrop.ItemData __0, ref string reason, ref bool __result) + { + if (!IsTablet(__0)) return true; + reason = "$mod_epicloot_socket_unavailable"; + __result = false; + return false; + } + + private static void ShowCraftResult(object __instance) + { + foreach (string field in new[] { "NameText", "Description", "Icon" }) + { + if (AccessTools.Field(__instance.GetType(), field)?.GetValue(__instance) is Behaviour component) + component.enabled = true; + } + } + + private static void ApplyCraftResultFix(Assembly assembly) + { + Type dialog = RequiredType(assembly, "EpicLoot.Crafting.CraftSuccessDialog"); + MethodInfo show = RequiredMethod(assembly, dialog.FullName!, "Show", typeof(ItemDrop.ItemData)); + MethodInfo enabled = typeof(Behaviour).GetProperty(nameof(Behaviour.enabled))!.GetSetMethod()!; + if (PatchProcessor.GetOriginalInstructions(show).Count(i => i.Calls(enabled)) >= 3) return; + foreach (string field in new[] { "NameText", "Description", "Icon" }) + { + FieldInfo member = AccessTools.Field(dialog, field) ?? throw new MissingFieldException(dialog.FullName, field); + if (!typeof(Behaviour).IsAssignableFrom(member.FieldType)) + throw new InvalidOperationException("Unexpected Epic Loot craft result component: " + field); + } + Patch(show, postfix: nameof(ShowCraftResult)); + PraetorisClientPlugin.Log.LogInfo("Endgame compatibility: applied temporary craft result visibility fix."); + } +} diff --git a/tests/EndgameCompatibility/EndgameCompatibility.csproj b/tests/EndgameCompatibility/EndgameCompatibility.csproj new file mode 100644 index 0000000..ef8a995 --- /dev/null +++ b/tests/EndgameCompatibility/EndgameCompatibility.csproj @@ -0,0 +1,9 @@ + + + Exe + net481 + 10 + enable + enable + + diff --git a/tests/EndgameCompatibility/Program.cs b/tests/EndgameCompatibility/Program.cs new file mode 100644 index 0000000..def3104 --- /dev/null +++ b/tests/EndgameCompatibility/Program.cs @@ -0,0 +1,104 @@ +using System.Collections; +using System.Reflection; + +// Exercise the actual candidate and Epic Loot IL without starting Unity. No copied compatibility implementation. +if (args.Length < 3 || args.Length > 4) + throw new ArgumentException("Usage: [native]"); +string game = Path.GetFullPath(args[2]); +string[] search = +{ + Path.Combine(game, "BepInEx/core"), + Path.Combine(game, "Valheim.app/Contents/Resources/Data/Managed"), + Path.Combine(game, "valheim_Data/Managed"), + Path.GetDirectoryName(Path.GetFullPath(args[0]))!, + Path.GetDirectoryName(Path.GetFullPath(args[1]))! +}; +AppDomain.CurrentDomain.AssemblyResolve += (_, request) => +{ + AssemblyName name = new(request.Name); + foreach (string directory in search) + { + string path = Path.Combine(directory, name.Name + ".dll"); + if (File.Exists(path)) return Assembly.LoadFrom(path); + } + string? plugin = Directory.EnumerateFiles(Path.Combine(game, "BepInEx/plugins"), name.Name + ".dll", SearchOption.AllDirectories).FirstOrDefault(); + return plugin == null ? null : Assembly.LoadFrom(plugin); +}; +Assembly epic = Assembly.LoadFrom(Path.GetFullPath(args[1])); +Assembly client = Assembly.LoadFrom(Path.GetFullPath(args[0])); +Type compatibility = client.GetType("PraetorisClient.EndgameCompatibility", true)!; +const BindingFlags flags = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; +object? Call(string name, params object?[] values) => compatibility.GetMethod(name, flags)!.Invoke(null, values); +void Require(bool condition, string name) +{ + if (!condition) throw new Exception("FAIL: " + name); + System.Console.WriteLine("PASS: " + name); +} +Require((bool)Call("IsTabletName", "ve_tablet")!, "legacy tablet"); +for (int tier = 1; tier <= 15; tier++) + Require((bool)Call("IsTabletName", "ve_tablet_tier" + tier.ToString("00"))!, "tablet tier " + tier); +foreach (string? name in new[] { null, "", "SwordIron", "ve_tablet_tier00", "ve_tablet_tier16", "ve_tablet_tier1", "ve_tablet_tier01(Clone)", "ve_tablet_tier+1" }) + Require(!(bool)Call("IsTabletName", new object?[] { name })!, "reject unrelated/invalid name " + (name ?? "null")); +bool native = args.Length == 4 && args[3] == "native"; +Require((bool)Call("HasNativeSupport", epic)! == native, native ? "upstream build recognized without item patches" : "legacy build does not claim native flags"); +if (native) return; + +Assembly harmony = Assembly.LoadFrom(Path.Combine(search[0], "0Harmony.dll")); +Type processor = harmony.GetType("HarmonyLib.PatchProcessor", true)!; +MethodInfo read = processor.GetMethods().Single(m => m.Name == "GetOriginalInstructions" && m.GetParameters().Length == 2 && !m.GetParameters()[1].ParameterType.IsByRef); +Type magic = epic.GetType("EpicLoot.MagicItem", true)!; +FieldInfo socketCount = magic.GetField("SocketCount")!; +Type rarity = epic.GetType("EpicLoot.ItemRarity", true)!; +Type item = epic.GetType("EpicLoot.EpicLoot", true)!.GetMethod("IsAllowedMagicItemType")!.GetParameters()[0].ParameterType; +Type roller = epic.GetType("EpicLoot.LootRoller", true)!; +MethodInfo weights = roller.GetMethod("GetSocketCountsPerRarity")!; +compatibility.GetField("socketCount", flags)!.SetValue(null, socketCount); +compatibility.GetField("socketWeights", flags)!.SetValue(null, weights); + +(MethodInfo Method, string Transform, string Helper, int Count)[] cases = +{ + (roller.GetMethod("RollMagicItem", new[] { rarity, item, typeof(float), typeof(float) })!, "PreventRolledSockets", "FilterSocketCount", 1), + (epic.GetType("EpicLoot.CraftingV2.EnchantingUIController", true)!.GetMethod("GetEnchantInfo", flags)!, "HideSocketPreview", "PreviewSocketWeights", 1), + (epic.GetType("EpicLoot.Crafting.TransferMagicalEffects", true)!.GetMethod("TransferMagicToCraftedItem", flags)!, "PreventTransferredSockets", "TransferSocketCount", 3) +}; +foreach ((MethodInfo method, string transform, string helper, int count) in cases) +{ + object instructions = read.Invoke(null, new object?[] { method, null })!; + object[] rewritten = ((IEnumerable)Call(transform, instructions)!).Cast().ToArray(); + int calls = rewritten.Count(i => (i.GetType().GetField("operand")!.GetValue(i) as MethodInfo)?.Name == helper); + Require(calls == count, method.Name + " rewrites actual production IL"); + // The version guard must not quietly accept a changed method body. + object empty = Activator.CreateInstance(instructions.GetType())!; + bool rejected = false; + try { _ = ((IEnumerable)Call(transform, empty)!).Cast().ToArray(); } + catch (InvalidOperationException) { rejected = true; } + Require(rejected, transform + " rejects changed IL"); +} + +compatibility.GetField("rarityType", flags)!.SetValue(null, rarity); +compatibility.GetField("craftedItem", flags)!.SetValue(null, + epic.GetType("EpicLoot.Crafting.TransferMagicalEffects", true)!.GetField("CraftedItem", flags)); +MethodInfo Required(string type, string method) => epic.GetType(type, true)!.GetMethod(method, flags)!; +(MethodInfo Method, string? Prefix, string? Postfix, string? Transpiler)[] patches = +{ + (Required("EpicLoot.EpicLoot", "IsAllowedMagicItemType"), null, "AllowTablet", null), + (Required("EpicLoot.GatedItemType.ItemTypeClassifier", "ClassifyFromFields"), null, "ClassifyTablet", null), + (cases[0].Method, null, null, cases[0].Transform), + (cases[1].Method, null, null, cases[1].Transform), + (cases[2].Method, null, null, cases[2].Transform), + (Required("EpicLoot.ShardStones.ShardSlotChisel", "CanApply"), "PreventChisel", null, null), + (Required("EpicLoot.ShardStones.ShardSocketManager", "CanSocket"), "PreventPlacement", null, null), + (Required("EpicLoot.ShardStones.ShardSocketManager", "CanCoexist"), "PreventPlacement", null, null), + (epic.GetType("EpicLoot.Crafting.CraftSuccessDialog", true)!.GetMethod("Show")!, null, "ShowCraftResult", null) +}; +foreach ((MethodInfo method, string? prefix, string? postfix, string? transpiler) in patches) +{ + Require(method != null, "patch target exists"); + foreach (string? callback in new[] { prefix, postfix, transpiler }.Where(name => name != null)) + Require(compatibility.GetMethod(callback!, flags) != null, "callback exists: " + callback); +} +foreach (string name in new[] { "CanSocket", "CanCoexist" }) + Require(Required("EpicLoot.ShardStones.ShardSocketManager", name).GetParameters() + .Any(p => p.Name == "reason" && p.ParameterType == typeof(string).MakeByRefType()), + name + " callback binds the actual rejection reason"); +System.Console.WriteLine("Runtime attachment and gameplay require a Unity client; this harness checks policy, targets, and IL transformations only."); diff --git a/tests/EndgameCompatibility/README.md b/tests/EndgameCompatibility/README.md new file mode 100644 index 0000000..c925486 --- /dev/null +++ b/tests/EndgameCompatibility/README.md @@ -0,0 +1,100 @@ +# Temporary Endgame compatibility + +This bridges Epic Loot PRs [45](https://github.com/MidnightsFX/Randy_Vapok_ValheimMods/pull/45) +and [47](https://github.com/MidnightsFX/Randy_Vapok_ValheimMods/pull/47) for Endgame testing. +Keep the separately built Endgame DLL and its Epic Loot JSON patches in the client +package. This change does not publish or bundle an Endgame build. + +## Scope + +- Runs only when BepInEx loads both Epic Loot and Endgame. The optional dependency + makes Endgame load first; no source or binary reference to Endgame is needed. +- Supports the exact production Epic Loot 0.14.13 binary with SHA-256 + `802790e0cbd8a6f951627619a488b84e6a406e8770e2687b2fff8b63d2adf9ea`. + An unrecognized older binary gets a warning and no compatibility patches. +- Allows enchanting `ve_tablet` and `ve_tablet_tier01` through `ve_tablet_tier15`. + Other eligibility checks still apply. Keeps these items in the Tablet category + when Epic Loot rebuilds item configuration. +- Prevents socket rolls, insertion, swaps, chisel use, and crafting socket + transfers for those items. Omits socket probabilities from their preview. + Existing socket data is not erased on load; removal of old socket contents + remains available. +- Restores the crafting result's text and icon when the native fix is absent. +- Does not add general-purpose JSON fields to older Epic Loot. The temporary + tablet policy is compiled into this bridge, so clients and servers running it + use the same policy. This is not a backport for arbitrary item categories. +- Uses a separate Harmony owner. Removing this code does not require changing + Epic Loot's saved-item format. + +## Native support and removal + +Item patches are skipped when the loaded assembly has both config fields, both +runtime properties, and the native checks in all five socket paths and the +enchanting eligibility path. The crafting result fix is checked separately. +Field presence alone is not sufficient. + +Keep Endgame's iteminfo JSON patch, including `Enchantable: true` and +`Socketable: false`, for the eventual native release. Older Epic Loot can discard +unknown fields when it rewrites its generated config; this bridge does not rely +on those generated fields. On upgrade, verify the Endgame JSON patch has been +applied again and the native runtime contains both settings for every tablet. + +Before removing the bridge, test the upgraded Epic Loot with tablets saved by +the old build. Confirm values, effects, socket rules, save/restart, and further +enchanting. Then remove `EndgameCompatibility.cs`, its startup/shutdown calls, +its optional Endgame dependency, and this test project in one change. No live +upgrade or removal proof has been completed yet. + +## Local checks + +Build the client and test harness, then run under Mono (not the .NET Core runtime). +The harness loads the actual client DLL and an actual Epic Loot DLL. It invokes +the production compatibility code; it does not copy that code into a test model. + +```sh +dotnet build PraetorisClient.csproj -c Release +dotnet build tests/EndgameCompatibility +mono tests/EndgameCompatibility/bin/Debug/net481/EndgameCompatibility.exe \ + bin/Release/PraetorisClient.dll /path/to/production/EpicLoot.dll /path/to/Valheim +mono tests/EndgameCompatibility/bin/Debug/net481/EndgameCompatibility.exe \ + bin/Release/PraetorisClient.dll /path/to/native-support/EpicLoot.dll /path/to/Valheim native +``` + +On 2026-10-18: Release build passed with 83 existing warnings and zero errors. +The harness build had zero warnings and errors. The production binary passed 51 +checks for item names, native detection, patch targets, parameter binding, three +real IL transformations, and rejection of changed method bodies. A local Epic +Loot build containing PR 45 (SHA-256 +`1d3244a7387458bf4b056387ef8707d6172733ad0d6fabc63327dce289fc5e3b`) +passed 25 checks, including native detection. + +These checks do not attach runtime patches or exercise Unity gameplay. An +attempt to attach patches outside Unity failed in MonoMod's runtime detour +initialization. Actual attachment, gameplay, and saved-item migration must be +tested in Valheim. + +## Remaining live proof + +Valnet Client 01 was started and inspected on 2026-10-18. Production was Season 8 +8.0.46 with Epic Loot 0.14.13 and Praetoris Client 0.1.101. Valnet's maintained +profile reported Season 8 8.0.34; its Epic Loot binary matched production. +The profile and mmcli registry were backed up before attempting alignment. +The SSH key agent then refused signing requests. Alignment, baseline +reproduction, candidate deployment, and live proof did not run. + +Resume with a device lease and a verified current production mirror: + +1. Run the ordinary enchanting-table flow with Endgame and the unchanged client. + Record tablet eligibility and socket behavior before deploying this change. +2. Deploy the candidate client to that same profile. Keep Epic Loot unchanged. +3. Confirm the compatibility startup log and absence of new errors. Check all + tablet names and a normal weapon as a control. +4. Use the existing `cli_epicloot_item`, `cli_epicloot_enchant_ui`, + `cli_epicloot_roll`, and UI inspection commands from valheimCLI. Exercise the + actual enchanting button, materials consumption, preview, and result dialog. +5. Exercise socket insertion/swaps, chisels, and crafting transfers. Check that + rejected actions consume no items and normal weapons retain their behavior. +6. Save/restart with an enchanted tablet. Upgrade to the native-support build, + verify the skip log and loaded JSON flags, and repeat the checks. +7. Repeat the server/client flow on the current production mirror. Inspect both + logs, restore profile backups, verify restoration, and release devices. From 33080102968a20cbe5d9c3ee9fef9e05fa865f4c Mon Sep 17 00:00:00 2001 From: jneb802 <73610029+jneb802@users.noreply.github.com> Date: Mon, 19 Oct 2026 10:27:34 -0700 Subject: [PATCH 2/7] Prepare PraetorisClient 0.1.102 with bundled Endgame --- CHANGELOG.md | 6 ++++++ Properties/AssemblyInfo.cs | 4 ++-- README.md | 7 +++++++ Source/Core/Plugin.cs | 2 +- manifest.json | 6 ++++-- thunderstore.toml | 20 +++++++++++++++++++- 6 files changed, 39 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36b95e6..bf1ab59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.1.102 + +- Include Valheim Endgame 0.5.0 and its tablet configuration in this package. +- Allow tablets to be enchanted with the released Epic Loot 0.14.13, prevent new tablet sockets and shardstones, and restore the enchanting result display. +- Add Star Level System and YamlDotNet as package dependencies for Endgame. + ## 0.1.101 - Allow everyone to gather loose flint, branches, and stones inside Protective Wards. Other pickables and ward protections are unchanged. diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index 476bd1c..935f296 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -31,5 +31,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.1.100")] -[assembly: AssemblyFileVersion("0.1.100")] +[assembly: AssemblyVersion("0.1.102")] +[assembly: AssemblyFileVersion("0.1.102")] diff --git a/README.md b/README.md index 2b455c4..c9995f2 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,18 @@ PraetorisClient is the shared client-and-server mod for Praetoris-specific gameplay, creative-zone, administration, and network measurement features. +This package also includes Valheim Endgame 0.5.0, which adds tablet encounters and map rooms. Tablets can be enchanted with the released Epic Loot 0.14.13. New tablets cannot receive sockets or shardstones. + +Remove a separately installed ValheimEndgame test DLL before installing this combined package. + +Valheim Endgame's original code and assets are all rights reserved. See `LICENSE.ValheimEndgame` for its license; the PraetorisClient license applies to PraetorisClient. + ## Requirements and integrations - [BepInEx](https://github.com/BepInEx/BepInEx) is required. - [Jotunn](https://github.com/Valheim-Modding/Jotunn) is required. - Epic Loot is required by the current build. PraetorisClient starts its Epic Loot extensions during plugin startup. +- Star Level System and YamlDotNet are required by the bundled Valheim Endgame mod. - ValheimRcon is optional. When present on the server, it receives the Server Chest commands. - Shudnal ExtraSlots is optional. When present on a client, its items are included in creative inventory reports. - VBNetTweaks is optional. When present, its `ZDOQueueLimit` supplies the socket metric queue budget. diff --git a/Source/Core/Plugin.cs b/Source/Core/Plugin.cs index a61e65e..149281d 100644 --- a/Source/Core/Plugin.cs +++ b/Source/Core/Plugin.cs @@ -33,7 +33,7 @@ namespace PraetorisClient public class PraetorisClientPlugin : BaseUnityPlugin { private const string ModName = "PraetorisClient"; - private const string ModVersion = "0.1.101"; + private const string ModVersion = "0.1.102"; private const string Author = "warpalicious"; private const string ModGUID = Author + "." + ModName; private const string EpicLootGuid = "randyknapp.mods.epicloot"; diff --git a/manifest.json b/manifest.json index 1fe85fc..a107c3e 100644 --- a/manifest.json +++ b/manifest.json @@ -1,12 +1,14 @@ { "name": "PraetorisClient", "description": "Client-side Praetoris tools for Discord linking, creative server bridges, RPC tracing, socket metrics, and RPC probes.", - "version_number": "0.1.101", + "version_number": "0.1.102", "website_url": "https://discord.gg/3aaru2VyHJ", "dependencies": [ "denikson-BepInExPack_Valheim-5.4.2333", "RandyKnapp-EpicLoot-0.14.13", "ValheimModding-Jotunn-2.30.2", - "ValheimModding-JsonDotNET-13.0.4" + "ValheimModding-JsonDotNET-13.0.4", + "MidnightMods-StarLevelSystem-1.26.3", + "ValheimModding-YamlDotNet-16.3.1" ] } diff --git a/thunderstore.toml b/thunderstore.toml index c31b09d..9d53f68 100644 --- a/thunderstore.toml +++ b/thunderstore.toml @@ -4,7 +4,7 @@ schemaVersion = "0.0.1" [package] namespace = "praetoris" name = "PraetorisClient" -versionNumber = "0.1.101" +versionNumber = "0.1.102" description = "Client-side Praetoris tools for Discord linking, creative server bridges, RPC tracing, socket metrics, and RPC probes." websiteUrl = "https://discord.gg/3aaru2VyHJ" containsNsfwContent = false @@ -14,6 +14,8 @@ denikson-BepInExPack_Valheim = "5.4.2333" RandyKnapp-EpicLoot = "0.14.13" ValheimModding-Jotunn = "2.30.2" ValheimModding-JsonDotNET = "13.0.4" +MidnightMods-StarLevelSystem = "1.26.3" +ValheimModding-YamlDotNet = "16.3.1" [build] icon = "./Assets/ServerGuide/praetoris.png" @@ -24,6 +26,22 @@ outdir = "./build" source = "./bin/Release/PraetorisClient.dll" target = "./PraetorisClient.dll" +[[build.copy]] +source = "./bin/Release/ValheimEndgame.dll" +target = "./ValheimEndgame.dll" + +[[build.copy]] +source = "./bin/Release/ValheimEndgame.LICENSE" +target = "./LICENSE.ValheimEndgame" + +[[build.copy]] +source = "./bin/Release/ValheimEndgame.template-MIT.txt" +target = "./licenses/template-MIT.txt" + +[[build.copy]] +source = "./bin/Release/ValheimEndgame-config" +target = "./config/EpicLoot/patches" + [[build.copy]] source = "./LICENSE" target = "./LICENSE" From f7db337f4e746bf437a2aeb058c04106969416f9 Mon Sep 17 00:00:00 2001 From: jneb802 <73610029+jneb802@users.noreply.github.com> Date: Mon, 19 Oct 2026 11:05:28 -0700 Subject: [PATCH 3/7] Reserve rights to original mod code and assets --- CHANGELOG.md | 1 + LICENSE | 26 ++++++++------------------ README.md | 2 +- thunderstore.toml | 4 ++++ 4 files changed, 14 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf1ab59..d64563e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## 0.1.102 +- Reserve rights to original PraetorisClient and Valheim Endgame code and assets while preserving third-party license notices. - Include Valheim Endgame 0.5.0 and its tablet configuration in this package. - Allow tablets to be enchanted with the released Epic Loot 0.14.13, prevent new tablet sockets and shardstones, and restore the enchanting result display. - Add Star Level System and YamlDotNet as package dependencies for Endgame. diff --git a/LICENSE b/LICENSE index 8978d7f..1952c61 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,11 @@ -MIT License +PraetorisClient -Copyright (c) 2025 warp +Copyright (c) 2025-2026 warpalicious. All rights reserved. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Except with prior written permission from the copyright holder, no permission +is granted to copy, modify, distribute, sublicense, or sell PraetorisClient's +original source code or assets. -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Third-party components retain their respective licenses. Their notices are +preserved in THIRD_PARTY_NOTICES.md. This notice does not change the rights +granted under third-party licenses. diff --git a/README.md b/README.md index c9995f2..41ad124 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This package also includes Valheim Endgame 0.5.0, which adds tablet encounters a Remove a separately installed ValheimEndgame test DLL before installing this combined package. -Valheim Endgame's original code and assets are all rights reserved. See `LICENSE.ValheimEndgame` for its license; the PraetorisClient license applies to PraetorisClient. +PraetorisClient's and Valheim Endgame's original code and assets are all rights reserved. See `LICENSE` and `LICENSE.ValheimEndgame`, respectively. Third-party licenses apply only to their respective components; their notices remain in `THIRD_PARTY_NOTICES.md` and `licenses/`. ## Requirements and integrations diff --git a/thunderstore.toml b/thunderstore.toml index 9d53f68..a3c6157 100644 --- a/thunderstore.toml +++ b/thunderstore.toml @@ -46,6 +46,10 @@ target = "./config/EpicLoot/patches" source = "./LICENSE" target = "./LICENSE" +[[build.copy]] +source = "./THIRD_PARTY_NOTICES.md" +target = "./THIRD_PARTY_NOTICES.md" + [[build.copy]] source = "./CHANGELOG.md" target = "./CHANGELOG.md" From 92002fd9641d2bf290739a5ef3204017eaf75339 Mon Sep 17 00:00:00 2001 From: jneb802 <73610029+jneb802@users.noreply.github.com> Date: Mon, 19 Oct 2026 11:07:40 -0700 Subject: [PATCH 4/7] Remove Endgame compatibility test harness --- PraetorisClient.csproj | 1 - .../EndgameCompatibility.csproj | 9 -- tests/EndgameCompatibility/Program.cs | 104 ------------------ tests/EndgameCompatibility/README.md | 100 ----------------- 4 files changed, 214 deletions(-) delete mode 100644 tests/EndgameCompatibility/EndgameCompatibility.csproj delete mode 100644 tests/EndgameCompatibility/Program.cs delete mode 100644 tests/EndgameCompatibility/README.md diff --git a/PraetorisClient.csproj b/PraetorisClient.csproj index 5b6fa25..82643f9 100644 --- a/PraetorisClient.csproj +++ b/PraetorisClient.csproj @@ -49,7 +49,6 @@ - PraetorisClient.GuideImages.%(Filename)%(Extension) diff --git a/tests/EndgameCompatibility/EndgameCompatibility.csproj b/tests/EndgameCompatibility/EndgameCompatibility.csproj deleted file mode 100644 index ef8a995..0000000 --- a/tests/EndgameCompatibility/EndgameCompatibility.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - Exe - net481 - 10 - enable - enable - - diff --git a/tests/EndgameCompatibility/Program.cs b/tests/EndgameCompatibility/Program.cs deleted file mode 100644 index def3104..0000000 --- a/tests/EndgameCompatibility/Program.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System.Collections; -using System.Reflection; - -// Exercise the actual candidate and Epic Loot IL without starting Unity. No copied compatibility implementation. -if (args.Length < 3 || args.Length > 4) - throw new ArgumentException("Usage: [native]"); -string game = Path.GetFullPath(args[2]); -string[] search = -{ - Path.Combine(game, "BepInEx/core"), - Path.Combine(game, "Valheim.app/Contents/Resources/Data/Managed"), - Path.Combine(game, "valheim_Data/Managed"), - Path.GetDirectoryName(Path.GetFullPath(args[0]))!, - Path.GetDirectoryName(Path.GetFullPath(args[1]))! -}; -AppDomain.CurrentDomain.AssemblyResolve += (_, request) => -{ - AssemblyName name = new(request.Name); - foreach (string directory in search) - { - string path = Path.Combine(directory, name.Name + ".dll"); - if (File.Exists(path)) return Assembly.LoadFrom(path); - } - string? plugin = Directory.EnumerateFiles(Path.Combine(game, "BepInEx/plugins"), name.Name + ".dll", SearchOption.AllDirectories).FirstOrDefault(); - return plugin == null ? null : Assembly.LoadFrom(plugin); -}; -Assembly epic = Assembly.LoadFrom(Path.GetFullPath(args[1])); -Assembly client = Assembly.LoadFrom(Path.GetFullPath(args[0])); -Type compatibility = client.GetType("PraetorisClient.EndgameCompatibility", true)!; -const BindingFlags flags = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; -object? Call(string name, params object?[] values) => compatibility.GetMethod(name, flags)!.Invoke(null, values); -void Require(bool condition, string name) -{ - if (!condition) throw new Exception("FAIL: " + name); - System.Console.WriteLine("PASS: " + name); -} -Require((bool)Call("IsTabletName", "ve_tablet")!, "legacy tablet"); -for (int tier = 1; tier <= 15; tier++) - Require((bool)Call("IsTabletName", "ve_tablet_tier" + tier.ToString("00"))!, "tablet tier " + tier); -foreach (string? name in new[] { null, "", "SwordIron", "ve_tablet_tier00", "ve_tablet_tier16", "ve_tablet_tier1", "ve_tablet_tier01(Clone)", "ve_tablet_tier+1" }) - Require(!(bool)Call("IsTabletName", new object?[] { name })!, "reject unrelated/invalid name " + (name ?? "null")); -bool native = args.Length == 4 && args[3] == "native"; -Require((bool)Call("HasNativeSupport", epic)! == native, native ? "upstream build recognized without item patches" : "legacy build does not claim native flags"); -if (native) return; - -Assembly harmony = Assembly.LoadFrom(Path.Combine(search[0], "0Harmony.dll")); -Type processor = harmony.GetType("HarmonyLib.PatchProcessor", true)!; -MethodInfo read = processor.GetMethods().Single(m => m.Name == "GetOriginalInstructions" && m.GetParameters().Length == 2 && !m.GetParameters()[1].ParameterType.IsByRef); -Type magic = epic.GetType("EpicLoot.MagicItem", true)!; -FieldInfo socketCount = magic.GetField("SocketCount")!; -Type rarity = epic.GetType("EpicLoot.ItemRarity", true)!; -Type item = epic.GetType("EpicLoot.EpicLoot", true)!.GetMethod("IsAllowedMagicItemType")!.GetParameters()[0].ParameterType; -Type roller = epic.GetType("EpicLoot.LootRoller", true)!; -MethodInfo weights = roller.GetMethod("GetSocketCountsPerRarity")!; -compatibility.GetField("socketCount", flags)!.SetValue(null, socketCount); -compatibility.GetField("socketWeights", flags)!.SetValue(null, weights); - -(MethodInfo Method, string Transform, string Helper, int Count)[] cases = -{ - (roller.GetMethod("RollMagicItem", new[] { rarity, item, typeof(float), typeof(float) })!, "PreventRolledSockets", "FilterSocketCount", 1), - (epic.GetType("EpicLoot.CraftingV2.EnchantingUIController", true)!.GetMethod("GetEnchantInfo", flags)!, "HideSocketPreview", "PreviewSocketWeights", 1), - (epic.GetType("EpicLoot.Crafting.TransferMagicalEffects", true)!.GetMethod("TransferMagicToCraftedItem", flags)!, "PreventTransferredSockets", "TransferSocketCount", 3) -}; -foreach ((MethodInfo method, string transform, string helper, int count) in cases) -{ - object instructions = read.Invoke(null, new object?[] { method, null })!; - object[] rewritten = ((IEnumerable)Call(transform, instructions)!).Cast().ToArray(); - int calls = rewritten.Count(i => (i.GetType().GetField("operand")!.GetValue(i) as MethodInfo)?.Name == helper); - Require(calls == count, method.Name + " rewrites actual production IL"); - // The version guard must not quietly accept a changed method body. - object empty = Activator.CreateInstance(instructions.GetType())!; - bool rejected = false; - try { _ = ((IEnumerable)Call(transform, empty)!).Cast().ToArray(); } - catch (InvalidOperationException) { rejected = true; } - Require(rejected, transform + " rejects changed IL"); -} - -compatibility.GetField("rarityType", flags)!.SetValue(null, rarity); -compatibility.GetField("craftedItem", flags)!.SetValue(null, - epic.GetType("EpicLoot.Crafting.TransferMagicalEffects", true)!.GetField("CraftedItem", flags)); -MethodInfo Required(string type, string method) => epic.GetType(type, true)!.GetMethod(method, flags)!; -(MethodInfo Method, string? Prefix, string? Postfix, string? Transpiler)[] patches = -{ - (Required("EpicLoot.EpicLoot", "IsAllowedMagicItemType"), null, "AllowTablet", null), - (Required("EpicLoot.GatedItemType.ItemTypeClassifier", "ClassifyFromFields"), null, "ClassifyTablet", null), - (cases[0].Method, null, null, cases[0].Transform), - (cases[1].Method, null, null, cases[1].Transform), - (cases[2].Method, null, null, cases[2].Transform), - (Required("EpicLoot.ShardStones.ShardSlotChisel", "CanApply"), "PreventChisel", null, null), - (Required("EpicLoot.ShardStones.ShardSocketManager", "CanSocket"), "PreventPlacement", null, null), - (Required("EpicLoot.ShardStones.ShardSocketManager", "CanCoexist"), "PreventPlacement", null, null), - (epic.GetType("EpicLoot.Crafting.CraftSuccessDialog", true)!.GetMethod("Show")!, null, "ShowCraftResult", null) -}; -foreach ((MethodInfo method, string? prefix, string? postfix, string? transpiler) in patches) -{ - Require(method != null, "patch target exists"); - foreach (string? callback in new[] { prefix, postfix, transpiler }.Where(name => name != null)) - Require(compatibility.GetMethod(callback!, flags) != null, "callback exists: " + callback); -} -foreach (string name in new[] { "CanSocket", "CanCoexist" }) - Require(Required("EpicLoot.ShardStones.ShardSocketManager", name).GetParameters() - .Any(p => p.Name == "reason" && p.ParameterType == typeof(string).MakeByRefType()), - name + " callback binds the actual rejection reason"); -System.Console.WriteLine("Runtime attachment and gameplay require a Unity client; this harness checks policy, targets, and IL transformations only."); diff --git a/tests/EndgameCompatibility/README.md b/tests/EndgameCompatibility/README.md deleted file mode 100644 index c925486..0000000 --- a/tests/EndgameCompatibility/README.md +++ /dev/null @@ -1,100 +0,0 @@ -# Temporary Endgame compatibility - -This bridges Epic Loot PRs [45](https://github.com/MidnightsFX/Randy_Vapok_ValheimMods/pull/45) -and [47](https://github.com/MidnightsFX/Randy_Vapok_ValheimMods/pull/47) for Endgame testing. -Keep the separately built Endgame DLL and its Epic Loot JSON patches in the client -package. This change does not publish or bundle an Endgame build. - -## Scope - -- Runs only when BepInEx loads both Epic Loot and Endgame. The optional dependency - makes Endgame load first; no source or binary reference to Endgame is needed. -- Supports the exact production Epic Loot 0.14.13 binary with SHA-256 - `802790e0cbd8a6f951627619a488b84e6a406e8770e2687b2fff8b63d2adf9ea`. - An unrecognized older binary gets a warning and no compatibility patches. -- Allows enchanting `ve_tablet` and `ve_tablet_tier01` through `ve_tablet_tier15`. - Other eligibility checks still apply. Keeps these items in the Tablet category - when Epic Loot rebuilds item configuration. -- Prevents socket rolls, insertion, swaps, chisel use, and crafting socket - transfers for those items. Omits socket probabilities from their preview. - Existing socket data is not erased on load; removal of old socket contents - remains available. -- Restores the crafting result's text and icon when the native fix is absent. -- Does not add general-purpose JSON fields to older Epic Loot. The temporary - tablet policy is compiled into this bridge, so clients and servers running it - use the same policy. This is not a backport for arbitrary item categories. -- Uses a separate Harmony owner. Removing this code does not require changing - Epic Loot's saved-item format. - -## Native support and removal - -Item patches are skipped when the loaded assembly has both config fields, both -runtime properties, and the native checks in all five socket paths and the -enchanting eligibility path. The crafting result fix is checked separately. -Field presence alone is not sufficient. - -Keep Endgame's iteminfo JSON patch, including `Enchantable: true` and -`Socketable: false`, for the eventual native release. Older Epic Loot can discard -unknown fields when it rewrites its generated config; this bridge does not rely -on those generated fields. On upgrade, verify the Endgame JSON patch has been -applied again and the native runtime contains both settings for every tablet. - -Before removing the bridge, test the upgraded Epic Loot with tablets saved by -the old build. Confirm values, effects, socket rules, save/restart, and further -enchanting. Then remove `EndgameCompatibility.cs`, its startup/shutdown calls, -its optional Endgame dependency, and this test project in one change. No live -upgrade or removal proof has been completed yet. - -## Local checks - -Build the client and test harness, then run under Mono (not the .NET Core runtime). -The harness loads the actual client DLL and an actual Epic Loot DLL. It invokes -the production compatibility code; it does not copy that code into a test model. - -```sh -dotnet build PraetorisClient.csproj -c Release -dotnet build tests/EndgameCompatibility -mono tests/EndgameCompatibility/bin/Debug/net481/EndgameCompatibility.exe \ - bin/Release/PraetorisClient.dll /path/to/production/EpicLoot.dll /path/to/Valheim -mono tests/EndgameCompatibility/bin/Debug/net481/EndgameCompatibility.exe \ - bin/Release/PraetorisClient.dll /path/to/native-support/EpicLoot.dll /path/to/Valheim native -``` - -On 2026-10-18: Release build passed with 83 existing warnings and zero errors. -The harness build had zero warnings and errors. The production binary passed 51 -checks for item names, native detection, patch targets, parameter binding, three -real IL transformations, and rejection of changed method bodies. A local Epic -Loot build containing PR 45 (SHA-256 -`1d3244a7387458bf4b056387ef8707d6172733ad0d6fabc63327dce289fc5e3b`) -passed 25 checks, including native detection. - -These checks do not attach runtime patches or exercise Unity gameplay. An -attempt to attach patches outside Unity failed in MonoMod's runtime detour -initialization. Actual attachment, gameplay, and saved-item migration must be -tested in Valheim. - -## Remaining live proof - -Valnet Client 01 was started and inspected on 2026-10-18. Production was Season 8 -8.0.46 with Epic Loot 0.14.13 and Praetoris Client 0.1.101. Valnet's maintained -profile reported Season 8 8.0.34; its Epic Loot binary matched production. -The profile and mmcli registry were backed up before attempting alignment. -The SSH key agent then refused signing requests. Alignment, baseline -reproduction, candidate deployment, and live proof did not run. - -Resume with a device lease and a verified current production mirror: - -1. Run the ordinary enchanting-table flow with Endgame and the unchanged client. - Record tablet eligibility and socket behavior before deploying this change. -2. Deploy the candidate client to that same profile. Keep Epic Loot unchanged. -3. Confirm the compatibility startup log and absence of new errors. Check all - tablet names and a normal weapon as a control. -4. Use the existing `cli_epicloot_item`, `cli_epicloot_enchant_ui`, - `cli_epicloot_roll`, and UI inspection commands from valheimCLI. Exercise the - actual enchanting button, materials consumption, preview, and result dialog. -5. Exercise socket insertion/swaps, chisels, and crafting transfers. Check that - rejected actions consume no items and normal weapons retain their behavior. -6. Save/restart with an enchanted tablet. Upgrade to the native-support build, - verify the skip log and loaded JSON flags, and repeat the checks. -7. Repeat the server/client flow on the current production mirror. Inspect both - logs, restore profile backups, verify restoration, and release devices. From 9a9183818ad4b4be5d944ee9061ce76dc8f5c056 Mon Sep 17 00:00:00 2001 From: jneb802 <73610029+jneb802@users.noreply.github.com> Date: Mon, 19 Oct 2026 11:38:20 -0700 Subject: [PATCH 5/7] Require server approval for Map Room beta entry --- CHANGELOG.md | 1 + Source/Core/Plugin.cs | 3 + Source/Endgame/EndgameBetaAccess.cs | 160 ++++++++++++++++++++++++ Source/NetworkWard/NetworkWardAccess.cs | 2 +- 4 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 Source/Endgame/EndgameBetaAccess.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index d64563e..fc27ac1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## 0.1.102 +- Require server approval from the Endgame beta Steam ID list before entering MWL_MapRoom1. Leaving remains unrestricted. - Reserve rights to original PraetorisClient and Valheim Endgame code and assets while preserving third-party license notices. - Include Valheim Endgame 0.5.0 and its tablet configuration in this package. - Allow tablets to be enchanted with the released Epic Loot 0.14.13, prevent new tablet sockets and shardstones, and restore the enchanting result display. diff --git a/Source/Core/Plugin.cs b/Source/Core/Plugin.cs index 149281d..4182302 100644 --- a/Source/Core/Plugin.cs +++ b/Source/Core/Plugin.cs @@ -104,6 +104,7 @@ public class PraetorisClientPlugin : BaseUnityPlugin internal static ConfigEntry CreatureOwnerWardUpdateIntervalSeconds = null!; internal static ConfigEntry DebugCreatureOwnerWard = null!; internal static ConfigEntry NetworkWardAllowedSteamIds = null!; + internal static ConfigEntry EndgameBetaAllowedSteamIds = null!; internal static ConfigEntry DebugServerChest = null!; internal static ConfigEntry FreeDeparturePortGuid = null!; internal static ConfigEntry BlockPeerServerSyncConfigSync = null!; @@ -297,6 +298,8 @@ private void BindConfig() SyncedDescription("Prevents trophy placement on the original spawn temple boss stones. Disable to allow normal placement. Changes apply without a restart. Reset stones and buildable boss stones remain usable.")); NetworkWardAllowedSteamIds = Config.Bind("NetworkWard", "AllowedSteamIds", "", "Server-only whitelist for Network Ward access. Comma-separated SteamID64 values (Steam_ prefix also accepted). Empty denies everyone, including admins. Client settings cannot grant access. Requires authenticated Steam connections."); + EndgameBetaAllowedSteamIds = Config.Bind("EndgameBeta", "AllowedSteamIds", "", + "Server-only list for entry to MWL_MapRoom1. Comma-separated SteamID64 values (Steam_ prefix also accepted). Empty denies everyone, including admins. Changes apply on the next entry. Client settings cannot grant access. Requires authenticated Steam connections."); LinkApiUrl = Config.Bind("BotApi", "LinkApiUrl", "", "Compatible bot Valheim link endpoint. Prefer the PRAETORISCLIENT_LINK_API_URL environment variable on dedicated servers."); BotApiKey = Config.Bind("BotApi", "ApiKey", "", "API key sent to the bot in the X-API-Key header. Prefer the PRAETORISCLIENT_BOT_API_KEY environment variable on dedicated servers."); LinkCommand = Config.Bind("Linking", "LinkCommand", "!link", "In-game chat command consumed before it is sent as chat."); diff --git a/Source/Endgame/EndgameBetaAccess.cs b/Source/Endgame/EndgameBetaAccess.cs new file mode 100644 index 0000000..c46ca6c --- /dev/null +++ b/Source/Endgame/EndgameBetaAccess.cs @@ -0,0 +1,160 @@ +using System; +using System.IO; +using BepInEx.Configuration; +using HarmonyLib; +using PraetorisClient.NetworkWardFeature; +using UnityEngine; + +namespace PraetorisClient +{ + internal sealed class EndgameBetaAccess : MonoBehaviour + { + private const string RequestRpc = "PraetorisEndgameBetaRequest"; + private const string ResponseRpc = "PraetorisEndgameBetaResponse"; + private static EndgameBetaAccess? _instance; + private ZNet? _session; + private ZRpc? _server; + private Teleport? _gateway; + private Teleport? _approvedGateway; + private int _request; + private bool _pending; + private float _deadline; + private float _retryAt; + + private static bool IsEntrance(Teleport gateway) + { + Transform? exterior = gateway.transform.parent; + while (exterior != null && exterior.name != "Exterior") exterior = exterior.parent; + if (exterior == null || exterior.parent == null) return false; + Transform root = exterior.parent; + if (root.name != "MWL_MapRoom1" && root.name != "MWL_MapRoom1(Clone)") return false; + return root.Find("Exterior/Blueprint/Gateway") == gateway.transform && + root.Find("Interior/ExteriorGateway") == gateway.m_targetPoint?.transform; + } + + private bool CurrentConnection() => _session != null && _session == ZNet.instance && + !_session.IsServer() && _server != null && ReferenceEquals(_server, _session.GetServerRPC()) && + _server.IsConnected() && Player.m_localPlayer != null && !Player.m_localPlayer.IsDead(); + + private void Request(Teleport gateway) + { + if (_pending || Time.unscaledTime < _retryAt) return; + _session = ZNet.instance; + _server = _session?.GetServerRPC(); + if (!CurrentConnection()) + { + Notice("Endgame beta access requires approval from a connected Steam server."); + _retryAt = Time.unscaledTime + 1; + return; + } + _gateway = gateway; + _request = _request == int.MaxValue ? 1 : _request + 1; + _pending = true; + _deadline = Time.unscaledTime + 5; + _server!.Invoke(RequestRpc, _request); + } + + private void Update() + { + if (!_pending) return; + if (!CurrentConnection()) { _pending = false; _gateway = null; return; } + if (Time.unscaledTime < _deadline) return; + _pending = false; + _gateway = null; + _retryAt = Time.unscaledTime + 1; + Notice("Unable to verify Endgame beta access. Please try again."); + } + + private static void OnRequest(ZRpc rpc, int request) + { + if (ZNet.instance == null || !ZNet.instance.IsServer()) return; + ZNetPeer peer = ZNet.instance.GetPeer(rpc); + ZSteamSocket? steam = NetworkWardAccess.FindSteamSocket(rpc.GetSocket()); + bool allowed = peer != null && peer.IsReady() && steam != null && IsAllowed(steam.GetHostName()); + PraetorisClientPlugin.Log.LogInfo("Endgame beta entry " + (allowed ? "approved." : "denied.")); + rpc.Invoke(ResponseRpc, request, allowed); + } + + private static bool IsAllowed(string steamId) + { + try + { + ConfigFile config = PraetorisClientPlugin.Instance!.Config; + if (!File.Exists(config.ConfigFilePath)) return false; + ConfigFile snapshot = new ConfigFile(config.ConfigFilePath, false) { SaveOnConfigSet = false }; + string allowed = snapshot.Bind("EndgameBeta", "AllowedSteamIds", "").Value; + config.Reload(); + PraetorisClientPlugin.EndgameBetaAllowedSteamIds.Value = allowed; + return NetworkWardAccessList.Contains(allowed, steamId); + } + catch (Exception exception) when (exception is IOException || exception is UnauthorizedAccessException) + { + PraetorisClientPlugin.Log.LogWarning("Endgame beta access denied: cannot read server configuration. " + exception.Message); + return false; + } + } + + private static void OnResponse(ZRpc rpc, int request, bool allowed) + { + EndgameBetaAccess? access = _instance; + if (access == null || !access._pending || request != access._request || + !ReferenceEquals(rpc, access._server) || !access.CurrentConnection() || + Time.unscaledTime >= access._deadline) return; + access._pending = false; + access._retryAt = Time.unscaledTime + 1; + Teleport? gateway = access._gateway; + access._gateway = null; + if (!allowed) + { + Notice("Valheim Endgame is currently in closed beta. You do not have access."); + return; + } + Player player = Player.m_localPlayer; + if (gateway == null || !gateway.isActiveAndEnabled || !IsEntrance(gateway) || player.IsTeleporting() || + Vector3.Distance(player.transform.position, gateway.transform.position) > 5) return; + access._approvedGateway = gateway; + try { gateway.Interact(player, false, false); } + finally { access._approvedGateway = null; } + } + + private static void Notice(string message) + { + Player.m_localPlayer?.Message(MessageHud.MessageType.Center, message); + PraetorisClientPlugin.Log.LogInfo(message); + } + + private void OnDestroy() + { + _pending = false; + _approvedGateway = _gateway = null; + if (_instance == this) _instance = null; + } + + [HarmonyPatch(typeof(Teleport), nameof(Teleport.Interact))] + private static class GateEntry + { + private static bool Prefix(Teleport __instance, Humanoid character, bool hold, ref bool __result) + { + if (!IsEntrance(__instance)) return true; + if (_instance != null && _instance._approvedGateway == __instance) return true; + __result = false; + if (hold || character != Player.m_localPlayer || character is not Player player || + player.IsDead() || player.IsTeleporting() || + Vector3.Distance(player.transform.position, __instance.transform.position) > 5) return false; + if (_instance == null) _instance = PraetorisClientPlugin.Instance!.gameObject.AddComponent(); + _instance.Request(__instance); + return false; + } + } + + [HarmonyPatch(typeof(ZNet), "OnNewConnection")] + private static class RegisterAccessRpc + { + private static void Postfix(ZNetPeer peer) + { + peer.m_rpc.Register(RequestRpc, OnRequest); + peer.m_rpc.Register(ResponseRpc, OnResponse); + } + } + } +} diff --git a/Source/NetworkWard/NetworkWardAccess.cs b/Source/NetworkWard/NetworkWardAccess.cs index e119117..be4049b 100644 --- a/Source/NetworkWard/NetworkWardAccess.cs +++ b/Source/NetworkWard/NetworkWardAccess.cs @@ -95,7 +95,7 @@ private static void OnRequest(ZRpc rpc, int request) rpc.Invoke(ResponseRpc, request, allowed); } - private static ZSteamSocket? FindSteamSocket(ISocket socket) + internal static ZSteamSocket? FindSteamSocket(ISocket socket) { // ServerSync and ConditionalConfigSync temporarily nest BufferingSocket // wrappers during login. Their Original fields retain the real connection. From 63c2c05b3e5de092b02b4bbe03eaba4270d9f71e Mon Sep 17 00:00:00 2001 From: jneb802 <73610029+jneb802@users.noreply.github.com> Date: Mon, 19 Oct 2026 11:48:44 -0700 Subject: [PATCH 6/7] Use Jotunn custom RPC for Endgame beta access --- Source/Core/Plugin.cs | 1 + Source/Endgame/EndgameBetaAccess.cs | 62 +++++++++++++++++------------ 2 files changed, 37 insertions(+), 26 deletions(-) diff --git a/Source/Core/Plugin.cs b/Source/Core/Plugin.cs index 4182302..cf99f59 100644 --- a/Source/Core/Plugin.cs +++ b/Source/Core/Plugin.cs @@ -142,6 +142,7 @@ public void Awake() Instance = this; BindConfig(); + EndgameBetaAccess.Initialize(); PrefabManager.OnPrefabsRegistered += BossSpawnMessageSuppression.Refresh; SynchronizationManager.OnConfigurationSynchronized += OnConfigurationSynchronized; PraetorisMagicEffects.Register(); diff --git a/Source/Endgame/EndgameBetaAccess.cs b/Source/Endgame/EndgameBetaAccess.cs index c46ca6c..cf9fdcc 100644 --- a/Source/Endgame/EndgameBetaAccess.cs +++ b/Source/Endgame/EndgameBetaAccess.cs @@ -1,7 +1,10 @@ using System; +using System.Collections; using System.IO; using BepInEx.Configuration; using HarmonyLib; +using Jotunn.Entities; +using Jotunn.Managers; using PraetorisClient.NetworkWardFeature; using UnityEngine; @@ -9,8 +12,7 @@ namespace PraetorisClient { internal sealed class EndgameBetaAccess : MonoBehaviour { - private const string RequestRpc = "PraetorisEndgameBetaRequest"; - private const string ResponseRpc = "PraetorisEndgameBetaResponse"; + private static CustomRPC _rpc = null!; private static EndgameBetaAccess? _instance; private ZNet? _session; private ZRpc? _server; @@ -21,6 +23,11 @@ internal sealed class EndgameBetaAccess : MonoBehaviour private float _deadline; private float _retryAt; + internal static void Initialize() + { + _rpc = NetworkManager.Instance.AddRPC("EndgameBetaAccess", OnRequest, OnResponse); + } + private static bool IsEntrance(Teleport gateway) { Transform? exterior = gateway.transform.parent; @@ -34,7 +41,8 @@ private static bool IsEntrance(Teleport gateway) private bool CurrentConnection() => _session != null && _session == ZNet.instance && !_session.IsServer() && _server != null && ReferenceEquals(_server, _session.GetServerRPC()) && - _server.IsConnected() && Player.m_localPlayer != null && !Player.m_localPlayer.IsDead(); + _server.IsConnected() && ZRoutedRpc.instance != null && + Player.m_localPlayer != null && !Player.m_localPlayer.IsDead(); private void Request(Teleport gateway) { @@ -51,7 +59,9 @@ private void Request(Teleport gateway) _request = _request == int.MaxValue ? 1 : _request + 1; _pending = true; _deadline = Time.unscaledTime + 5; - _server!.Invoke(RequestRpc, _request); + ZPackage package = new ZPackage(); + package.Write(_request); + _rpc.SendPackage(ZRoutedRpc.instance.GetServerPeerID(), package); } private void Update() @@ -65,14 +75,20 @@ private void Update() Notice("Unable to verify Endgame beta access. Please try again."); } - private static void OnRequest(ZRpc rpc, int request) + private static IEnumerator OnRequest(long sender, ZPackage package) { - if (ZNet.instance == null || !ZNet.instance.IsServer()) return; - ZNetPeer peer = ZNet.instance.GetPeer(rpc); - ZSteamSocket? steam = NetworkWardAccess.FindSteamSocket(rpc.GetSocket()); - bool allowed = peer != null && peer.IsReady() && steam != null && IsAllowed(steam.GetHostName()); + if (ZNet.instance == null || !ZNet.instance.IsServer() || package.Size() != sizeof(int)) yield break; + ZNetPeer peer = ZNet.instance.GetPeer(sender); + if (peer == null || !peer.IsReady()) yield break; + int request = package.ReadInt(); + ZSteamSocket? steam = NetworkWardAccess.FindSteamSocket(peer.m_rpc.GetSocket()); + bool allowed = steam != null && IsAllowed(steam.GetHostName()); PraetorisClientPlugin.Log.LogInfo("Endgame beta entry " + (allowed ? "approved." : "denied.")); - rpc.Invoke(ResponseRpc, request, allowed); + ZPackage response = new ZPackage(); + response.Write(request); + response.Write(allowed); + _rpc.SendPackage(sender, response); + yield break; } private static bool IsAllowed(string steamId) @@ -94,12 +110,15 @@ private static bool IsAllowed(string steamId) } } - private static void OnResponse(ZRpc rpc, int request, bool allowed) + private static IEnumerator OnResponse(long sender, ZPackage package) { EndgameBetaAccess? access = _instance; - if (access == null || !access._pending || request != access._request || - !ReferenceEquals(rpc, access._server) || !access.CurrentConnection() || - Time.unscaledTime >= access._deadline) return; + if (access == null || !access._pending || !access.CurrentConnection() || + sender != ZRoutedRpc.instance.GetServerPeerID() || + package.Size() != sizeof(int) + sizeof(bool) || Time.unscaledTime >= access._deadline) yield break; + int request = package.ReadInt(); + bool allowed = package.ReadBool(); + if (request != access._request) yield break; access._pending = false; access._retryAt = Time.unscaledTime + 1; Teleport? gateway = access._gateway; @@ -107,14 +126,15 @@ private static void OnResponse(ZRpc rpc, int request, bool allowed) if (!allowed) { Notice("Valheim Endgame is currently in closed beta. You do not have access."); - return; + yield break; } Player player = Player.m_localPlayer; if (gateway == null || !gateway.isActiveAndEnabled || !IsEntrance(gateway) || player.IsTeleporting() || - Vector3.Distance(player.transform.position, gateway.transform.position) > 5) return; + Vector3.Distance(player.transform.position, gateway.transform.position) > 5) yield break; access._approvedGateway = gateway; try { gateway.Interact(player, false, false); } finally { access._approvedGateway = null; } + yield break; } private static void Notice(string message) @@ -146,15 +166,5 @@ private static bool Prefix(Teleport __instance, Humanoid character, bool hold, r return false; } } - - [HarmonyPatch(typeof(ZNet), "OnNewConnection")] - private static class RegisterAccessRpc - { - private static void Postfix(ZNetPeer peer) - { - peer.m_rpc.Register(RequestRpc, OnRequest); - peer.m_rpc.Register(ResponseRpc, OnResponse); - } - } } } From 864fb502271ecc88a7f71187bf26c274f8a5ca54 Mon Sep 17 00:00:00 2001 From: jneb802 <73610029+jneb802@users.noreply.github.com> Date: Mon, 19 Oct 2026 12:12:59 -0700 Subject: [PATCH 7/7] Add live server switch for Endgame beta access --- CHANGELOG.md | 1 + Source/Core/Plugin.cs | 3 +++ Source/Endgame/EndgameBetaAccess.cs | 37 ++++++++++++++++++++++++----- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc27ac1..e0b1568 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## 0.1.102 - Require server approval from the Endgame beta Steam ID list before entering MWL_MapRoom1. Leaving remains unrestricted. +- Add a server-synced Endgame beta enable setting that can restore normal Map Room entry without a restart. - Reserve rights to original PraetorisClient and Valheim Endgame code and assets while preserving third-party license notices. - Include Valheim Endgame 0.5.0 and its tablet configuration in this package. - Allow tablets to be enchanted with the released Epic Loot 0.14.13, prevent new tablet sockets and shardstones, and restore the enchanting result display. diff --git a/Source/Core/Plugin.cs b/Source/Core/Plugin.cs index cf99f59..efcd5b1 100644 --- a/Source/Core/Plugin.cs +++ b/Source/Core/Plugin.cs @@ -105,6 +105,7 @@ public class PraetorisClientPlugin : BaseUnityPlugin internal static ConfigEntry DebugCreatureOwnerWard = null!; internal static ConfigEntry NetworkWardAllowedSteamIds = null!; internal static ConfigEntry EndgameBetaAllowedSteamIds = null!; + internal static ConfigEntry EndgameBetaEnabled = null!; internal static ConfigEntry DebugServerChest = null!; internal static ConfigEntry FreeDeparturePortGuid = null!; internal static ConfigEntry BlockPeerServerSyncConfigSync = null!; @@ -299,6 +300,8 @@ private void BindConfig() SyncedDescription("Prevents trophy placement on the original spawn temple boss stones. Disable to allow normal placement. Changes apply without a restart. Reset stones and buildable boss stones remain usable.")); NetworkWardAllowedSteamIds = Config.Bind("NetworkWard", "AllowedSteamIds", "", "Server-only whitelist for Network Ward access. Comma-separated SteamID64 values (Steam_ prefix also accepted). Empty denies everyone, including admins. Client settings cannot grant access. Requires authenticated Steam connections."); + EndgameBetaEnabled = Config.Bind("EndgameBeta", "Enabled", true, + SyncedDescription("Restricts entry to MWL_MapRoom1 to the server beta list. Disable to restore normal gateway entry. Changes apply to connected clients without a restart.")); EndgameBetaAllowedSteamIds = Config.Bind("EndgameBeta", "AllowedSteamIds", "", "Server-only list for entry to MWL_MapRoom1. Comma-separated SteamID64 values (Steam_ prefix also accepted). Empty denies everyone, including admins. Changes apply on the next entry. Client settings cannot grant access. Requires authenticated Steam connections."); LinkApiUrl = Config.Bind("BotApi", "LinkApiUrl", "", "Compatible bot Valheim link endpoint. Prefer the PRAETORISCLIENT_LINK_API_URL environment variable on dedicated servers."); diff --git a/Source/Endgame/EndgameBetaAccess.cs b/Source/Endgame/EndgameBetaAccess.cs index cf9fdcc..ec26a69 100644 --- a/Source/Endgame/EndgameBetaAccess.cs +++ b/Source/Endgame/EndgameBetaAccess.cs @@ -22,10 +22,13 @@ internal sealed class EndgameBetaAccess : MonoBehaviour private bool _pending; private float _deadline; private float _retryAt; + private float _refreshAt; + private static DateTime _configWriteUtc; internal static void Initialize() { _rpc = NetworkManager.Instance.AddRPC("EndgameBetaAccess", OnRequest, OnResponse); + _instance = PraetorisClientPlugin.Instance!.gameObject.AddComponent(); } private static bool IsEntrance(Teleport gateway) @@ -66,6 +69,21 @@ private void Request(Teleport gateway) private void Update() { + if (ZNet.instance != null && ZNet.instance.IsServer()) + { + if (Time.unscaledTime >= _refreshAt) + { + _refreshAt = Time.unscaledTime + 1; + RefreshServerConfig(); + } + return; + } + if (!PraetorisClientPlugin.EndgameBetaEnabled.Value) + { + _pending = false; + _gateway = null; + return; + } if (!_pending) return; if (!CurrentConnection()) { _pending = false; _gateway = null; return; } if (Time.unscaledTime < _deadline) return; @@ -82,7 +100,8 @@ private static IEnumerator OnRequest(long sender, ZPackage package) if (peer == null || !peer.IsReady()) yield break; int request = package.ReadInt(); ZSteamSocket? steam = NetworkWardAccess.FindSteamSocket(peer.m_rpc.GetSocket()); - bool allowed = steam != null && IsAllowed(steam.GetHostName()); + bool allowed = RefreshServerConfig() && (!PraetorisClientPlugin.EndgameBetaEnabled.Value || + steam != null && NetworkWardAccessList.Contains(PraetorisClientPlugin.EndgameBetaAllowedSteamIds.Value, steam.GetHostName())); PraetorisClientPlugin.Log.LogInfo("Endgame beta entry " + (allowed ? "approved." : "denied.")); ZPackage response = new ZPackage(); response.Write(request); @@ -91,17 +110,24 @@ private static IEnumerator OnRequest(long sender, ZPackage package) yield break; } - private static bool IsAllowed(string steamId) + private static bool RefreshServerConfig() { try { ConfigFile config = PraetorisClientPlugin.Instance!.Config; if (!File.Exists(config.ConfigFilePath)) return false; + DateTime written = File.GetLastWriteTimeUtc(config.ConfigFilePath); + if (written == _configWriteUtc) return true; ConfigFile snapshot = new ConfigFile(config.ConfigFilePath, false) { SaveOnConfigSet = false }; + bool enabled = snapshot.Bind("EndgameBeta", "Enabled", true).Value; string allowed = snapshot.Bind("EndgameBeta", "AllowedSteamIds", "").Value; config.Reload(); + bool repairEnabled = PraetorisClientPlugin.EndgameBetaEnabled.Value != enabled; + PraetorisClientPlugin.EndgameBetaEnabled.Value = enabled; PraetorisClientPlugin.EndgameBetaAllowedSteamIds.Value = allowed; - return NetworkWardAccessList.Contains(allowed, steamId); + if (repairEnabled) config.Reload(); + _configWriteUtc = File.GetLastWriteTimeUtc(config.ConfigFilePath); + return true; } catch (Exception exception) when (exception is IOException || exception is UnauthorizedAccessException) { @@ -155,14 +181,13 @@ private static class GateEntry { private static bool Prefix(Teleport __instance, Humanoid character, bool hold, ref bool __result) { - if (!IsEntrance(__instance)) return true; + if (!PraetorisClientPlugin.EndgameBetaEnabled.Value || !IsEntrance(__instance)) return true; if (_instance != null && _instance._approvedGateway == __instance) return true; __result = false; if (hold || character != Player.m_localPlayer || character is not Player player || player.IsDead() || player.IsTeleporting() || Vector3.Distance(player.transform.position, __instance.transform.position) > 5) return false; - if (_instance == null) _instance = PraetorisClientPlugin.Instance!.gameObject.AddComponent(); - _instance.Request(__instance); + _instance!.Request(__instance); return false; } }