Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of OathBound v0.1.2
plugins\OathBound.dll
Decompiled 2 months ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using System.Text.RegularExpressions; using System.Threading; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using ConfigReload; using HarmonyLib; using JetBrains.Annotations; using Microsoft.CodeAnalysis; using PatchGuard; using ServerSync; using SyncedConfig; using TMPro; using UnityEngine; using YamlConfig; using YamlDotNet.Core; using YamlDotNet.Core.Events; using YamlDotNet.Core.Tokens; using YamlDotNet.Helpers; using YamlDotNet.Serialization; using YamlDotNet.Serialization.BufferedDeserialization; using YamlDotNet.Serialization.BufferedDeserialization.TypeDiscriminators; using YamlDotNet.Serialization.Callbacks; using YamlDotNet.Serialization.Converters; using YamlDotNet.Serialization.EventEmitters; using YamlDotNet.Serialization.NamingConventions; using YamlDotNet.Serialization.NodeDeserializers; using YamlDotNet.Serialization.NodeTypeResolvers; using YamlDotNet.Serialization.ObjectFactories; using YamlDotNet.Serialization.ObjectGraphTraversalStrategies; using YamlDotNet.Serialization.ObjectGraphVisitors; using YamlDotNet.Serialization.Schemas; using YamlDotNet.Serialization.TypeInspectors; using YamlDotNet.Serialization.TypeResolvers; using YamlDotNet.Serialization.Utilities; using YamlDotNet.Serialization.ValueDeserializers; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: IgnoresAccessChecksTo("assembly_utils")] [assembly: IgnoresAccessChecksTo("assembly_valheim")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyCompany("OathBound")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyDescription("OathBound: boss progression gated on the whole group")] [assembly: AssemblyFileVersion("0.1.2.0")] [assembly: AssemblyInformationalVersion("0.1.2")] [assembly: AssemblyProduct("OathBound")] [assembly: AssemblyTitle("OathBound")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.1.2.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace OathBound { public sealed class Stage { public string Name; public string Key; public string BossPrefab; public int Order; } public static class Chain { public static IReadOnlyList<Stage> Stages { get; private set; } = Vanilla(); public static event Action Changed; public static List<Stage> Vanilla() { return new List<Stage> { new Stage { Name = "Eikthyr", Key = "defeated_eikthyr", BossPrefab = "Eikthyr", Order = 1 }, new Stage { Name = "TheElder", Key = "defeated_gdking", BossPrefab = "gd_king", Order = 2 }, new Stage { Name = "Bonemass", Key = "defeated_bonemass", BossPrefab = "Bonemass", Order = 3 }, new Stage { Name = "Moder", Key = "defeated_dragon", BossPrefab = "Dragon", Order = 4 }, new Stage { Name = "Yagluth", Key = "defeated_goblinking", BossPrefab = "GoblinKing", Order = 5 }, new Stage { Name = "TheQueen", Key = "defeated_queen", BossPrefab = "SeekerQueen", Order = 6 }, new Stage { Name = "Fader", Key = "defeated_fader", BossPrefab = "Fader", Order = 7 } }; } public static void Set(IEnumerable<Stage> stages) { Stages = stages.OrderBy((Stage s) => s.Order).ToList(); OathBound.Log.LogInfo((object)("Progression chain: " + string.Join(" > ", Stages.Select((Stage s) => s.Name)))); Chain.Changed?.Invoke(); } public static Stage ByBoss(string prefabName) { return Stages.FirstOrDefault((Stage s) => string.Equals(s.BossPrefab, prefabName, StringComparison.OrdinalIgnoreCase)); } public static Stage ByKey(string key) { return Stages.FirstOrDefault((Stage s) => string.Equals(s.Key, key, StringComparison.OrdinalIgnoreCase)); } public static Stage Find(string nameOrKey) { return Stages.FirstOrDefault((Stage s) => string.Equals(s.Name, nameOrKey, StringComparison.OrdinalIgnoreCase)) ?? ByKey(nameOrKey) ?? ByBoss(nameOrKey); } public static Stage Previous(Stage stage) { int num = Stages.ToList().IndexOf(stage); if (num <= 0) { return null; } return Stages[num - 1]; } } public sealed class ChainDocument : YamlDocument { private readonly List<Stage> stages = new List<Stage>(); protected override void ParsePrimarySection(Section sect) { int target = 0; string target2 = null; string target3 = null; ParseScalarFromObject(ref target, sect, "order"); ParseStringFromObject(ref target2, sect, "key"); ParseStringFromObject(ref target3, sect, "boss"); if (string.IsNullOrEmpty(target2)) { configErrors.Add(sect.Path + ": missing 'key', the global key the game sets when the boss dies"); } if (string.IsNullOrEmpty(target3)) { configErrors.Add(sect.Path + ": missing 'boss', the prefab name of the boss the altar spawns"); } stages.Add(new Stage { Name = sect.dictKey, Key = target2, BossPrefab = target3, Order = target }); } protected override void Verify() { foreach (IGrouping<int, Stage> item in from s in stages group s by s.Order into g where g.Count() > 1 select g) { configWarnings.Add(string.Format("stages {0} share order {1}; their relative order is undefined", string.Join(", ", item.Select((Stage s) => s.Name)), item.Key)); } if (stages.Count == 0) { configErrors.Add("the chain contains no stages"); } } public List<Stage> Process() { return stages; } } public static class AltarGate { public static bool IsClosed(OfferingBowl bowl, Humanoid user) { if ((Object)(object)bowl.m_bossPrefab == (Object)null) { return false; } ProgressState.StageStatus stageStatus = ProgressState.ForBoss(((Object)bowl.m_bossPrefab).name); if (stageStatus == null || stageStatus.Open) { return false; } if (user != null) { ((Character)user).Message((MessageType)2, ProgressState.ClosedMessage(stageStatus), 0, (Sprite)null, false); } return true; } } [HarmonyPatch(typeof(OfferingBowl), "UseItem")] public static class OfferingBowlUseItemPatch { [HarmonyPrefix] public static bool Prefix(OfferingBowl __instance, Humanoid user, ref bool __result) { if (!AltarGate.IsClosed(__instance, user)) { return true; } __result = false; return false; } } [HarmonyPatch(typeof(OfferingBowl), "Interact")] public static class OfferingBowlInteractPatch { [HarmonyPrefix] public static bool Prefix(OfferingBowl __instance, Humanoid user, ref bool __result) { if (!__instance.m_useItemStands || !AltarGate.IsClosed(__instance, user)) { return true; } __result = false; return false; } } [HarmonyPatch(typeof(OfferingBowl), "RPC_SpawnBoss")] public static class OfferingBowlSpawnGuardPatch { [HarmonyPrefix] public static bool Prefix(OfferingBowl __instance) { if (!OathBoundConfiguration.SpawnGuard.Value || !AltarGate.IsClosed(__instance, null)) { return true; } OathBound.Log.LogWarning((object)("Blocked a spawn of " + ((Object)__instance.m_bossPrefab).name + ": its stage is closed. The summoning client may have stale state.")); return false; } } [HarmonyPatch(typeof(Character), "OnDeath")] public static class KillReporter { [HarmonyPrefix] public static void Prefix(Character __instance) { //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_011f: Unknown result type (might be due to invalid IL or missing references) if (__instance.IsPlayer() || string.IsNullOrEmpty(__instance.m_defeatSetGlobalKey)) { return; } ZNetView nview = __instance.m_nview; if ((Object)(object)nview == (Object)null || !nview.IsValid() || !nview.IsOwner() || Chain.ByKey(__instance.m_defeatSetGlobalKey) == null) { return; } ZDO zDO = nview.GetZDO(); List<string> list = new List<string>(); foreach (PlayerInfo player in ZNet.instance.GetPlayerList()) { int s_attackers = ZDOVars.s_attackers; if (zDO.GetBool(s_attackers + player.m_name, false)) { list.Add(player.m_name); } } OathBound.Log.LogInfo((object)(__instance.m_name + " died, attackers: " + ((list.Count > 0) ? string.Join(", ", list) : "none recorded") + ".")); ZRoutedRpc.instance.InvokeRoutedRPC("OathBound_BossDefeated", new object[3] { __instance.m_defeatSetGlobalKey, string.Join("\n", list), ((Component)__instance).transform.position }); } } public static class ProgressState { public sealed class StageStatus { public string BossPrefab; public string Name; public string PreviousName; public string Waiting; public bool Open => string.IsNullOrEmpty(Waiting); } private static CustomSyncedValue<string> synced; private static readonly Dictionary<string, StageStatus> byBoss = new Dictionary<string, StageStatus>(StringComparer.OrdinalIgnoreCase); public static IReadOnlyCollection<StageStatus> Stages => byBoss.Values; public static void Initialize(SyncedConfiguration config) { synced = new CustomSyncedValue<string>(config.Sync, "oathbound_state", ""); synced.ValueChanged += delegate { Guard.Run("progress state changed", Parse); }; } public static void Assign(string text) { if (synced.Value != text) { synced.AssignLocalValue(text); } } private static void Parse() { byBoss.Clear(); string[] array = (synced.Value ?? "").Split(new char[1] { '\n' }); for (int i = 0; i < array.Length; i++) { string[] array2 = array[i].Split(new char[1] { '\t' }); if (array2.Length >= 4 && array2[0].Length != 0) { byBoss[array2[0]] = new StageStatus { BossPrefab = array2[0], Name = array2[1], PreviousName = array2[2], Waiting = array2[3] }; } } } public static StageStatus ForBoss(string bossPrefab) { if (bossPrefab == null || !byBoss.TryGetValue(bossPrefab, out var value)) { return null; } return value; } public static string ClosedMessage(StageStatus status) { string text = (OathBoundConfiguration.NameMissingPlayers.Value ? status.Waiting : "the group"); return status.Name + " will not answer. Waiting for " + text + " to defeat " + status.PreviousName + "."; } public static string Summary() { if (byBoss.Count == 0) { return "OathBound: no progression state received from the server yet."; } return "OathBound status\n" + string.Join("\n", byBoss.Values.Select((StageStatus s) => " " + s.Name + ": " + (s.Open ? "open" : ("waiting for " + s.Waiting)))); } } public static class Commands { [Serializable] [CompilerGenerated] private sealed class <>c { public static readonly <>c <>9 = new <>c(); public static ConsoleEvent <>9__1_0; public static ConsoleOptionsFetcher <>9__1_1; internal void <Register>b__1_0(ConsoleEventArgs args) { Guard.Run("oathbound command", new <>c__DisplayClass1_0 { args = args }.<Register>b__2); } internal List<string> <Register>b__1_1() { return new List<string> { "status", "grant", "revoke", "ignore", "unignore", "forget" }; } } [CompilerGenerated] private sealed class <>c__DisplayClass1_0 { public ConsoleEventArgs args; internal void <Register>b__2() { Run(args); } } private const string Usage = "oathbound status | grant <player> <stage> | revoke <player> <stage> | ignore <player> | unignore <player> | forget <player>"; public static void Register() { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Expected O, but got Unknown //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Expected O, but got Unknown object obj = <>c.<>9__1_0; if (obj == null) { ConsoleEvent val = delegate(ConsoleEventArgs args) { Guard.Run("oathbound command", delegate { Run(args); }); }; <>c.<>9__1_0 = val; obj = (object)val; } object obj2 = <>c.<>9__1_1; if (obj2 == null) { ConsoleOptionsFetcher val2 = () => new List<string> { "status", "grant", "revoke", "ignore", "unignore", "forget" }; <>c.<>9__1_1 = val2; obj2 = (object)val2; } new ConsoleCommand("oathbound", "oathbound status | grant <player> <stage> | revoke <player> <stage> | ignore <player> | unignore <player> | forget <player>", (ConsoleEvent)obj, false, true, false, false, false, false, (ConsoleOptionsFetcher)obj2, false, false, false); } private static void Run(ConsoleEventArgs args) { if ((Object)(object)ZNet.instance == (Object)null || ZRoutedRpc.instance == null) { Print("OathBound: not connected to a world."); return; } string text = ((args.Args.Length > 1) ? string.Join(" ", args.Args, 1, args.Args.Length - 1) : "status"); ZRoutedRpc.instance.InvokeRoutedRPC("OathBound_Command", new object[1] { text }); } public static void Print(string text) { if ((Object)(object)Console.instance == (Object)null) { OathBound.Log.LogInfo((object)text); return; } string[] array = text.Split(new char[1] { '\n' }); foreach (string text2 in array) { ((Terminal)Console.instance).AddString(text2); } } } [HarmonyPatch(typeof(Terminal), "InitTerminal")] public static class TerminalInitPatch { private static bool registered; [HarmonyPostfix] public static void Postfix() { if (!registered) { registered = true; Commands.Register(); } } } [HarmonyPatch(typeof(ZNet), "Awake")] public static class ZNetAwakePatch { [HarmonyPostfix] [HarmonyPriority(200)] public static void Postfix(ZNet __instance) { ProgressServer.RegisterRpcs(); if (__instance.IsServer() && __instance.IsDedicated()) { OathBound.Synced.Yaml.ApplyAll(); } } } [HarmonyPatch(typeof(ZNet), "OnDestroy")] public static class ZNetDestroyPatch { [HarmonyPostfix] public static void Postfix() { ProgressServer.Shutdown(); } } [HarmonyPatch(typeof(ZNet), "RPC_PlayerID")] public static class ZNetPlayerIdPatch { [HarmonyPostfix] public static void Postfix(ZNet __instance, ZRpc rpc) { ZNetPeer peer = __instance.GetPeer(rpc); if (peer != null) { ProgressServer.PlayerSeen(peer.m_playerID, peer.m_playerName); } } } [HarmonyPatch(typeof(ZNet), "Disconnect")] public static class ZNetDisconnectPatch { [HarmonyPostfix] public static void Postfix(ZNetPeer peer) { if (peer != null) { ProgressServer.PlayerLeft(peer.m_playerID); } } } [HarmonyPatch(typeof(Player), "OnSpawned")] public static class PlayerSpawnedPatch { [HarmonyPostfix] public static void Postfix(Player __instance) { if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer() && !ZNet.instance.IsDedicated() && (Object)(object)__instance == (Object)(object)Player.m_localPlayer) { ProgressServer.PlayerSeen(__instance.GetPlayerID(), __instance.GetPlayerName()); } } } [BepInPlugin("com.OathBound", "OathBound", "0.1.2")] public class OathBound : BaseUnityPlugin { public const string PluginGuid = "com.OathBound"; public const string PluginName = "OathBound"; public const string PluginVersion = "0.1.2"; public static ManualLogSource Log { get; private set; } public static SyncedConfiguration Synced { get; private set; } private void Awake() { //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; Synced = new SyncedConfiguration((BaseUnityPlugin)(object)this, ((BaseUnityPlugin)this).Logger, "OathBound", "0.1.2"); OathBoundConfiguration.Initialize(Synced); ProgressState.Initialize(Synced); Chain.Changed += ProgressServer.Publish; Harmony val = new Harmony("com.OathBound"); val.PatchAll(Assembly.GetExecutingAssembly()); Synced.Finish(val); Guard.Install(val, ((BaseUnityPlugin)this).Logger, Assembly.GetExecutingAssembly()); Log.LogInfo((object)"OathBound 0.1.2 loaded."); } } public enum LateJoinerMode { Catchup, Earn } public static class OathBoundConfiguration { public static ConfigEntry<bool> LockConfiguration { get; private set; } public static ConfigEntry<float> CreditRadius { get; private set; } public static ConfigEntry<bool> CreditEveryoneOnline { get; private set; } public static ConfigEntry<int> InactiveDays { get; private set; } public static ConfigEntry<bool> CountOnlyOnline { get; private set; } public static ConfigEntry<LateJoinerMode> LateJoiners { get; private set; } public static ConfigEntry<bool> SpawnGuard { get; private set; } public static ConfigEntry<bool> NameMissingPlayers { get; private set; } public static YamlConfigSource ChainSource { get; private set; } public static void Initialize(SyncedConfiguration config) { LockConfiguration = config.BindLocking("General", "Lock Configuration", defaultValue: true, "[Server Only] The configuration is locked and may not be changed by clients once it has been synced from the server. Only valid for server config, will have no effect on clients."); CreditRadius = config.Bind("Credit", "Credit Radius", 200f, "Players within this many meters of a boss when it dies are credited with defeating it even if they never hit it. Players who hit the boss are always credited. 0 disables the radius rule.", true, null); CreditEveryoneOnline = config.Bind("Credit", "Credit Everyone Online", false, "Credit every online player when a boss dies, regardless of distance. For groups who trust each other.", true, null); InactiveDays = config.Bind("Group", "Inactive Days", 14, "Players who have not logged in for this many days are not counted when checking whether the whole group has defeated a boss. 0 disables the timeout.", true, null); CountOnlyOnline = config.Bind("Group", "Count Only Online", false, "Only players who are online right now must have defeated the previous boss. Off means every known player counts, minus ignored and inactive ones.", true, null); LateJoiners = config.Bind("Group", "Late Joiners", LateJoinerMode.Catchup, "Catchup: a player joining for the first time is credited with every boss the world has already defeated. Earn: they must fight every boss.", true, null); SpawnGuard = config.Bind("Gate", "Spawn Guard", true, "Also block the boss spawn itself when the stage is closed, in case a client with stale state or another mod uses the altar.", true, null); NameMissingPlayers = config.Bind("Gate", "Name Missing Players", true, "The altar message names the players who still need to defeat the previous boss. Off says 'the group' instead.", true, null); ChainSource = config.AddYaml(new YamlConfigSource("OathBoundChain*.yml", "oathboundchain", () => new ChainDocument(), delegate(YamlDocument doc) { Chain.Set(((ChainDocument)doc).Process()); }) { DefaultFileContent = SyncedConfiguration.EmbeddedResource(Assembly.GetExecutingAssembly(), "OathBound.OathBoundChain.yml"), EditButtonName = () => "Edit progression chain" }); config.Config.SettingChanged += delegate(object _, SettingChangedEventArgs args) { Guard.Run("republish after config change", delegate { if (args.ChangedSetting.Definition.Section == "Group") { ProgressServer.Publish(); } }); }; } } public static class ProgressServer { private struct OnlinePlayer { public long Id; public string Name; public Vector3 Position; } public const string RpcBossDefeated = "OathBound_BossDefeated"; public const string RpcCommand = "OathBound_Command"; public const string RpcReply = "OathBound_Reply"; private static Roster roster; public static bool IsServer { get { if ((Object)(object)ZNet.instance != (Object)null) { return ZNet.instance.IsServer(); } return false; } } public static void RegisterRpcs() { ZRoutedRpc.instance.Register<string, string, Vector3>("OathBound_BossDefeated", (Action<long, string, string, Vector3>)delegate(long sender, string key, string attackers, Vector3 position) { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) Guard.Run("boss defeated", delegate { //IL_000d: Unknown result type (might be due to invalid IL or missing references) OnBossDefeated(key, attackers, position); }); }); ZRoutedRpc.instance.Register<string>("OathBound_Command", (Action<long, string>)delegate(long sender, string line) { Guard.Run("command", delegate { OnCommand(sender, line); }); }); ZRoutedRpc.instance.Register<string>("OathBound_Reply", (Action<long, string>)delegate(long sender, string text) { Guard.Run("reply", delegate { Commands.Print(text); }); }); } public static void Shutdown() { roster?.Dispose(); roster = null; } private static Roster EnsureRoster() { if (roster != null) { return roster; } roster = new Roster(ZNet.instance.GetWorldName()); if (!roster.Load()) { roster.Data.InstalledKeys = (from s in Chain.Stages where IsWorldKeySet(s.Key) select s.Key).ToList(); roster.Save(); OathBound.Log.LogInfo((object)("Created roster " + roster.FilePath + ((roster.Data.InstalledKeys.Count > 0) ? (", already defeated: " + string.Join(", ", roster.Data.InstalledKeys)) : ""))); } roster.Changed += Publish; roster.Watch(); return roster; } private static bool IsWorldKeySet(string key) { if ((Object)(object)ZoneSystem.instance != (Object)null) { return ZoneSystem.instance.GetGlobalKey(key); } return false; } public static void PlayerSeen(long id, string name) { if (!IsServer || id == 0L) { return; } Roster roster = EnsureRoster(); bool isNew; RosterEntry rosterEntry = roster.Touch(id, name, out isNew); if (isNew && OathBoundConfiguration.LateJoiners.Value == LateJoinerMode.Catchup) { foreach (Stage stage in Chain.Stages) { if (IsWorldKeySet(stage.Key) && !rosterEntry.Cleared.Contains(stage.Key)) { rosterEntry.Cleared.Add(stage.Key); } } if (rosterEntry.Cleared.Count > 0) { OathBound.Log.LogInfo((object)(name + " joined and caught up with the group: " + string.Join(", ", rosterEntry.Cleared))); } } roster.Save(); Publish(); } public static void PlayerLeft(long id) { if (IsServer && id != 0L) { RosterEntry rosterEntry = EnsureRoster().Find(id); if (rosterEntry != null) { rosterEntry.LastSeen = Roster.Now; roster.Save(); } Publish(); } } private static IEnumerable<OnlinePlayer> Online() { foreach (ZNetPeer peer in ZNet.instance.GetPeers()) { if (peer.m_playerID != 0L) { yield return new OnlinePlayer { Id = peer.m_playerID, Name = peer.m_playerName, Position = peer.m_refPos }; } } Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer != (Object)null && !ZNet.instance.IsDedicated()) { yield return new OnlinePlayer { Id = localPlayer.GetPlayerID(), Name = localPlayer.GetPlayerName(), Position = ((Component)localPlayer).transform.position }; } } private static void OnBossDefeated(string key, string attackers, Vector3 position) { //IL_00c2: Unknown result type (might be due to invalid IL or missing references) //IL_00c7: Unknown result type (might be due to invalid IL or missing references) if (!IsServer) { return; } Stage stage = Chain.ByKey(key); if (stage == null) { OathBound.Log.LogInfo((object)("A boss with key " + key + " died; it is not in the chain.")); return; } Roster roster = EnsureRoster(); HashSet<string> hashSet = new HashSet<string>(from n in attackers.Split(new char[1] { '\n' }) where n.Length > 0 select n, StringComparer.Ordinal); float value = OathBoundConfiguration.CreditRadius.Value; bool value2 = OathBoundConfiguration.CreditEveryoneOnline.Value; List<string> list = new List<string>(); foreach (OnlinePlayer item in Online()) { bool num = hashSet.Contains(item.Name); bool flag = value > 0f && Vector3.Distance(item.Position, position) <= value; if (num || flag || value2) { bool isNew; RosterEntry rosterEntry = roster.Touch(item.Id, item.Name, out isNew); if (!rosterEntry.Cleared.Contains(key)) { rosterEntry.Cleared.Add(key); list.Add(item.Name); } } } roster.Save(); OathBound.Log.LogInfo((object)(stage.Name + " defeated. Credited: " + ((list.Count > 0) ? string.Join(", ", list) : "nobody new") + ".")); Publish(); } private static bool Counts(RosterEntry entry, HashSet<long> online) { if (entry.Ignored) { return false; } if (online.Contains(entry.Id)) { return true; } if (OathBoundConfiguration.CountOnlyOnline.Value) { return false; } int value = OathBoundConfiguration.InactiveDays.Value; if (value > 0) { return (DateTime.UtcNow - entry.LastSeenUtc).TotalDays <= (double)value; } return true; } public static List<string> WaitingFor(Stage stage) { Stage previous = Chain.Previous(stage); if (previous == null) { return new List<string>(); } Roster roster = EnsureRoster(); if (roster.Data.InstalledKeys.Contains(previous.Key)) { return new List<string>(); } HashSet<long> online = new HashSet<long>(from p in Online() select p.Id); return (from e in roster.Data.Players where Counts(e, online) && !e.Cleared.Contains(previous.Key) select e.Name).ToList(); } public static void Publish() { if (!IsServer || roster == null) { return; } StringBuilder stringBuilder = new StringBuilder(); foreach (Stage stage2 in Chain.Stages) { Stage stage = Chain.Previous(stage2); stringBuilder.Append(stage2.BossPrefab).Append('\t').Append(stage2.Name) .Append('\t') .Append(stage?.Name ?? "") .Append('\t') .Append(string.Join(", ", WaitingFor(stage2))) .Append('\n'); } ProgressState.Assign(stringBuilder.ToString()); } private static bool IsAdmin(long sender) { if (sender == ZNet.GetUID()) { return true; } ZNetPeer peer = ZNet.instance.GetPeer(sender); if (peer != null) { return ZNet.instance.IsAdmin(peer.m_socket.GetHostName()); } return false; } private static void Reply(long sender, string text) { ZRoutedRpc.instance.InvokeRoutedRPC(sender, "OathBound_Reply", new object[1] { text }); } private static void OnCommand(long sender, string line) { if (!IsServer) { return; } string[] array = line.Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries); string text = ((array.Length != 0) ? array[0].ToLowerInvariant() : "status"); if (text == "status") { Reply(sender, Status()); return; } if (!IsAdmin(sender)) { Reply(sender, "OathBound: only admins can change the roster."); return; } Roster roster = EnsureRoster(); switch (text) { case "grant": case "revoke": { if (array.Length < 3) { Reply(sender, "Usage: oathbound " + text + " <player> <stage>"); break; } RosterEntry rosterEntry2 = roster.Find(array[1]); Stage stage = Chain.Find(array[2]); if (rosterEntry2 == null || stage == null) { Reply(sender, (rosterEntry2 == null) ? ("No player '" + array[1] + "' in the roster.") : ("No stage '" + array[2] + "' in the chain.")); break; } if (text == "grant" && !rosterEntry2.Cleared.Contains(stage.Key)) { rosterEntry2.Cleared.Add(stage.Key); } if (text == "revoke") { rosterEntry2.Cleared.Remove(stage.Key); } roster.Save(); Publish(); Reply(sender, rosterEntry2.Name + ": " + stage.Name + " " + ((text == "grant") ? "granted" : "revoked") + "."); break; } case "ignore": case "unignore": case "forget": { if (array.Length < 2) { Reply(sender, "Usage: oathbound " + text + " <player>"); break; } RosterEntry rosterEntry = roster.Find(array[1]); if (rosterEntry == null) { Reply(sender, "No player '" + array[1] + "' in the roster."); break; } if (text == "forget") { roster.Data.Players.Remove(rosterEntry); } else { rosterEntry.Ignored = text == "ignore"; } roster.Save(); Publish(); Reply(sender, (text == "forget") ? (rosterEntry.Name + " removed from the roster.") : (rosterEntry.Name + " is " + (rosterEntry.Ignored ? "now ignored" : "counted again") + ".")); break; } default: Reply(sender, "Usage: oathbound status | grant <player> <stage> | revoke <player> <stage> | ignore <player> | unignore <player> | forget <player>"); break; } } private static string Status() { Roster roster = EnsureRoster(); HashSet<long> hashSet = new HashSet<long>(from p in Online() select p.Id); StringBuilder stringBuilder = new StringBuilder("OathBound status\n"); foreach (Stage stage in Chain.Stages) { List<string> list = WaitingFor(stage); stringBuilder.Append(" ").Append(stage.Name).Append(": ") .Append((list.Count == 0) ? "open" : ("waiting for " + string.Join(", ", list))) .Append('\n'); } stringBuilder.Append("Players\n"); foreach (RosterEntry entry in roster.Data.Players) { string value = (entry.Ignored ? "ignored" : (hashSet.Contains(entry.Id) ? "online" : (Counts(entry, hashSet) ? "counted" : "inactive"))); string text = string.Join(", ", from s in Chain.Stages where entry.Cleared.Contains(s.Key) select s.Name); stringBuilder.Append(" ").Append(entry.Name).Append(" (") .Append(entry.Id) .Append(") ") .Append(value) .Append(", last seen ") .Append(entry.LastSeen) .Append(", cleared: ") .Append((text.Length > 0) ? text : "none") .Append('\n'); } return stringBuilder.ToString().TrimEnd(new char[1] { '\n' }); } } public sealed class RosterEntry { public long Id { get; set; } public string Name { get; set; } = ""; public string LastSeen { get; set; } = ""; public bool Ignored { get; set; } public List<string> Cleared { get; set; } = new List<string>(); public DateTime LastSeenUtc { get { if (!DateTime.TryParseExact(LastSeen, "yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out var result)) { return DateTime.MinValue; } return result; } } } public sealed class RosterFile { public List<string> InstalledKeys { get; set; } = new List<string>(); public List<RosterEntry> Players { get; set; } = new List<RosterEntry>(); } public sealed class Roster { public const string TimeFormat = "yyyy-MM-dd HH:mm"; private string lastWrittenText = ""; private Timer poller; private DateTime lastWriteTime; private long lastLength = -1L; private int reloading; private static readonly ISerializer serializer = new SerializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance).Build(); private static readonly IDeserializer deserializer = new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance).IgnoreUnmatchedProperties().Build(); public string FilePath { get; } public RosterFile Data { get; private set; } = new RosterFile(); public static string Now => DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture); public event Action Changed; public Roster(string worldName) { string text = string.Join("_", worldName.Split(Path.GetInvalidFileNameChars())); FilePath = Path.Combine(Paths.ConfigPath, "OathBound." + text + ".roster.yml"); } public bool Load() { if (!File.Exists(FilePath)) { return false; } string text = File.ReadAllText(FilePath); Data = Parse(text) ?? new RosterFile(); lastWrittenText = text; return true; } public void Save() { File.WriteAllText(contents: lastWrittenText = Header() + serializer.Serialize(Data), path: FilePath); } private static string Header() { return "# OathBound roster. Written by the server, hot reloaded when edited.\n# ignored: true the player never holds the group back\n# cleared global keys of the bosses the player has been credited with\n# installedKeys bosses the world had already defeated when OathBound was installed\n# Console: oathbound status | grant | revoke | ignore | unignore | forget\n"; } private static RosterFile Parse(string text) { try { return deserializer.Deserialize<RosterFile>(text); } catch (Exception ex) { OathBound.Log.LogError((object)("Roster file could not be read, keeping the previous roster: " + ex.Message)); return null; } } public void Watch() { if (poller == null) { Snapshot(out lastWriteTime, out lastLength); poller = new Timer(delegate { Poll(); }, null, 5000, 5000); } } public void Dispose() { poller?.Dispose(); poller = null; } private void Poll() { try { Snapshot(out var writeTime, out var length); if (writeTime == lastWriteTime && length == lastLength) { return; } lastWriteTime = writeTime; lastLength = length; if (!File.Exists(FilePath)) { return; } string text = File.ReadAllText(FilePath); if (!(text == lastWrittenText) && Interlocked.CompareExchange(ref reloading, 1, 0) == 0) { ThreadingHelper.SynchronizingObject.BeginInvoke((Action)delegate { Reload(text); }, null); } } catch (IOException) { } catch (Exception ex2) { OathBound.Log.LogError((object)("Watching the roster file failed: " + ex2.Message)); } } private void Reload(string text) { try { RosterFile rosterFile = Parse(text); if (rosterFile != null) { Data = rosterFile; lastWrittenText = text; OathBound.Log.LogInfo((object)"Roster file was edited, reloaded it."); this.Changed?.Invoke(); } } finally { Interlocked.Exchange(ref reloading, 0); } } private void Snapshot(out DateTime writeTime, out long length) { FileInfo fileInfo = new FileInfo(FilePath); writeTime = (fileInfo.Exists ? fileInfo.LastWriteTimeUtc : DateTime.MinValue); length = (fileInfo.Exists ? fileInfo.Length : (-1)); } public RosterEntry Find(long id) { return Data.Players.FirstOrDefault((RosterEntry p) => p.Id == id); } public RosterEntry Find(string nameOrId) { if (long.TryParse(nameOrId, out var result)) { return Find(result); } return Data.Players.FirstOrDefault((RosterEntry p) => string.Equals(p.Name, nameOrId, StringComparison.OrdinalIgnoreCase)); } public RosterEntry Touch(long id, string name, out bool isNew) { RosterEntry rosterEntry = Find(id); isNew = rosterEntry == null; if (isNew) { rosterEntry = new RosterEntry { Id = id }; Data.Players.Add(rosterEntry); } if (!string.IsNullOrEmpty(name)) { rosterEntry.Name = name; } rosterEntry.LastSeen = Now; return rosterEntry; } } } namespace System.Runtime.CompilerServices { [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] internal sealed class IgnoresAccessChecksToAttribute : Attribute { public IgnoresAccessChecksToAttribute(string assemblyName) { } } } namespace SyncedConfig { public sealed class SyncedConfiguration { public ConfigSync Sync { get; } public ConfigFile Config { get; } public ManualLogSource Log { get; } public YamlConfigManager Yaml { get; } public YamlConfigEditor YamlEditor { get; } public IReadOnlyList<string> SearchPaths { get; } public IDisposable? ConfigWatcher { get; private set; } public bool IsLocked => Sync.IsLocked; public bool IsAdmin => Sync.IsAdmin; public bool IsSourceOfTruth => Sync.IsSourceOfTruth; public SyncedConfiguration(BaseUnityPlugin plugin, ManualLogSource log, string displayName, string version, string? minimumVersion = null, bool modRequired = true) { Config = plugin.Config; Log = log; string gUID = plugin.Info.Metadata.GUID; Sync = new ConfigSync(gUID) { DisplayName = displayName, CurrentVersion = version, MinimumRequiredVersion = (minimumVersion ?? version), ModRequired = modRequired }; SearchPaths = new string[2] { Path.GetDirectoryName(Config.ConfigFilePath) ?? Paths.ConfigPath, Path.GetDirectoryName(((object)plugin).GetType().Assembly.Location) ?? Paths.PluginPath }; Yaml = new YamlConfigManager(displayName, Log, SearchPaths, Sync); YamlEditor = new YamlConfigEditor(Yaml, displayName + " YAML Editor"); } public ConfigEntry<T> Bind<T>(string section, string key, T defaultValue, string description, bool synced = true, AcceptableValueBase? acceptableValues = null, params object[] tags) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Expected O, but got Unknown return Bind(section, key, defaultValue, new ConfigDescription(description, acceptableValues, tags), synced); } public ConfigEntry<T> Bind<T>(string section, string key, T defaultValue, ConfigDescription description, bool synced = true) { ConfigEntry<T> val = Config.Bind<T>(section, key, defaultValue, description); Sync.AddConfigEntry<T>(val).SynchronizedConfig = synced; return val; } public SyncedConfigEntry<T> Add<T>(ConfigEntry<T> entry, bool synced = true) { SyncedConfigEntry<T> syncedConfigEntry = Sync.AddConfigEntry<T>(entry); syncedConfigEntry.SynchronizedConfig = synced; return syncedConfigEntry; } public ConfigEntry<T> BindLocking<T>(string section, string key, T defaultValue, string description) where T : IConvertible { ConfigEntry<T> val = Config.Bind<T>(section, key, defaultValue, description); Sync.AddLockingConfigEntry<T>(val); return val; } public YamlConfigSource AddYaml(YamlConfigSource source) { return Yaml.Register(source); } public void Finish(Harmony harmony, int yamlApplyPriority = 400) { ConfigWatcher = ConfigReloader.Setup(Config, Log); if (Yaml.Sources.Count > 0) { Yaml.PatchValheimHooks(harmony, yamlApplyPriority); } } public static Func<byte[]?> EmbeddedResource(Assembly assembly, string resourceName) { return delegate { using Stream stream = assembly.GetManifestResourceStream(resourceName); if (stream != null) { using MemoryStream memoryStream = new MemoryStream(); stream.CopyTo(memoryStream); return memoryStream.ToArray(); } return (byte[]?)null; }; } } } namespace YamlConfig { public sealed class YamlConfigEditor { private const int WindowId = -69; private readonly YamlConfigManager manager; private readonly string windowTitle; private YamlConfigSource? active; private Dictionary<string, string> input = new Dictionary<string, string>(); private readonly Dictionary<string, bool> collapsed = new Dictionary<string, bool>(); private readonly Dictionary<string, Vector2> scroll = new Dictionary<string, Vector2>(); private Vector2 errorScroll; private bool hasErrors; public Func<string, string> Translate { get; set; } = (string s) => s; public bool IsOpen => active != null; public YamlConfigEditor(YamlConfigManager manager, string windowTitle) { this.manager = manager; this.windowTitle = windowTitle; } public void Open(YamlConfigSource source) { //IL_00a0: Unknown result type (might be due to invalid IL or missing references) active = source; input = source.Files.ToDictionary<KeyValuePair<string, string>, string, string>((KeyValuePair<string, string> kv) => kv.Key, (KeyValuePair<string, string> kv) => kv.Value); foreach (string key in input.Keys) { if (!collapsed.ContainsKey(key)) { collapsed[key] = collapsed.Count > 3; scroll[key] = Vector2.zero; } } } public void Close() { active = null; } public void DrawOpenButtons() { GUILayout.BeginVertical(Array.Empty<GUILayoutOption>()); foreach (YamlConfigSource source in manager.Sources) { if (source.Files.Count > 0 && GUILayout.Button(source.EditButtonName(), (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.ExpandWidth(true) })) { Open(source); } } GUILayout.EndVertical(); } public void Update() { if (active != null) { Cursor.lockState = (CursorLockMode)0; Cursor.visible = true; } } public void OnGUI() { //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Expected O, but got Unknown //IL_0049: Unknown result type (might be due to invalid IL or missing references) if (active != null) { Update(); GUILayout.Window(-69, new Rect(10f, 10f, (float)(Screen.width - 20), (float)(Screen.height - 20)), new WindowFunction(Draw), windowTitle, Array.Empty<GUILayoutOption>()); } } private void Draw(int id) { //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_04cc: Unknown result type (might be due to invalid IL or missing references) //IL_04e4: Unknown result type (might be due to invalid IL or missing references) //IL_04e9: Unknown result type (might be due to invalid IL or missing references) //IL_0505: Unknown result type (might be due to invalid IL or missing references) //IL_050c: Expected O, but got Unknown //IL_0522: Unknown result type (might be due to invalid IL or missing references) //IL_019c: Unknown result type (might be due to invalid IL or missing references) //IL_01a1: Unknown result type (might be due to invalid IL or missing references) //IL_01a3: Unknown result type (might be due to invalid IL or missing references) //IL_01a6: Unknown result type (might be due to invalid IL or missing references) //IL_01a8: Invalid comparison between Unknown and I4 //IL_01bf: Unknown result type (might be due to invalid IL or missing references) //IL_01c6: Invalid comparison between Unknown and I4 //IL_025f: Unknown result type (might be due to invalid IL or missing references) //IL_0264: Unknown result type (might be due to invalid IL or missing references) //IL_026b: Unknown result type (might be due to invalid IL or missing references) //IL_0272: Unknown result type (might be due to invalid IL or missing references) //IL_0279: Unknown result type (might be due to invalid IL or missing references) //IL_0295: Expected O, but got Unknown //IL_01db: Unknown result type (might be due to invalid IL or missing references) //IL_01e1: Invalid comparison between Unknown and I4 //IL_01f7: Unknown result type (might be due to invalid IL or missing references) //IL_01fe: Expected O, but got Unknown //IL_02dd: Unknown result type (might be due to invalid IL or missing references) //IL_02f1: Unknown result type (might be due to invalid IL or missing references) if (active == null) { return; } GUI.Box(new Rect(0f, 0f, (float)Screen.width, (float)Screen.height), (Texture)(object)Texture2D.blackTexture); GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); GUI.enabled = !hasErrors && (!manager.IsLocked || manager.IsSourceOfTruth); if (GUILayout.Button(Translate("Save and Apply"), Array.Empty<GUILayoutOption>()) && !hasErrors) { manager.SetLocalFiles(active, input, saveToDisk: true); GUILayout.EndHorizontal(); Close(); return; } if (manager.IsSourceOfTruth && GUILayout.Button(Translate("Apply without Saving"), Array.Empty<GUILayoutOption>()) && !hasErrors) { manager.SetLocalFiles(active, input, saveToDisk: false); GUILayout.EndHorizontal(); Close(); return; } GUI.enabled = true; if (GUILayout.Button(Translate("Discard and Close"), Array.Empty<GUILayoutOption>())) { GUILayout.EndHorizontal(); Close(); return; } GUILayout.EndHorizontal(); hasErrors = false; string text = ""; string text2 = ""; foreach (string item in input.Keys.ToList()) { string text3 = "yaml textarea for " + item; bool flag = GUI.GetNameOfFocusedControl() == text3 && Event.current.isKey; if (flag) { EventType type = Event.current.type; bool flag2 = type - 4 <= 1; flag = flag2; } if (flag && ((int)Event.current.keyCode == 9 || Event.current.character == '\t')) { if ((int)Event.current.type == 4) { TextEditor val = (TextEditor)GUIUtility.GetStateObject(typeof(TextEditor), GUIUtility.keyboardControl); val.Insert(' '); val.Insert(' '); input[item] = val.text; } Event.current.Use(); } if (input.Count > 1) { GUILayout.BeginVertical(GUI.skin.box, Array.Empty<GUILayoutOption>()); if (GUILayout.Button(Path.GetFileName(item), new GUIStyle(GUI.skin.label) { alignment = (TextAnchor)1, wordWrap = true, stretchWidth = true, fontSize = 15 }, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.ExpandWidth(true) })) { collapsed[item] = !collapsed[item]; } } else { collapsed[item] = false; } if (!collapsed[item]) { scroll[item] = GUILayout.BeginScrollView(scroll[item], (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.ExpandHeight(true) }); GUI.SetNextControlName(text3); input[item] = GUILayout.TextArea(input[item], (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true) }); GUILayout.EndScrollView(); } if (input.Count > 1) { GUILayout.EndVertical(); } try { YamlDocument yamlDocument = active.CreateDocument(); yamlDocument.Parse(YamlDocument.ParseYaml(input[item])); if (yamlDocument.configErrors.Count > 0) { hasErrors = true; text = text + "There are errors in " + Path.GetFileName(item) + ":\n" + string.Join("\n", yamlDocument.configErrors) + "\n"; } if (yamlDocument.configWarnings.Count > 0) { text2 = text2 + "Warnings for " + Path.GetFileName(item) + " (not critical, but results may be unexpected):\n" + string.Join("\n", yamlDocument.configWarnings) + "\n"; } } catch (YamlException ex) { hasErrors = true; text = text + "The syntax of " + Path.GetFileName(item) + " is invalid:\n" + ex.Message + ((ex.InnerException != null) ? (": " + ex.InnerException.Message) : "") + "\n"; } } GUILayout.BeginVertical(GUI.skin.box, Array.Empty<GUILayoutOption>()); errorScroll = GUILayout.BeginScrollView(errorScroll, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(100f) }); if (text != "") { GUIStyle val2 = new GUIStyle(GUI.skin.label); val2.normal.textColor = new Color(0.9f, 0.2f, 0.2f); GUILayout.Label(text, val2, Array.Empty<GUILayoutOption>()); } else { GUILayout.Label(Translate("Configuration syntax is valid."), Array.Empty<GUILayoutOption>()); } if (text2 != "") { GUILayout.Label(text2, Array.Empty<GUILayoutOption>()); } GUILayout.EndScrollView(); GUILayout.EndVertical(); } } public sealed class YamlConfigManager { private readonly string modName; private readonly ManualLogSource log; private readonly List<string> searchPaths; private readonly ConfigSync? configSync; private readonly List<YamlConfigSource> sources = new List<YamlConfigSource>(); private readonly List<FileSystemWatcher> watchers = new List<FileSystemWatcher>(); private bool loaded; private static readonly List<YamlConfigManager> hookedManagers = new List<YamlConfigManager>(); private static bool hooksInstalled; private static readonly FieldInfo? firstSpawnField = AccessTools.Field(typeof(Game), "m_firstSpawn"); public IReadOnlyList<YamlConfigSource> Sources => sources; public bool SkipSavingOfValueChange { get; set; } public Func<bool> CanApply { get; set; } = () => true; public bool IsSourceOfTruth => configSync?.IsSourceOfTruth ?? true; public bool IsLocked => configSync?.IsLocked ?? false; public event Action<YamlConfigSource>? Applied; public YamlConfigManager(string modName, ManualLogSource log, IEnumerable<string> searchPaths, ConfigSync? configSync) { this.modName = modName; this.log = log; this.searchPaths = searchPaths.ToList(); this.configSync = configSync; } public YamlConfigSource Register(YamlConfigSource source) { if (loaded) { throw new InvalidOperationException("Register sources before LoadAll"); } if (configSync != null) { source.FileData = new CustomSyncedValue<List<string>>(configSync, source.Identifier, new List<string>()); } sources.Add(source); return source; } public static Dictionary<string, string> PairsToDictionary(List<string> pairs) { Dictionary<string, string> dictionary = new Dictionary<string, string>(); for (int i = 0; i + 1 < pairs.Count; i += 2) { dictionary[pairs[i]] = pairs[i + 1]; } return dictionary; } public static List<string> DictionaryToPairs(IReadOnlyDictionary<string, string> files) { return files.SelectMany<KeyValuePair<string, string>, string>((KeyValuePair<string, string> kv) => new string[2] { kv.Key, kv.Value }).ToList(); } public void LoadAll() { if (loaded) { return; } loaded = true; foreach (YamlConfigSource source in sources) { try { Load(source); } catch (Exception arg) { log.LogError((object)$"Failed to initialize {source.FilePattern}: {arg}"); } } } public void ApplyAll() { foreach (YamlConfigSource source in sources) { if (source.Document != null) { ApplySource(source); } } } private void ApplySource(YamlConfigSource source) { try { source.Apply(source.Document); this.Applied?.Invoke(source); } catch (Exception arg) { log.LogError((object)$"Applying {source.FilePattern} failed: {arg}"); } } private List<string> FindFiles(YamlConfigSource source) { return (from p in searchPaths.Where(Directory.Exists).SelectMany((string path) => Directory.GetFiles(path, source.FilePattern)) orderby Path.GetFileName(p) != source.DefaultFileName select p).ThenBy(Path.GetFileName).ToList(); } public bool TryParse(YamlConfigSource source, IReadOnlyDictionary<string, string> files, out YamlDocument document, string contextDescription = "the") { document = source.CreateDocument(); bool result = true; foreach (KeyValuePair<string, string> file in files) { try { document.Parse(YamlDocument.ParseYaml(file.Value)); if (document.configWarnings.Count > 0) { log.LogWarning((object)(modName + ": warnings in " + contextDescription + " " + Path.GetFileName(file.Key) + " (" + file.Key + "):\n" + string.Join("\n", document.configWarnings))); document.configWarnings.Clear(); } if (document.configErrors.Count > 0) { log.LogError((object)(modName + ": errors in " + contextDescription + " " + Path.GetFileName(file.Key) + " (" + file.Key + "). Please review the syntax of your file:\n" + string.Join("\n", document.configErrors))); document.configErrors.Clear(); result = false; } } catch (YamlException ex) { log.LogError((object)(modName + ": " + file.Key + " is not valid YAML, parsing failed:\n" + ex.Message + ((ex.InnerException != null) ? (": " + ex.InnerException.Message) : ""))); result = false; } } return result; } private void Load(YamlConfigSource source) { List<string> list = FindFiles(source); if (list.Count > 0) { Dictionary<string, string> files = list.ToDictionary((string p) => p, File.ReadAllText); if (!TryParse(source, files, out YamlDocument document)) { log.LogWarning((object)(modName + ": parts of " + source.FilePattern + " were ignored because of the errors above.")); } source.Document = document; source.Files = files; if (source.FileData != null) { source.FileData.Value = DictionaryToPairs(files); } } else if (!WriteDefaultFile(source) && source.IsEnabled()) { log.LogWarning((object)(modName + ": " + source.FilePattern + " is enabled, but no such file was found in " + string.Join(" or ", searchPaths) + ".")); } if (source.FileData != null) { source.FileData.ValueChanged += delegate { OnSyncedValueChanged(source); }; } foreach (string item in searchPaths.Where(Directory.Exists)) { FileSystemWatcher fileSystemWatcher = new FileSystemWatcher(item, source.FilePattern); fileSystemWatcher.Changed += delegate(object _, FileSystemEventArgs args) { OnFileChanged(source, args); }; fileSystemWatcher.Created += delegate(object _, FileSystemEventArgs args) { OnFileChanged(source, args); }; fileSystemWatcher.Renamed += delegate(object _, RenamedEventArgs args) { OnFileChanged(source, args); }; fileSystemWatcher.IncludeSubdirectories = true; fileSystemWatcher.SynchronizingObject = ThreadingHelper.SynchronizingObject; fileSystemWatcher.EnableRaisingEvents = true; watchers.Add(fileSystemWatcher); } } private bool WriteDefaultFile(YamlConfigSource source) { if (source.DefaultFileContent == null || searchPaths.Count == 0) { return false; } try { string path = Path.Combine(searchPaths[0], source.DefaultFileName); if (!File.Exists(path)) { byte[] array = source.DefaultFileContent(); if (array != null) { File.WriteAllBytes(path, array); log.LogInfo((object)(modName + ": first run, wrote the example " + source.DefaultFileName + " to " + searchPaths[0])); return true; } } } catch (Exception ex) { log.LogWarning((object)(modName + ": could not write example " + source.DefaultFileName + ": " + ex.Message)); } return false; } private void OnSyncedValueChanged(YamlConfigSource source) { Dictionary<string, string> dictionary = PairsToDictionary(source.FileData.Value); if (!TryParse(source, dictionary, out YamlDocument document, "received")) { log.LogWarning((object)(modName + ": ignoring the changed " + source.FilePattern + " data, keeping the previous configuration. Contents follow for inspection.")); { foreach (KeyValuePair<string, string> item in dictionary) { log.LogWarning((object)("Contents of " + item.Key + ":")); log.LogInfo((object)item.Value); } return; } } source.Document = document; source.Files = dictionary; if (CanApply()) { ApplySource(source); } if (!SkipSavingOfValueChange && IsSourceOfTruth) { WriteBack(dictionary); } } private void WriteBack(Dictionary<string, string> files) { foreach (KeyValuePair<string, string> file in files) { try { string text = file.Key + ".temp"; File.WriteAllText(text, file.Value); if (File.Exists(file.Key)) { File.Replace(text, file.Key, null); } else { File.Move(text, file.Key); } } catch (IOException ex) { log.LogError((object)(modName + ": failed writing config back to " + file.Key + ": " + ex.Message)); } } } private void OnFileChanged(YamlConfigSource source, FileSystemEventArgs args) { if (!Path.GetFileName(args.Name).StartsWith(Regex.Replace(source.FilePattern, "\\*.*", ""), StringComparison.OrdinalIgnoreCase)) { return; } List<string> list = FindFiles(source); if (list.Count == 0) { return; } Dictionary<string, string> files; try { files = list.ToDictionary((string p) => p, File.ReadAllText); } catch (IOException) { return; } if (!TryParse(source, files, out YamlDocument _)) { log.LogWarning((object)(modName + ": ignoring the changed " + source.FilePattern + ", keeping the previously loaded configuration.")); return; } SetLocalFiles(source, files, saveToDisk: false); log.LogInfo((object)(modName + ": reloaded " + source.FilePattern + (IsSourceOfTruth ? "" : ", but did not apply it because the server's configuration is active."))); } public void SetLocalFiles(YamlConfigSource source, IReadOnlyDictionary<string, string> files, bool saveToDisk) { bool skipSavingOfValueChange = SkipSavingOfValueChange; SkipSavingOfValueChange = !saveToDisk; try { if (source.FileData != null) { source.FileData.AssignLocalValue(DictionaryToPairs(files)); } else { if (!TryParse(source, files, out YamlDocument document)) { return; } source.Document = document; source.Files = files; if (CanApply()) { ApplySource(source); } if (saveToDisk) { WriteBack(files.ToDictionary<KeyValuePair<string, string>, string, string>((KeyValuePair<string, string> kv) => kv.Key, (KeyValuePair<string, string> kv) => kv.Value)); } } } finally { SkipSavingOfValueChange = skipSavingOfValueChange; } } public void PatchValheimHooks(Harmony harmony, int applyPriority = 400) { //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Expected O, but got Unknown //IL_0077: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Expected O, but got Unknown //IL_00ad: Unknown result type (might be due to invalid IL or missing references) //IL_00b2: Unknown result type (might be due to invalid IL or missing references) //IL_00c1: Expected O, but got Unknown hookedManagers.Add(this); if (!hooksInstalled) { hooksInstalled = true; harmony.Patch((MethodBase)AccessTools.Method(typeof(FejdStartup), "Start", (Type[])null, (Type[])null), (HarmonyMethod)null, new HarmonyMethod(typeof(YamlConfigManager), "OnFejdStartup", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); harmony.Patch((MethodBase)AccessTools.Method(typeof(ZNet), "Awake", (Type[])null, (Type[])null), (HarmonyMethod)null, new HarmonyMethod(typeof(YamlConfigManager), "OnZNetAwake", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); harmony.Patch((MethodBase)AccessTools.Method(typeof(Game), "RequestRespawn", (Type[])null, (Type[])null), (HarmonyMethod)null, new HarmonyMethod(typeof(YamlConfigManager), "OnRequestRespawn", (Type[])null) { priority = applyPriority }, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } } private static void OnFejdStartup() { foreach (YamlConfigManager hookedManager in hookedManagers) { hookedManager.LoadAll(); } } private static void OnZNetAwake(ZNet __instance) { if (!__instance.IsDedicated() || !__instance.IsServer()) { return; } foreach (YamlConfigManager hookedManager in hookedManagers) { hookedManager.LoadAll(); } } private static void OnRequestRespawn(Game __instance) { object obj = firstSpawnField?.GetValue(__instance); if (obj is bool && !(bool)obj) { return; } foreach (YamlConfigManager hookedManager in hookedManagers) { hookedManager.ApplyAll(); } } } public sealed class YamlConfigSource { public string FilePattern { get; } public string Identifier { get; } public Func<YamlDocument> CreateDocument { get; } public Action<YamlDocument> Apply { get; } public Func<bool> IsEnabled { get; set; } = () => true; public Func<byte[]?>? DefaultFileContent { get; set; } public Func<string> EditButtonName { get; set; } public YamlDocument? Document { get; internal set; } public CustomSyncedValue<List<string>>? FileData { get; internal set; } public IReadOnlyDictionary<string, string> Files { get; internal set; } = new Dictionary<string, string>(); public string DefaultFileName => FilePattern.Replace("*", ""); public YamlConfigSource(string filePattern, string identifier, Func<YamlDocument> createDocument, Action<YamlDocument> apply) { FilePattern = filePattern; Identifier = identifier; CreateDocument = createDocument; Apply = apply; EditButtonName = () => "Edit " + filePattern.Replace("*", ""); } } public abstract class YamlDocument { public sealed class Section : IDisposable { private readonly Section? parent; private readonly Dictionary<string, object?> dict; public readonly string dictKey; private readonly HashSet<string> knownKeys = new HashSet<string>(StringComparer.CurrentCultureIgnoreCase); private readonly YamlDocument document; public string Path => ((parent == null || parent.dictKey == "") ? "" : (parent.Path + ".")) + dictKey; public IEnumerable<string> Keys => dict.Keys; public Section(Dictionary<string, object?> dict, string key, Section? parent, YamlDocument document) { this.dict = dict; dictKey = key; this.parent = parent; this.document = document; } public object? Get(string key) { knownKeys.Add(key); if (dict.TryGetValue(key, out object value) && value == null) { document.configErrors.Add(Path + "." + key + " cannot be empty"); } return value; } public bool Has(string key) { return dict.ContainsKey(key); } public IEnumerable<string> KeysWithPrefix(string prefix) { return dict.Keys.Where((string k) => k.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)); } public void Dispose() { string[] array = dict.Keys.Where((string key) => !knownKeys.Contains(key)).ToArray(); if (array.Length != 0) { document.configWarnings.Add(Path + " contains unknown keys " + string.Join(", ", array)); } } } public delegate bool ParserDelegate<T>(ref T target, Section sect, string key); public readonly List<string> configErrors = new List<string>(); public readonly List<string> configWarnings = new List<string>(); protected readonly Dictionary<string, List<string>> groups = new Dictionary<string, List<string>>(); private static readonly string[] falseWords = new string[12] { "0", "false", "off", "no", "nope", "nah", "-", "hell no", "pls dont", "lol no", "certainly not", "absolutely not" }; private static readonly string[] trueWords = new string[14] { "1", "true", "on", "yes", "yep", "yeah", "+", "hell yeah", "ok", "okay", "k", "yaaas", "certainly", "absolutely" }; public IReadOnlyDictionary<string, List<string>> Groups => groups; protected virtual int LevelCount => 1; protected virtual bool TryParseCustomScalar(Type type, string input, out object? output) { output = null; return false; } public static object? ParseYaml(string text) { return new DeserializerBuilder().Build().Deserialize<object>(text.TrimStart(new char[1] { '\ufeff' })); } public void Parse(object? configObj) { if (configObj is Dictionary<object, object> dict) { Dictionary<string, object> dictionary = castDictToStringDict(dict); foreach (KeyValuePair<string, object> item in dictionary) { if (string.Equals(item.Key, "groups", StringComparison.OrdinalIgnoreCase)) { ParseGroups(item.Value); continue; } Section section = this.section(new Section(dictionary, "", null, this), item.Key); if (section != null) { ParsePrimarySection(section); section.Dispose(); continue; } configErrors.Add("All top-level keys must be a mapping. Got unexpected " + (item.Value?.GetType().ToString() ?? "null") + " for key '" + item.Key + "'."); } } else if (configObj != null) { configErrors.Add("The file must be a mapping of names to settings, got " + configObj.GetType().Name); } Verify(); } private void ParseGroups(object? value) { if (!(value is Dictionary<object, object> dictionary)) { configErrors.Add("groups must contain a mapping from group to a list of group or member names"); return; } foreach (KeyValuePair<object, object> item2 in dictionary) { string text = item2.Key.ToString(); List<string> source; if (item2.Value is string item) { source = new List<string> { item }; } else { if (!(item2.Value is List<object> source2)) { configErrors.Add("groups." + text + " contains an invalid value (" + (item2.Value?.GetType().Name ?? "null") + ")"); continue; } source = (from s in source2 where s != null select s.ToString()).ToList(); } groups[text.ToLower()] = source.Select((string s) => s.ToLower()).ToList(); } } protected abstract void ParsePrimarySection(Section primarySection); protected virtual void Verify() { } protected void resolveGroupsForName(List<string> groupNames, string name) { foreach (KeyValuePair<string, List<string>> group in groups) { if (!groupNames.Contains(group.Key) && group.Value.Contains(name)) { groupNames.Add(group.Key); resolveGroupsForName(groupNames, group.Key); } } } public List<string> ResolveNames(IEnumerable<string> ownNames, params string[] trailingSpecialGroups) { List<string> list = ownNames.Select((string n) => n.ToLower()).Distinct().ToList(); string[] array = list.ToArray(); foreach (string name in array) { resolveGroupsForName(list, name); } list.AddRange(trailingSpecialGroups.Select((string g) => g.ToLower())); return list; } protected static void AssignConfig<T>(ref T? newConfig, T? inConfig) where T : class { if (newConfig == null) { newConfig = inConfig; } } protected static void AssignConfig<T>(ref T? newConfig, T? inConfig) where T : struct { T? val = newConfig; if (!val.HasValue) { newConfig = inConfig; } } protected static void AssignEnumConfig<T>(ref Dictionary<int, Dictionary<T, float[]?>>? newConfig, Dictionary<int, Dictionary<T, float[]?>>? inConfig) where T : notnull { if (inConfig == null) { return; } if (newConfig == null) { newConfig = inConfig.ToDictionary<KeyValuePair<int, Dictionary<T, float[]>>, int, Dictionary<T, float[]>>((KeyValuePair<int, Dictionary<T, float[]>> kv) => kv.Key, (KeyValuePair<int, Dictionary<T, float[]>> kv) => kv.Value.ToDictionary((KeyValuePair<T, float[]> k) => k.Key, (KeyValuePair<T, float[]> k) => k.Value)); } foreach (KeyValuePair<int, Dictionary<T, float[]>> item in inConfig) { if (!newConfig.ContainsKey(item.Key)) { newConfig[item.Key] = item.Value; continue; } foreach (KeyValuePair<T, float[]> item2 in item.Value) { if (!newConfig[item.Key].ContainsKey(item2.Key)) { newConfig[item.Key][item2.Key] = item2.Value; } } } } protected void fillLevels<T>(Dictionary<int, T>? config) { if (config == null) { return; } T value = default(T); int num = -1; for (int i = 0; i < LevelCount; i++) { if (config.TryGetValue(i, out T value2)) { value = value2; if (num == -1) { num = i; } } else if (num != -1) { config[i] = value; } } for (int j = 0; j < num; j++) { config[j] = config[num]; } } protected static Dictionary<string, object?> castDictToStringDict(Dictionary<object, object?> dict) { Dictionary<string, object> dictionary = new Dictionary<string, object>(StringComparer.CurrentCultureIgnoreCase); foreach (KeyValuePair<object, object> item in dict) { dictionary[item.Key.ToString()] = item.Value; } return dictionary; } protected Section? section(Section parent, string key) { object obj = parent.Get(key); if (obj is Dictionary<object, object> dict) { return new Section(castDictToStringDict(dict), key, parent, this); } if (obj != null) { configWarnings.Add($"Expected a section containing further configuration values, got {obj.GetType()} for {parent.Path}.{key}"); } return null; } private bool parseScalar(Type type, string input, out object? output) { if (type == typeof(int)) { if (int.TryParse(input, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) { output = result; return true; } } else if (type == typeof(float)) { if (float.TryParse(input, NumberStyles.Float, CultureInfo.InvariantCulture, out var result2)) { output = result2; return true; } } else if (type == typeof(bool)) { if (falseWords.Contains(input.ToLower())) { output = false; return true; } if (trueWords.Contains(input.ToLower())) { output = true; return true; } } else if (type.IsEnum) { try { output = Enum.Parse(type, input, ignoreCase: true); return true; } catch (ArgumentException) { } } else if (TryParseCustomScalar(type, input, out output)) { return true; } output = null; return false; } protected static bool TryParseBoolWord(string input, out bool value) { if (falseWords.Contains(input.ToLower())) { value = false; return true; } if (trueWords.Contains(input.ToLower())) { value = true; return true; } value = false; return false; } private bool parseScalar<T>(string input, out T output) where T : struct { if (parseScalar(typeof(T), input, out object output2) && output2 is T val) { output = val; return true; } output = default(T); return false; } protected bool ParseScalarFromObject<T>(ref T target, Section sect, string key) where T : struct { T? target2 = null; if (ParseScalarFromObject(ref target2, sect, key)) { target = target2.Value; return true; } return false; } protected bool ParseScalarFromObject<T>(ref T? target, Section sect, string key) where T : struct { object obj = sect.Get(key); if (obj == null) { return false; } if (obj is string input && parseScalar<T>(input, out var output)) { target = output; return true; } configErrors.Add(sect.Path + "." + key + " is not a " + typeof(T).Name + " (" + key + ")"); return false; } protected bool ParseStringFromObject(ref string? target, Section sect, string key) { object obj = sect.Get(key); if (obj is string text) { target = text; return true; } if (obj != null) { configErrors.Add(sect.Path + "." + key + " is not a text value"); } return false; } private bool parseStructFromDict<T>(Dictionary<object, object?> input, out T output, string path) where T : struct { output = default(T); if (typeof(T).IsPrimitive || typeof(T).IsEnum) { return false; } object obj = output; HashSet<string> hashSet = new HashSet<string>(); Dictionary<string, object> dictionary = castDictToStringDict(input); FieldInfo[] fields = typeof(T).GetFields(BindingFlags.Instance | BindingFlags.Public); foreach (FieldInfo fieldInfo in fields) { if (dictionary.TryGetValue(fieldInfo.Name, out var value)) { hashSet.Add(fieldInfo.Name); if (value is string input2 && parseScalar(fieldInfo.FieldType, input2, out object output2)) { fieldInfo.SetValue(obj, output2); continue; } configErrors.Add(path + "." + fieldInfo.Name + " is not " + fieldInfo.FieldType.Name); } } foreach (string key in dictionary.Keys) { if (!hashSet.Contains(key)) { configErrors.Add(path + " contains invalid key '" + key + "'. Allowed keys are " + string.Join(", ", from f in typeof(T).GetFields() select f.Name) + "."); } } output = (T)obj; return true; } protected float[]? ParseFloatList(string path, object? value) { if (!(value is List<object> list)) { return null; } float[] array = new float[list.Count]; for (int i = 0; i < list.Count; i++) { if (list[i] is string s && float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { array[i] = result; } else { configErrors.Add($"{path}[{i}] is not a number"); } } return array; } protected List<string>? ParseStringList(string path, object? value) { if (value is string item) { return new List<string> { item }; } if (!(value is List<object> source)) { return null; } return (from s in source where s != null select s.ToString()).ToList(); } private Dictionary<int, T> Repeat<T>(T value) { return Enumerable.Range(0, LevelCount).ToDictionary((int i) => i, (int _) => value); } protected bool ParseLevelledScalar<T>(ref Dictionary<int, T?>? target, Section sect, string key) where T : struct { string name = typeof(T).Name; object obj = sect.Get(key); if (obj == null) { return target != null; } T output; if (obj.ToString().ToLower() == "default") { target = Repeat<T?>(null); } else if (obj is string input && parseScalar<T>(input, out output)) { target = Repeat((T?)output); } else if (obj is Dictionary<object, object> dictionary) { target = new Dictionary<int, T?>(); bool flag = true; foreach (KeyValuePair<object, object> item in dictionary) { if (int.TryParse(item.Key.ToString(), out var result)) { T output2; T output3; if (item.Value?.ToString().ToLower() == "default") { target[result] = null; } else if (item.Value is string input2 && parseScalar<T>(input2, out output2)) { target[result] = output2; } else if (item.Value is Dictionary<object, object> input3 && parseStructFromDict<T>(input3, out output3, $"{sect.Path}.{key}.{result}")) { target[result] = output3; } else { configErrors.Add($"{sect.Path}.{key}.{result} is not 'default' nor {name}"); } } else { if (flag && !typeof(T).IsPrimitive && !typeof(T).IsEnum && parseStructFromDict<T>(dictionary, out var output4, sect.Path + "." + key)) { target = Repeat((T?)output4); break; } configErrors.Add($"{sect.Path}.{key} contains a non-integer key {item.Key}"); } flag = false; } } else { configErrors.Add(sect.Path + "." + key + " is not 'default', " + name + " nor a map"); } return target != null; } protected bool ParseLevelledFloatList(ref Dictionary<int, float[]?>? target, Section sect, string key) { return ParseLevelledFloatList(ref target, sect, key, null); } protected bool ParseLevelledFloatList(ref Dictionary<int, float[]?>? target, Section sect, string key, Func<string, float[]?>? interpretScalar = null) { object obj = sect.Get(key); if (obj == null) { return target != null; } string text = ((interpretScalar != null) ? "integer" : "number"); if (obj.ToString().ToLower() == "default") { target = Repeat<float[]>(null); } else { if (obj is string s) { float[] array = fromScalar(s); if (array != null) { target = Repeat(array); goto IL_023f; } } float[] array2 = ParseFloatList(sect.Path + "." + key, obj); if (array2 != null) { target = Repeat(array2); } else if (obj is Dictionary<object, object> dictionary) { target = new Dictionary<int, float[]>(); foreach (KeyValuePair<object, object> item in dictionary) { if (!int.TryParse(item.Key.ToString(), out var result)) { configErrors.Add($"{sect.Path}.{key} contains a non-integer key {item.Key}"); continue; } if (item.Value?.ToString().ToLower() == "default") { target[result] = null; continue; } if (item.Value is string s2) { float[] array3 = fromScalar(s2); if (array3 != null) { target[result] = array3; continue; } } float[] array4 = ParseFloatList($"{sect.Path}.{key}.{result}", item.Value); if (array4 != null) { target[result] = array4; continue; } configErrors.Add($"{sect.Path}.{key}.{result} is not 'default', {text} nor a list"); } } else { configErrors.Add(sect.Path + "." + key + " is not 'default', " + text + ", a list nor a map"); } } goto IL_023f; IL_023f: return target != null; float[]? fromScalar(string text2) { if (interpretScalar != null) { return interpretScalar(text2); } if (float.TryParse(text2, NumberStyles.Float, CultureInfo.InvariantCulture, out var result2)) { return new float[1] { result2 }; } return null; } } protected bool ParseLevelledEnumMap<T>(ref Dictionary<int, Dictionary<T, float[]?>>? target, Section sect, string key) where T : struct, IConvertible { return ParseLevelledEnumMap(ref target, sect, key, null); } protected bool ParseLevelledEnumMap<T>(ref Dictionary<int, Dictionary<T, float[]?>>? target, Section sect, string key, T? bareValueKey = null) where T : struct, IConvertible { List<T> allEnumValues = ((T[])Enum.GetValues(typeof(T))).Where((T t) => t.ToInt32(CultureInfo.InvariantCulture) != 0).ToList(); object obj = sect.Get(key); if (obj == null) { return target != null; } if (obj.ToString().ToLower() == "default") { target = Repeat(allDefault()); } else if (obj is string text) { T result2; if (float.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { target = Repeat(fromNumber(result)); } else if (!bareValueKey.HasValue && Enum.TryParse<T>(text, ignoreCase: true, out result2)) { target = Repeat(new Dictionary<T, float[]> { { result2, new float[1] { 100f } } }); } else { configErrors.Add(sect.Path + "." + key + " contains invalid value " + text + ", expecting default, a number, " + ((!bareValueKey.HasValue) ? (string.Join(", ", allEnumValues) + " or a map") : "a list or a map")); } } else { if (bareValueKey.HasValue) { T valueOrDefault = bareValueKey.GetValueOrDefault(); float[] array = ParseFloatList(sect.Path + "." + key, obj); if (array != null) { target = Repeat(new Dictionary<T, float[]> { { valueOrDefault, array } }); goto IL_03fa; } } if (obj is Dictionary<object, object> dictionary) { target = new Dictionary<int, Dictionary<T, float[]>>(); if (dictionary.Count > 0) { if (int.TryParse(dictionary.Keys.First().ToString(), out var _)) { foreach (KeyValuePair<object, object> item in dictionary) { float result5; if (!int.TryParse(item.Key.ToString(), out var result4)) { configErrors.Add(sect.Path + "." + key + " mixes level numbers and names as keys"); } else if (item.Value?.ToString().ToLower() == "default") { target[result4] = allDefault(); } else if (item.Value is string s && float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out result5)) { target[result4] = fromNumber(result5); } else if (item.Value is Dictionary<object, object> dict) { target[result4] = ParseEnumChances($"{sect.Path}.{key}[{result4}]", dict, allEnumValues); } else { configErrors.Add($"{sect.Path}.{key}[{result4}] is not a mapping from names to chances"); } } } else { Dictionary<T, float[]> value = ParseEnumChances(sect.Path + "." + key, dictionary, allEnumValues); for (int num = 0; num < LevelCount; num++) { target[num] = value; } } } } else { configErrors.Add(sect.Path + "." + key + " is not a number" + (bareValueKey.HasValue ? ", a list" : "") + ", 'default' nor a map"); } } goto IL_03fa; IL_03fa: return target != null; Dictionary<T, float[]?> allDefault() { return allEnumValues.ToDictionary((T k) => k, (T _) => (float[])null); } Dictionary<T, float[]?> fromNumber(float probability) { if (!bareValueKey.HasValue) { return allEnumValues.ToDictionary((T k) => k, (T _) => new float[1] { probability }); } T valueOrDefault2 = bareValueKey.GetValueOrDefault(); return new Dictionary<T, float[]> { { valueOrDefault2, new float[1] { probability } } }; } } private Dictionary<T, float[]?> ParseEnumChances<T>(string path, Dictionary<object, object?> dict, List<T> allEnumValues) where T : struct, IConvertible { Dictionary<T, float[]> dictionary = new Dictionary<T, float[]>(); bool flag = false; float[] array = null; foreach (KeyValuePair<object, object> item in dict) { string text = item.Key.ToString(); bool flag2 = text.ToLower() == "other"; T result = default(T); if (!flag2 && !Enum.TryParse<T>(text, ignoreCase: true, out result)) { configErrors.Add(path + " contains unexpected key " + text + ", expecting either other, " + string.Join(", ", allEnumValues)); continue; } float[] array2; float result2; if (item.Value?.ToString().ToLower() == "default") { array2 = null; } else if (item.Value is string s && float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out result2)) { array2 = new float[1] { result2 }; } else { float[] array3 = ParseFloatList(path + "." + text, item.Value); if (array3 == null) { configErrors.Add(string.Format("{0}.{1} (value '{2}') is not a number or list of numbers", path, text, (item.Value is string) ? item.Value : (item.Value?.GetType().Name ?? "null"))); continue; } array2 = array3; } if (flag2) { flag = true; array = array2; } else { dictionary[result] = array2; } } if (flag) { int undefined = allEnumValues.Count - dictionary.Count; if (array != null && undefined > 0) { array = array.Select((float p) => p / (float)undefined).ToArray(); } foreach (T allEnumValue in allEnumValues) { if (!dictionary.ContainsKey(allEnumValue)) { dictionary[allEnumValue] = array; } } } return dictionary; } } } namespace ConfigReload { internal static class ConfigReloader { private sealed class Poller : IDisposable { private const int IntervalMilliseconds = 5000; private readonly ConfigFile config; private readonly ManualLogSource? log; private readonly string fileName; private readonly Timer timer; private DateTime lastWriteTime; private long lastLength; private string lastContent; private int reloading; public Poller(ConfigFile config, ManualLogSource? log) { this.config = config; this.log = log; fileName = Path.GetFileName(config.ConfigFilePath); Snapshot(out lastWriteTime, out lastLength); lastContent = ReadOrNull(config.ConfigFilePath) ?? ""; timer = new Timer(delegate { Poll(); }, null, 5000, 5000); } private void Poll() { try { Snapshot(out var writeTime, out var length); if (writeTime == lastWriteTime && length == lastLength) { return; } lastWriteTime = writeTime; lastLength = length; string text = ReadOrNull(config.ConfigFilePath); if (text != null && !(text == lastContent)) { lastContent = text; if (Interlocked.CompareExchange(ref reloading, 1, 0) == 0) { ThreadingHelper.SynchronizingObject.BeginInvoke(new Action(Reload), null); } } } catch (Exception ex) { ManualLogSource? obj = log; if (obj != null) { obj.LogError((object)("Watching " + fileName + " failed: " + ex.Message)); } } } private void Reload() { try { ManualLogSource? obj = log; if (obj != null) { obj.LogInfo((object)(fileName + " changed on disk, reloading")); } config.Reload(); } catch (Exception ex) { ManualLogSource? obj2 = log; if (obj2 != null) { obj2.LogError((object)("Failed to reload " + fileName + ", please check it for typos and formatting: " + ex.Message)); } } finally { Interlocked.Exchange(ref reloading, 0); } } private void Snapshot(out DateTime writeTime, out long length) { FileInfo fileInfo = new FileInfo(config.ConfigFilePath); if (fileInfo.Exists) { writeTime = fileInfo.LastWriteTimeUtc; length = fileInfo.Length; } else { writeTime = DateTime.MinValue; length = -1L; } } private static string? ReadOrNull(string path) { try { return File.Exists(path) ? File.ReadAllText(path) : null; } catch (IOException) { return null; } } public void Dispose() { timer.Dispose(); } } public static IDisposable Setup(ConfigFile config, ManualLogSource? log = null, bool saveNow = true) { if (saveNow) { config.Save(); } return new Poller(config, log); } } } namespace ServerSync { [PublicAPI] public abstract class OwnConfigEntryBase { public object? LocalBaseValue; public bool SynchronizedConfig = true; public abstract ConfigEntryBase BaseConfig { get; } } [PublicAPI] public class SyncedConfigEntry<T>(ConfigEntry<T> sourceConfig) : OwnConfigEntryBase() { public readonly ConfigEntry<T> SourceConfig = sourceConfig; public override ConfigEntryBase BaseConfig => (ConfigEntryBase)(object)SourceConfig; public T Value { get { return SourceConfig.Value; } set { SourceConfig.Value = value; } } public void AssignLocalValue(T value) { if (LocalBaseValue == null) { Value = value; } else { LocalBaseValue = value; } } } public abstract class CustomSyncedValueBase { public object? LocalBaseValue; public readonly string Identifier; public readonly Type Type; private object? boxedValue; protected bool localIsOwner; public readonly int Priority; public object? BoxedValue { get { return boxedValue; } set { boxedValue = value; this.ValueChanged?.Invoke(); } } public event Action? ValueChanged; protected CustomSyncedValueBase(ConfigSync configSync, string identifier, Type type, int priority) { Priority = priority; Identifier = identifier; Type = type; configSync.AddCustomValue(this); localIsOwner = configSync.IsSourceOfTruth; configSync.SourceOfTruthChanged += delegate(bool truth) { localIsOwner = truth; }; } } [PublicAPI] public sealed class CustomSyncedValue<T> : CustomSyncedValueBase { public T Value { get { return (T)base.BoxedValue; } set { base.BoxedValue = value; } } public CustomSyncedValue(ConfigSync configSync, string identifier, T value = default(T), int priority = 0) : base(configSync, identifier, typeof(T), priority) { Value = value; } public void AssignLocalValue(T value) { if (localIsOwner) { Value = value; } else { LocalBaseValue = value; } } } internal class ConfigurationManagerAttributes { [UsedImplicitly] public bool? ReadOnly = false; } [PublicAPI] public class ConfigSync { [HarmonyPatch(typeof(ZRpc), "HandlePackage")] private static class SnatchCurrentlyHandlingRPC { public static ZRpc? currentRpc; [HarmonyPrefix] private static void Prefix(ZRpc __instance) { currentRpc = __instance; } } [HarmonyPatch(typeof(ZNet), "Awake")] internal static class RegisterRPCPatch { [HarmonyPostfix] private static void Postfix(ZNet __instance) { isServer = __instance.IsServer(); foreach (ConfigSync configSync2 in configSyncs) { ZRoutedRpc.instance.Register<ZPackage>(configSync2.Name + " ConfigSync", (Action<long, ZPackage>)configSync2.RPC_FromOtherClientConfigSync); if (isServer) { configSync2.InitialSyncDone = true; Debug.Log((object)("Registered '" + configSync2.Name + " ConfigSync' RPC - waiting for incoming connections")); } } if (isServer) { ((MonoBehaviour)__instance).StartCoroutine(WatchAdminListChanges()); } static void SendAdmin(List<ZNetPeer> peers, bool isAdmin) { ZPackage package = ConfigsToPackage(null, null, new PackageEntry[1] { new PackageEntry { section = "Internal", key = "lockexempt", type = typeof(bool), value = isAdmin } }); ConfigSync configSync = configSyncs.First(); if (configSync != null) { ((MonoBehaviour)ZNet.instance).StartCoroutine(configSync.sendZPackage(peers, package)); } } static IEnumerator WatchAdminListChanges() { MethodInfo listContainsId = AccessTools.DeclaredMethod(typeof(ZNet), "ListContainsId", (Type[])null, (Type[])null); SyncedList adminList = (SyncedList)AccessTools.DeclaredField(typeof(ZNet), "m_adminList").GetValue(ZNet.instance); List<string> CurrentList = new List<string>(adminList.GetList()); while (true) { yield return (object)new WaitForSeconds(30f); if (!adminList.GetList().SequenceEqual(CurrentList)) { CurrentList = new List<string>(adminList.GetList()); List<ZNetPeer> list = ZNet.instance.GetPeers().Where(delegate(ZNetPeer p) { string hostName = p.m_rpc.GetSocket().GetHostName(); return ((object)listContainsId != null) ? ((bool)listContainsId.Invoke(ZNet.instance, new object[2] { adminList, hostName })) : adminList.Contains(hostName); }).ToList(); SendAdmin(ZNet.instance.GetPeers().Except(list).ToList(), isAdmin: false); SendAdmin(list, isAdmin: true); } } } } } [HarmonyPatch(typeof(ZNet), "OnNewConnection")] private static class RegisterClientRPCPatch { [HarmonyPostfix] private static void Postfix(ZNet __instance, ZNetPeer peer) { if (__instance.IsServer()) { return; } foreach (ConfigSync configSync in configSyncs) { peer.m_rpc.Register<ZPackage>(configSync.Name + " ConfigSync", (Action<ZRpc, ZPackage>)configSync.RPC_FromServerConfigSync); } } } private class ParsedConfigs { public readonly Dictionary<OwnConfigEntryBase, object?> configValues = new Dictionary<OwnConfigEntryBase, object>(); public readonly Dictionary<CustomSyncedValueBase, object?> customValues = new Dictionary<CustomSyncedValueBase, object>(); } [HarmonyPatch(typeof(ZNet), "Shutdown")] private class ResetConfigsOnShutdown { [HarmonyPostfix] private static void Postfix() { ProcessingServerUpdate = true; foreach (ConfigSync configSync in configSyncs) { configSync.resetConfigsFromServer(); configSync.IsSourceOfTruth = true; configSync.InitialSyncDone = false; } ProcessingServerUpdate = false; } } [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] private class SendConfigsAfterLogin { private class BufferingSocket : ZPlayFabSocket, ISocket { public volatile bool finished; public volatile int versionMatchQueued = -1; public readonly List<ZPackage> Package = new List<ZPackage>(); public readonly ISocket Original; public BufferingSocket(ISocket original) { Original = original; ((ZPlayFabSocket)this)..ctor(); } public bool IsConnected() { return Original.IsConnected(); } public ZPackage Recv() { return Original.Recv(); } public int GetSendQueueSize() { return Original.GetSendQueueSize(); } public int GetCurrentSendRate() { return Original.GetCurrentSendRate(); } public bool IsHost() { return Original.IsHost(); } public void Dispose() { Original.Dispose(); } public bool GotNewData() { return Original.GotNewData(); } public void Close() { Original.Close(); } public string GetEndPointString() { return Original.GetEndPointString(); } public void GetAndResetStats(out int totalSent, out int totalRecv) { Original.GetAndResetStats(ref totalSent, ref totalRecv); } public void GetConnectionQuality(out float localQuality, out float remoteQuality, out int ping, out float outByteSec, out float inByteSec) { Original.GetConnectionQuality(ref localQuality, ref remoteQuality, ref ping, ref outByteSec, ref inByteSec); } public ISocket Accept() { return Original.Accept(); } public int GetHostPort() { return Original.GetHostPort(); } public bool Flush() { return Original.Flush(); } public string GetHostName() { return Original.GetHostName(); } public void VersionMatch() { if (finished) { Original.VersionMatch(); } else { versionMatchQueued = Package.Count; } } public void Send(ZPackage pkg) { //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Expected O, but got Unknown int pos = pkg.GetPos(); pkg.SetPos(0); int num = pkg.ReadInt(); if ((num == StringExtensionMethods.GetStableHashCode("PeerInfo") || num == StringExtensionMethods.GetStableHashCode("RoutedRPC") || num == StringExtensionMethods.GetStableHashCode("ZDOData")) && !finished) { ZPackage val = new ZPackage(pkg.GetArray()); val.SetPos(pos); Package.Add(val); } else { pkg.SetPos(pos); Original.Send(pkg); } } } [HarmonyPriority(800)] [HarmonyPrefix] private static void Prefix(ref Dictionary<Assembly, BufferingSocket>? __state, ZNet __instance, ZRpc rpc) { //IL_0073: Unknown result type (might be due to invalid IL or missing references) if (!__instance.IsServer()) { return; } BufferingSocket bufferingSocket = new BufferingSocket(rpc.GetSocket()); AccessTools.DeclaredField(typeof(ZRpc), "m_socket").SetValue(rpc, bufferingSocket); object? obj = AccessTools.DeclaredMethod(typeof(ZNet), "GetPeer", new Type[1] { typeof(ZRpc) }, (Type[])null).Invoke(__instance, new object[1] { rpc }); ZNetPeer val = (ZNetPeer)((obj is ZNetPeer) ? obj : null); if (val != null && (int)ZNet.m_onlineBackend != 0) { FieldInfo fieldInfo = AccessTools.DeclaredField(typeof(ZNetPeer), "m_socket"); object? value = fieldInfo.GetValue(val); ZPlayFabSocket val2 = (ZPlayFabSocket)((value is ZPlayFabSocket) ? value : null); if (val2 != null) { typeof(ZPlayFabSocket).GetField("m_remotePlayerId").SetValue(bufferingSocket, val2.m_remotePlayerId); } fieldInfo.SetValue(val, bufferingSocket); } if (__state == null) { __state = new Dictionary<Assembly, BufferingSocket>(); } __state[Assembly.GetExecutingAssembly()] = bufferingSocket; } [HarmonyPostfix] private static void Postfix(Dictionary<Assembly, BufferingSocket> __state, ZNet __instance, ZRpc rpc) { ZNetPeer peer; if (__instance.IsServer()) { object obj = AccessTools.DeclaredMethod(typeof(ZNet), "GetPeer", new Type[1] { typeof(ZRpc) }, (Type[])null).Invoke(__instance, new object[1] { rpc }); peer = (ZNetPeer)((obj is ZNetPeer) ? obj : null); if (peer == null) { SendBufferedData(); } else { ((MonoBehaviour)__instance).StartCoroutine(sendAsync()); } } void SendBufferedData() { if (rpc.GetSocket() is BufferingSocket bufferingSocket) { AccessTools.DeclaredField(typeof(ZRpc), "m_socket").SetValue(rpc, bufferingSocket.Original); object? obj2 = AccessTools.DeclaredMethod(typeof(ZNet), "GetPeer", new Type[1] { typeof(ZRpc) }, (Type[])null).Invoke(__instance, new object[1] { rpc }); ZNetPeer val = (ZNetPeer)((obj2 is ZNetPeer) ? obj2 : null); if (val != null) { AccessTools.DeclaredField(typeof(ZNetPeer), "m_socket").SetValue(val, bufferingSocket.Original); } } BufferingSocket bufferingSocket2 = __state[Assembly.GetExecutingAssembly()]; bufferingSocket2.finished = true; for (int i = 0; i < bufferingSocket2.Package.Count; i++) { if (i == bufferingSocket2.versionMatchQueued) { bufferingSocket2.Original.VersionMatch(); } bufferingSocket2.Original.Send(bufferingSocket2.Package[i]); } if (bufferingSocket2.Package.Count == bufferingSocket2.versionMatchQueued) { bufferingSocket2.Original.VersionMatch(); } } IEnumerator sendAsync() { foreach (ConfigSync configSync in configSyncs) { List<PackageEntry> list = new List<PackageEntry>(); if (configSync.CurrentVersion != null) { list.Add(new PackageEntry { section = "Internal", key = "serverversion", type = typeof(string), value = configSync.CurrentVersion }); } MethodInfo methodInfo = AccessTools.DeclaredMethod(typeof(ZNet), "ListContainsId", (Type[])null, (Type[])null); SyncedList val = (SyncedList)AccessTools.DeclaredField(typeof(ZNet), "m_adminList").GetValue(ZNet.instance); list.Add(new PackageEntry { section = "Internal", key = "lockexempt", type = typeof(bool), value = (((object)methodInfo == null) ? ((object)val.Contains(rpc.GetSocket().GetHostName())) : methodInfo.Invoke(ZNet.instance, new object[2] { val, rpc.GetSocket().GetHostName() })) }); ZPackage package = ConfigsToPackage(configSync.allConfigs.Select((OwnConfigEntryBase c) => c.BaseConfig), configSync.allCustomValues, list, partial: false); yield return ((MonoBehaviour)__instance).StartCoroutine(configSync.sendZPackage(new List<ZNetPeer> { peer }, package)); } SendBufferedData(); } } } private class PackageEntry { public string section; public string key; public Type type; public object? value; } [HarmonyPatch(typeof(ConfigEntryBase), "GetSerializedValue")] private static class PreventSavingServerInfo { [HarmonyPrefix] private static bool Prefix(ConfigEntryBase __instance, ref string __result) { OwnConfigEntryBase ownConfigEntryBase = configData(__instance); if (ownConfigEntryBase == null || isWritableConfig(ownConfigEntryBase)) { return true; } __result = TomlTypeConverter.ConvertToString(ownConfigEntryBase.LocalBaseValue, __instance.SettingType); return false; } } [HarmonyPatch(typeof(ConfigEntryBase), "SetSer