diff --git a/CHANGELOG.md b/CHANGELOG.md index 36b95e6..e0b1568 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## 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. +- 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/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/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..41ad124 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. + +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 - [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 e2378a1..efcd5b1 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)] @@ -32,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"; @@ -103,6 +104,8 @@ 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 EndgameBetaEnabled = null!; internal static ConfigEntry DebugServerChest = null!; internal static ConfigEntry FreeDeparturePortGuid = null!; internal static ConfigEntry BlockPeerServerSyncConfigSync = null!; @@ -140,6 +143,7 @@ public void Awake() Instance = this; BindConfig(); + EndgameBetaAccess.Initialize(); PrefabManager.OnPrefabsRegistered += BossSpawnMessageSuppression.Refresh; SynchronizationManager.OnConfigurationSynchronized += OnConfigurationSynchronized; PraetorisMagicEffects.Register(); @@ -166,6 +170,7 @@ public void Awake() FrameTimeMonitor.Initialize(); RpcTraceTelemetry.Initialize(); ApplyHarmonyPatches(epicLootLoaded); + EndgameCompatibility.Initialize(); CraftyBoxesWardGuard.TryApply(_harmony); PublicBasicPickables.TryApply(_harmony); ShippingPortDepartures.Initialize(_harmony); @@ -226,6 +231,7 @@ private void Update() private void OnDestroy() { + EndgameCompatibility.Shutdown(); GuardStoneFeature.GuardStoneMapPins.Clear(); GuardStoneFeature.SpawnIslandWards.ClearSurvey(); PrefabManager.OnPrefabsRegistered -= BossSpawnMessageSuppression.Refresh; @@ -294,6 +300,10 @@ 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."); 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..ec26a69 --- /dev/null +++ b/Source/Endgame/EndgameBetaAccess.cs @@ -0,0 +1,195 @@ +using System; +using System.Collections; +using System.IO; +using BepInEx.Configuration; +using HarmonyLib; +using Jotunn.Entities; +using Jotunn.Managers; +using PraetorisClient.NetworkWardFeature; +using UnityEngine; + +namespace PraetorisClient +{ + internal sealed class EndgameBetaAccess : MonoBehaviour + { + private static CustomRPC _rpc = null!; + 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 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) + { + 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() && ZRoutedRpc.instance != null && + 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; + ZPackage package = new ZPackage(); + package.Write(_request); + _rpc.SendPackage(ZRoutedRpc.instance.GetServerPeerID(), package); + } + + 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; + _pending = false; + _gateway = null; + _retryAt = Time.unscaledTime + 1; + Notice("Unable to verify Endgame beta access. Please try again."); + } + + private static IEnumerator OnRequest(long sender, ZPackage package) + { + 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 = 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); + response.Write(allowed); + _rpc.SendPackage(sender, response); + yield break; + } + + 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; + if (repairEnabled) config.Reload(); + _configWriteUtc = File.GetLastWriteTimeUtc(config.ConfigFilePath); + return true; + } + 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 IEnumerator OnResponse(long sender, ZPackage package) + { + EndgameBetaAccess? access = _instance; + 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; + access._gateway = null; + if (!allowed) + { + Notice("Valheim Endgame is currently in closed beta. You do not have access."); + 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) yield break; + access._approvedGateway = gateway; + try { gateway.Interact(player, false, false); } + finally { access._approvedGateway = null; } + yield break; + } + + 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 (!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; + _instance!.Request(__instance); + return false; + } + } + } +} 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/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. 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..a3c6157 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,10 +26,30 @@ 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" +[[build.copy]] +source = "./THIRD_PARTY_NOTICES.md" +target = "./THIRD_PARTY_NOTICES.md" + [[build.copy]] source = "./CHANGELOG.md" target = "./CHANGELOG.md"