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 Lockstep v0.3.0
plugins\Lockstep.dll
Decompiled 7 hours 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 Charter; using ConfigReload; using HarmonyLib; using Microsoft.CodeAnalysis; using PatchGuard; using SyncedConfig; 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("Lockstep")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyDescription("Lockstep: boss progression gated on the whole group")] [assembly: AssemblyFileVersion("0.3.0.0")] [assembly: AssemblyInformationalVersion("0.3.0+6bfa7e58761853b59c3962d1144cef1b935b57e4")] [assembly: AssemblyProduct("Lockstep")] [assembly: AssemblyTitle("Lockstep")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.3.0.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 Lockstep { 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(); Lockstep.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 : YamlModel { private readonly List<Stage> stages = new List<Stage>(); protected override void Read(YamlNode root) { foreach (KeyValuePair<string, YamlNode> entry in root.Entries) { ReadStage(entry.Key, entry.Value); } } private void ReadStage(string name, YamlNode node) { node.Get("order").TryInt(out var result); if (!node.Get("key").TryString(out string result2) || string.IsNullOrEmpty(result2)) { node.Error("missing 'key', the global key the game sets when the boss dies"); } if (!node.Get("boss").TryString(out string result3) || string.IsNullOrEmpty(result3)) { node.Error("missing 'boss', the prefab name of the boss the altar spawns"); } stages.Add(new Stage { Name = name, Key = result2, BossPrefab = result3, Order = result }); } 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) { base.Warnings.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) { base.Errors.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 (!LockstepConfiguration.SpawnGuard.Value || !AltarGate.IsClosed(__instance, null)) { return true; } Lockstep.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); } } Lockstep.Log.LogInfo((object)(__instance.m_name + " died, attackers: " + ((list.Count > 0) ? string.Join(", ", list) : "none recorded") + ".")); ZRoutedRpc.instance.InvokeRoutedRPC("Lockstep_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 Article<string> article; 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) { article = new Article<string>(config.Sync, "lockstep_state", "", standing: true); article.Changed += delegate { Guard.Run("progress state changed", Parse); }; } public static void Assign(string text) { if (article.Value != text) { article.Assign(text); } } private static void Parse() { byBoss.Clear(); string[] array = (article.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 = (LockstepConfiguration.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 "Lockstep: no progression state received from the server yet."; } return "Lockstep 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("lockstep 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 = "lockstep 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("lockstep 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("lockstep", "lockstep 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("Lockstep: 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("Lockstep_Command", new object[1] { text }); } public static void Print(string text) { if ((Object)(object)Console.instance == (Object)null) { Lockstep.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()) { Lockstep.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.Lockstep", "Lockstep", "0.3.0")] public class Lockstep : BaseUnityPlugin { public const string PluginGuid = "com.Lockstep"; public const string PluginName = "Lockstep"; public const string PluginVersion = "0.3.0"; 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, "Lockstep", "0.3.0"); LockstepConfiguration.Initialize(Synced); ProgressState.Initialize(Synced); Chain.Changed += ProgressServer.Publish; Harmony val = new Harmony("com.Lockstep"); val.PatchAll(Assembly.GetExecutingAssembly()); Synced.Finish(val); Guard.Install(val, ((BaseUnityPlugin)this).Logger, Assembly.GetExecutingAssembly()); Log.LogInfo((object)"Lockstep 0.3.0 loaded."); } } public enum LateJoinerMode { Catchup, Earn } public static class LockstepConfiguration { 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 YamlFileSet ChainSource { get; private set; } public static void Initialize(SyncedConfiguration config) { BindGeneral(config); BindCredit(config); BindGroup(config); BindGate(config); AddChain(config); RepublishOnGroupChange(config); } private static void BindGeneral(SyncedConfiguration config) { LockConfiguration = config.BindLocking("General", "Lock Configuration", defaultValue: true, "Server only. When on, every player uses the server's values for this file and cannot override them locally."); } private static void BindCredit(SyncedConfiguration config) { 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); } private static void BindGroup(SyncedConfiguration config) { 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); } private static void BindGate(SyncedConfiguration config) { 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); } private static void AddChain(SyncedConfiguration config) { ChainSource = config.AddYaml(new YamlFileSet("LockstepChain*.yml", "lockstepchain", () => new ChainDocument(), delegate(YamlModel model) { Chain.Set(((ChainDocument)model).Process()); }) { DefaultContent = SyncedConfiguration.EmbeddedResource(Assembly.GetExecutingAssembly(), "Lockstep.LockstepChain.yml"), EditorLabel = () => "Edit progression chain" }); } private static void RepublishOnGroupChange(SyncedConfiguration config) { 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 { internal struct OnlinePlayer { public long Id; public string Name; public Vector3 Position; } public const string RpcBossDefeated = "Lockstep_BossDefeated"; public const string RpcCommand = "Lockstep_Command"; public const string RpcReply = "Lockstep_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>("Lockstep_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>("Lockstep_Command", (Action<long, string>)delegate(long sender, string line) { Guard.Run("command", delegate { ServerCommands.OnCommand(sender, line); }); }); ZRoutedRpc.instance.Register<string>("Lockstep_Reply", (Action<long, string>)delegate(long sender, string text) { Guard.Run("reply", delegate { Commands.Print(text); }); }); } public static void Shutdown() { roster?.Dispose(); roster = null; } internal 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(); Lockstep.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 && LockstepConfiguration.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) { Lockstep.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(); } } internal 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_0035: Unknown result type (might be due to invalid IL or missing references) if (IsServer) { Stage stage = Chain.ByKey(key); if (stage == null) { Lockstep.Log.LogInfo((object)("A boss with key " + key + " died; it is not in the chain.")); return; } Roster obj = EnsureRoster(); List<string> list = CreditPlayers(obj, key, attackers, position); obj.Save(); Lockstep.Log.LogInfo((object)(stage.Name + " defeated. Credited: " + ((list.Count > 0) ? string.Join(", ", list) : "nobody new") + ".")); Publish(); } } private static List<string> CreditPlayers(Roster r, string key, string attackers, Vector3 position) { //IL_005c: Unknown result type (might be due to invalid IL or missing references) HashSet<string> attackerNames = new HashSet<string>(from n in attackers.Split(new char[1] { '\n' }) where n.Length > 0 select n, StringComparer.Ordinal); List<string> list = new List<string>(); foreach (OnlinePlayer item in Online()) { if (EarnedCredit(item, attackerNames, position)) { bool isNew; RosterEntry rosterEntry = r.Touch(item.Id, item.Name, out isNew); if (!rosterEntry.Cleared.Contains(key)) { rosterEntry.Cleared.Add(key); list.Add(item.Name); } } } return list; } private static bool EarnedCredit(OnlinePlayer player, HashSet<string> attackerNames, Vector3 position) { //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) if (LockstepConfiguration.CreditEveryoneOnline.Value || attackerNames.Contains(player.Name)) { return true; } float value = LockstepConfiguration.CreditRadius.Value; if (value > 0f) { return Vector3.Distance(player.Position, position) <= value; } return false; } internal static bool Counts(RosterEntry entry, HashSet<long> online) { if (entry.Ignored) { return false; } if (online.Contains(entry.Id)) { return true; } if (LockstepConfiguration.CountOnlyOnline.Value) { return false; } int value = LockstepConfiguration.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()); } } 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, "Lockstep." + 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 "# Lockstep 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 Lockstep was installed\n# Console: lockstep status | grant | revoke | ignore | unignore | forget\n"; } private static RosterFile Parse(string text) { try { return deserializer.Deserialize<RosterFile>(text); } catch (Exception ex) { Lockstep.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 { PollOnce(); } catch (IOException) { } catch (Exception ex2) { Lockstep.Log.LogError((object)("Watching the roster file failed: " + ex2.Message)); } } private void PollOnce() { 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); } } private void Reload(string text) { try { RosterFile rosterFile = Parse(text); if (rosterFile != null) { Data = rosterFile; lastWrittenText = text; Lockstep.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; } } public static class ServerCommands { private const string Usage = "Usage: lockstep status | grant <player> <stage> | revoke <player> <stage> | ignore <player> | unignore <player> | forget <player>"; 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, "Lockstep_Reply", new object[1] { text }); } public static void OnCommand(long sender, string line) { if (ProgressServer.IsServer) { string[] array = line.Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries); string text = ((array.Length != 0) ? array[0].ToLowerInvariant() : "status"); if (text == "status") { Reply(sender, Status()); } else if (!IsAdmin(sender)) { Reply(sender, "Lockstep: only admins can change the roster."); } else { Reply(sender, RunAdminVerb(text, array)); } } } private static string RunAdminVerb(string verb, string[] args) { return verb switch { "grant" => Grant(args), "revoke" => Revoke(args), "ignore" => Ignore(args), "unignore" => Unignore(args), "forget" => Forget(args), _ => "Usage: lockstep status | grant <player> <stage> | revoke <player> <stage> | ignore <player> | unignore <player> | forget <player>", }; } private static string Grant(string[] args) { RosterEntry entry; Stage stage; string text = FindPlayerAndStage("grant", args, out entry, out stage); if (text != null) { return text; } if (!entry.Cleared.Contains(stage.Key)) { entry.Cleared.Add(stage.Key); } SaveAndPublish(); return entry.Name + ": " + stage.Name + " granted."; } private static string Revoke(string[] args) { RosterEntry entry; Stage stage; string text = FindPlayerAndStage("revoke", args, out entry, out stage); if (text != null) { return text; } entry.Cleared.Remove(stage.Key); SaveAndPublish(); return entry.Name + ": " + stage.Name + " revoked."; } private static string Ignore(string[] args) { return SetIgnored("ignore", args, ignored: true); } private static string Unignore(string[] args) { return SetIgnored("unignore", args, ignored: false); } private static string SetIgnored(string verb, string[] args, bool ignored) { RosterEntry entry; string text = FindPlayer(verb, args, out entry); if (text != null) { return text; } entry.Ignored = ignored; SaveAndPublish(); return entry.Name + " is " + (entry.Ignored ? "now ignored" : "counted again") + "."; } private static string Forget(string[] args) { RosterEntry entry; string text = FindPlayer("forget", args, out entry); if (text != null) { return text; } ProgressServer.EnsureRoster().Data.Players.Remove(entry); SaveAndPublish(); return entry.Name + " removed from the roster."; } private static string FindPlayer(string verb, string[] args, out RosterEntry entry) { entry = null; if (args.Length < 2) { return "Usage: lockstep " + verb + " <player>"; } entry = ProgressServer.EnsureRoster().Find(args[1]); if (entry != null) { return null; } return "No player '" + args[1] + "' in the roster."; } private static string FindPlayerAndStage(string verb, string[] args, out RosterEntry entry, out Stage stage) { entry = null; stage = null; if (args.Length < 3) { return "Usage: lockstep " + verb + " <player> <stage>"; } entry = ProgressServer.EnsureRoster().Find(args[1]); stage = Chain.Find(args[2]); if (entry == null) { return "No player '" + args[1] + "' in the roster."; } if (stage != null) { return null; } return "No stage '" + args[2] + "' in the chain."; } private static void SaveAndPublish() { ProgressServer.EnsureRoster().Save(); ProgressServer.Publish(); } private static string Status() { Roster roster = ProgressServer.EnsureRoster(); HashSet<long> online = new HashSet<long>(from p in ProgressServer.Online() select p.Id); StringBuilder stringBuilder = new StringBuilder("Lockstep status\n"); foreach (Stage stage in Chain.Stages) { List<string> list = ProgressServer.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 player in roster.Data.Players) { AppendPlayer(stringBuilder, player, online); } return stringBuilder.ToString().TrimEnd(new char[1] { '\n' }); } private static void AppendPlayer(StringBuilder text, RosterEntry entry, HashSet<long> online) { string value = (entry.Ignored ? "ignored" : (online.Contains(entry.Id) ? "online" : (ProgressServer.Counts(entry, online) ? "counted" : "inactive"))); string text2 = string.Join(", ", from s in Chain.Stages where entry.Cleared.Contains(s.Key) select s.Name); text.Append(" ").Append(entry.Name).Append(" (") .Append(entry.Id) .Append(") ") .Append(value) .Append(", last seen ") .Append(entry.LastSeen) .Append(", cleared: ") .Append((text2.Length > 0) ? text2 : "none") .Append('\n'); } } } 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 global::Charter.Charter Sync { get; } public ConfigFile Config { get; } public ManualLogSource Log { get; } public YamlFileHub Yaml { get; } public YamlEditorWindow YamlEditor { get; } public IReadOnlyList<string> SearchPaths { get; } public IDisposable? ConfigWatcher { get; private set; } public bool IsLocked { get { if (Sync.IsBound) { return !Sync.MayAmend; } return false; } } public bool IsAdmin => Sync.IsSteward; public bool IsAuthor => Sync.IsAuthor; public SyncedConfiguration(BaseUnityPlugin plugin, ManualLogSource log, string title, string version, string? oldestAccepted = null, bool mandatory = true) { Config = plugin.Config; Log = log; string gUID = plugin.Info.Metadata.GUID; Sync = new global::Charter.Charter(gUID, title, version, oldestAccepted, mandatory); SearchPaths = new string[2] { Path.GetDirectoryName(Config.ConfigFilePath) ?? Paths.ConfigPath, Path.GetDirectoryName(((object)plugin).GetType().Assembly.Location) ?? Paths.PluginPath }; Yaml = new YamlFileHub(title, Log, SearchPaths, Sync); YamlEditor = new YamlEditorWindow(Yaml, title + " 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.Clause<T>(val, !synced); return val; } public Clause<T> Add<T>(ConfigEntry<T> entry, bool synced = true) { return Sync.Clause<T>(entry, !synced); } public ConfigEntry<bool> BindLocking(string section, string key, bool defaultValue, string description) { ConfigEntry<bool> val = Config.Bind<bool>(section, key, defaultValue, description); Sync.Binding(val); return val; } public YamlFileSet AddYaml(YamlFileSet set) { return Yaml.Register(set); } public void Finish(Harmony harmony, int yamlApplyPriority = 400) { global::Charter.Charter.Install(harmony); ConfigWatcher = ConfigReloader.Setup(Config, Log); if (Yaml.Sets.Count > 0) { Yaml.HookGame(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 YamlEditorWindow { private const float ValidationHeight = 100f; private readonly YamlFileHub hub; private readonly string windowTitle; private readonly List<string> paths = new List<string>(); private readonly List<string> texts = new List<string>(); private readonly List<bool> expanded = new List<bool>(); private readonly List<Vector2> scrolls = new List<Vector2>(); private readonly List<string> errors = new List<string>(); private readonly List<string> warnings = new List<string>(); private Vector2 validationScroll; private YamlFileSet? set; private Texture2D? background; private GUIStyle? errorStyle; public Func<string, string> Translate { get; set; } = (string caption) => caption; public bool IsOpen => set != null; private Texture2D Background { get { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Expected O, but got Unknown //IL_0037: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)background == (Object)null) { background = new Texture2D(1, 1); background.SetPixel(0, 0, new Color(0f, 0f, 0f, 0.9f)); background.Apply(); } return background; } } private GUIStyle ErrorStyle { get { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Expected O, but got Unknown //IL_0031: Expected O, but got Unknown GUIStyle obj = errorStyle; if (obj == null) { GUIStyle val = new GUIStyle(GUI.skin.label); val.normal.textColor = Color.red; GUIStyle val2 = val; errorStyle = val; obj = val2; } return obj; } } public YamlEditorWindow(YamlFileHub hub, string windowTitle) { this.hub = hub; this.windowTitle = windowTitle; } public void Open(YamlFileSet set) { //IL_007e: Unknown result type (might be due to invalid IL or missing references) this.set = set; paths.Clear(); texts.Clear(); expanded.Clear(); scrolls.Clear(); foreach (KeyValuePair<string, string> file in set.Files) { paths.Add(file.Key); texts.Add(file.Value); expanded.Add(item: true); scrolls.Add(Vector2.zero); } Validate(); } public void Close() { set = null; } public void DrawButtons() { foreach (YamlFileSet item in hub.Sets.Where((YamlFileSet s) => s.Files.Count > 0)) { if (GUILayout.Button(Translate(item.EditorLabel()), (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.ExpandWidth(true) })) { Open(item); } } } public void Update() { if (IsOpen) { Cursor.lockState = (CursorLockMode)0; Cursor.visible = true; } } public void OnGUI() { //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) if (set != null) { HandleEditingKeys(); GUI.DrawTexture(new Rect(0f, 0f, (float)Screen.width, (float)Screen.height), (Texture)(object)Background); GUILayout.BeginArea(new Rect(20f, 20f, (float)(Screen.width - 40), (float)(Screen.height - 40))); GUILayout.Label(Translate(windowTitle), Array.Empty<GUILayoutOption>()); DrawTopRow(); for (int i = 0; i < texts.Count; i++) { DrawFile(i); } DrawValidation(); GUILayout.EndArea(); } } private void DrawTopRow() { GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); GUI.enabled = hub.IsAuthor && errors.Count == 0; if (GUILayout.Button(Translate("Save and apply"), Array.Empty<GUILayoutOption>())) { Submit(saveToDisk: true); } if (GUILayout.Button(Translate("Apply without saving"), Array.Empty<GUILayoutOption>())) { Submit(saveToDisk: false); } GUI.enabled = true; if (GUILayout.Button(Translate("Discard"), Array.Empty<GUILayoutOption>())) { Close(); } GUILayout.EndHorizontal(); } private void Submit(bool saveToDisk) { if (set != null) { Dictionary<string, string> dictionary = new Dictionary<string, string>(); for (int i = 0; i < paths.Count; i++) { dictionary[paths[i]] = texts[i]; } hub.Replace(set, dictionary, saveToDisk); Close(); } } private void DrawFile(int index) { //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0072: Unknown result type (might be due to invalid IL or missing references) if (texts.Count > 1) { expanded[index] = GUILayout.Toggle(expanded[index], Path.GetFileName(paths[index]), Array.Empty<GUILayoutOption>()); if (!expanded[index]) { return; } } scrolls[index] = GUILayout.BeginScrollView(scrolls[index], (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.ExpandHeight(true) }); GUI.SetNextControlName(ControlName(index)); string text = GUILayout.TextArea(texts[index], (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.ExpandHeight(true) }); GUILayout.EndScrollView(); if (text != texts[index]) { texts[index] = text; Validate(); } } private void DrawValidation() { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) validationScroll = GUILayout.BeginScrollView(validationScroll, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(100f) }); if (errors.Count == 0 && warnings.Count == 0) { GUILayout.Label(Translate("Configuration is valid."), Array.Empty<GUILayoutOption>()); } foreach (string error in errors) { GUILayout.Label(error, ErrorStyle, Array.Empty<GUILayoutOption>()); } foreach (string warning in warnings) { GUILayout.Label(warning, Array.Empty<GUILayoutOption>()); } GUILayout.EndScrollView(); } private void Validate() { errors.Clear(); warnings.Clear(); if (set != null) { YamlModel yamlModel = set.CreateModel(); yamlModel.LoadAll(paths.Select((string path, int i) => new KeyValuePair<string, string>(path, texts[i]))); errors.AddRange(yamlModel.Errors); warnings.AddRange(yamlModel.Warnings); } } private void HandleEditingKeys() { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Invalid comparison between Unknown and I4 //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Invalid comparison between Unknown and I4 //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Invalid comparison between Unknown and I4 //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Invalid comparison between Unknown and I4 //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Expected O, but got Unknown Event current = Event.current; if ((int)current.type == 4) { bool flag = (int)current.keyCode == 9 || current.character == '\t'; bool flag2 = (int)current.keyCode == 13 || (int)current.keyCode == 271 || current.character == '\n'; int num = FocusedFile(); if (num >= 0 && (flag || flag2)) { TextEditor val = (TextEditor)GUIUtility.GetStateObject(typeof(TextEditor), GUIUtility.keyboardControl); val.ReplaceSelection(flag ? " " : ("\n" + IndentationOfLineAt(val.text, val.cursorIndex))); texts[num] = val.text; current.Use(); Validate(); } } } private int FocusedFile() { string nameOfFocusedControl = GUI.GetNameOfFocusedControl(); for (int i = 0; i < texts.Count; i++) { if (nameOfFocusedControl == ControlName(i)) { return i; } } return -1; } private static string IndentationOfLineAt(string text, int position) { int num = Math.Max(0, Math.Min(position, text.Length)); while (num > 0 && text[num - 1] != '\n') { num--; } int i; for (i = num; i < text.Length && text[i] == ' '; i++) { } return text.Substring(num, i - num); } private static string ControlName(int index) { return "YamlEditorWindow.File" + index; } } public sealed class YamlFileHub { private readonly string modName; private readonly ManualLogSource log; private readonly global::Charter.Charter? charter; private readonly List<YamlFileSet> sets = new List<YamlFileSet>(); private readonly YamlFileStore store; private readonly YamlFileWatcher watcher; private bool loaded; public IReadOnlyList<YamlFileSet> Sets => sets; public bool IsAuthor => charter?.IsAuthor ?? true; public bool IsLocked { get { if (charter != null && charter.IsBound) { return !charter.MayAmend; } return false; } } public bool SuppressWriteBack { get; set; } public Func<bool> CanApply { get; set; } = () => true; public event Action<YamlFileSet>? Applied; public YamlFileHub(string modName, ManualLogSource log, IEnumerable<string> searchFolders, global::Charter.Charter? charter) { this.modName = modName; this.log = log; this.charter = charter; store = new YamlFileStore(modName, log, searchFolders); watcher = new YamlFileWatcher(store, sets, ReloadFromDisk, log, modName); } public YamlFileSet Register(YamlFileSet set) { if (sets.Any((YamlFileSet s) => string.Equals(s.SyncKey, set.SyncKey, StringComparison.OrdinalIgnoreCase))) { throw new ArgumentException(modName + ": a YAML file set with sync key '" + set.SyncKey + "' is already registered"); } sets.Add(set); if (charter != null) { Article<List<string>> channel = new Article<List<string>>(charter, set.SyncKey, new List<string>()); set.Channel = channel; channel.Changed += delegate { Receive(set, YamlFileList.Unflatten(channel.Value)); }; } return set; } public void LoadAll() { loaded = true; foreach (YamlFileSet set in sets) { LoadSet(set); } watcher.Start(); } public void ApplyAll() { if (!CanApply()) { log.LogDebug((object)(modName + ": YAML apply skipped, the game is not ready")); return; } foreach (YamlFileSet set in sets) { Apply(set); } } public bool TryBuild(YamlFileSet set, IReadOnlyDictionary<string, string> files, out YamlModel model, string context) { model = set.CreateModel(); model.LoadAll(files); foreach (string warning in model.Warnings) { log.LogWarning((object)(modName + ": " + warning)); } if (model.Errors.Count == 0) { return true; } log.LogError((object)$"{modName}: {set.MainFileName} ({context}) has {model.Errors.Count} error(s), the previous configuration stays:"); foreach (string error in model.Errors) { log.LogError((object)(modName + ": " + error)); } return false; } public void Replace(YamlFileSet set, IReadOnlyDictionary<string, string> files, bool saveToDisk) { if (TryBuild(set, files, out YamlModel _, "editor")) { Publish(set, files, saveToDisk); } } public void HookGame(Harmony harmony, int applyPriority) { YamlGameHooks.Install(this, harmony, applyPriority); } internal void LoadFromHook() { if (loaded) { return; } try { LoadAll(); } catch (Exception arg) { log.LogError((object)$"{modName}: loading YAML files failed: {arg}"); } } internal void ApplyFromHook() { try { ApplyAll(); } catch (Exception arg) { log.LogError((object)$"{modName}: applying YAML files failed: {arg}"); } } private void LoadSet(YamlFileSet set) { Dictionary<string, string> dictionary = store.ReadOrCreate(set); watcher.TakeSnapshot(set); YamlModel model; if (dictionary.Count == 0) { if (set.Enabled()) { log.LogWarning((object)(modName + ": no " + set.FilePattern + " found in " + string.Join(" or ", store.SearchFolders))); } } else if (TryBuild(set, dictionary, out model, "startup")) { Publish(set, dictionary, writeBack: false); } } private void ReloadFromDisk(YamlFileSet set) { Dictionary<string, string> dictionary = store.Read(store.Discover(set)); watcher.TakeSnapshot(set); YamlModel model; if (dictionary.Count == 0) { log.LogWarning((object)(modName + ": every " + set.FilePattern + " file is gone, keeping the loaded configuration")); } else if (TryBuild(set, dictionary, out model, "reload")) { Publish(set, dictionary, writeBack: false); log.LogInfo((object)(modName + ": " + set.MainFileName + " reloaded" + (IsAuthor ? "" : ", not applied, remote configuration active"))); } } private void Publish(YamlFileSet set, IReadOnlyDictionary<string, string> files, bool writeBack) { bool suppressWriteBack = SuppressWriteBack; SuppressWriteBack = suppressWriteBack || !writeBack; try { if (set.Channel == null) { Receive(set, files); } else { set.Channel.Assign(YamlFileList.Flatten(files)); } } finally { SuppressWriteBack = suppressWriteBack; } } private void Receive(YamlFileSet set, IReadOnlyDictionary<string, string> files) { if (!TryBuild(set, files, out YamlModel model, "received")) { foreach (KeyValuePair<string, string> file in files) { log.LogWarning((object)(modName + ": content of " + file.Key + ":\n" + file.Value)); } return; } set.Files = files; set.Current = model; if (CanApply()) { Apply(set); } if (IsAuthor && !SuppressWriteBack) { store.WriteAll(files); watcher.TakeSnapshot(set); } } private void Apply(YamlFileSet set) { if (set.Current == null || !set.Enabled()) { return; } try { set.Apply(set.Current); this.Applied?.Invoke(set); } catch (Exception arg) { log.LogError((object)$"{modName}: applying {set.MainFileName} failed: {arg}"); } } } internal static class YamlFileList { public static List<string> Flatten(IReadOnlyDictionary<string, string> files) { List<string> list = new List<string>(files.Count * 2); foreach (KeyValuePair<string, string> file in files) { list.Add(file.Key); list.Add(file.Value); } return list; } public static Dictionary<string, string> Unflatten(List<string>? list) { Dictionary<string, string> dictionary = new Dictionary<string, string>(); int num = 0; while (list != null && num + 1 < list.Count) { dictionary[list[num]] = list[num + 1]; num += 2; } return dictionary; } } public sealed class YamlFileSet { internal int ReloadPending; public string FilePattern { get; } public string MainFileName { get; } public string SyncKey { get; } public Func<YamlModel> CreateModel { get; } public Action<YamlModel> Apply { get; } public Func<bool> Enabled { get; set; } = () => true; public Func<byte[]?>? DefaultContent { get; set; } public Func<string> EditorLabel { get; set; } public IReadOnlyDictionary<string, string> Files { get; internal set; } = new Dictionary<string, string>(); public YamlModel? Current { get; internal set; } internal Article<List<string>>? Channel { get; set; } internal Dictionary<string, DateTime> Snapshot { get; } = new Dictionary<string, DateTime>(StringComparer.OrdinalIgnoreCase); public YamlFileSet(string filePattern, string syncKey, Func<YamlModel> createModel, Action<YamlModel> apply) { FilePattern = filePattern; MainFileName = filePattern.Replace("*", ""); SyncKey = syncKey; CreateModel = createModel; Apply = apply; EditorLabel = () => "Edit " + MainFileName; } } internal sealed class YamlFileStore { private readonly string modName; private readonly ManualLogSource log; private readonly List<string> searchFolders; public IReadOnlyList<string> SearchFolders => searchFolders; public YamlFileStore(string modName, ManualLogSource log, IEnumerable<string> searchFolders) { this.modName = modName; this.log = log; this.searchFolders = searchFolders.ToList(); } public List<string> Discover(YamlFileSet set) { Regex nameFilter = new Regex("^" + Regex.Escape(set.FilePattern).Replace("\\*", ".*") + "$", RegexOptions.IgnoreCase); List<string> list = new List<string>(); foreach (string item in searchFolders.Where(Directory.Exists)) { list.AddRange(from p in Directory.GetFiles(item, set.FilePattern) where nameFilter.IsMatch(Path.GetFileName(p)) select p); } return list.OrderBy((string p) => (!string.Equals(Path.GetFileName(p), set.MainFileName, StringComparison.OrdinalIgnoreCase)) ? 1 : 0).ThenBy<string, string>(Path.GetFileName, StringComparer.OrdinalIgnoreCase).ToList(); } public Dictionary<string, string> Read(IEnumerable<string> paths) { Dictionary<string, string> dictionary = new Dictionary<string, string>(); foreach (string path in paths) { try { dictionary[path] = File.ReadAllText(path); } catch (Exception ex) { log.LogError((object)(modName + ": reading " + path + " failed: " + ex.Message)); } } return dictionary; } public Dictionary<string, string> ReadOrCreate(YamlFileSet set) { Dictionary<string, string> dictionary = Read(Discover(set)); if (dictionary.Count <= 0) { return WriteDefault(set); } return dictionary; } public Dictionary<string, DateTime> WriteTimes(YamlFileSet set) { Dictionary<string, DateTime> dictionary = new Dictionary<string, DateTime>(StringComparer.OrdinalIgnoreCase); foreach (string item in Discover(set)) { dictionary[item] = File.GetLastWriteTimeUtc(item); } return dictionary; } public void WriteAll(IReadOnlyDictionary<string, string> files) { foreach (KeyValuePair<string, string> file in files) { WriteAtomic(file.Key, file.Value); } } private Dictionary<string, string> WriteDefault(YamlFileSet set) { byte[] array = set.DefaultContent?.Invoke(); if (array == null || searchFolders.Count == 0) { return new Dictionary<string, string>(); } string text = Path.Combine(searchFolders[0], set.MainFileName); try { Directory.CreateDirectory(searchFolders[0]); File.WriteAllBytes(text, array); log.LogInfo((object)(modName + ": wrote default " + text)); } catch (Exception ex) { log.LogError((object)(modName + ": writing default " + text + " failed: " + ex.Message)); return new Dictionary<string, string>(); } return Read(new string[1] { text }); } private void WriteAtomic(string path, string content) { try { if (!File.Exists(path) || !(File.ReadAllText(path) == content)) { ReplaceThroughTempFile(path, content); log.LogInfo((object)(modName + ": wrote " + path)); } } catch (Exception ex) { log.LogError((object)(modName + ": writing " + path + " failed: " + ex.Message)); } } private static void ReplaceThroughTempFile(string path, string content) { string text = path + ".tmp"; File.WriteAllText(text, content, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); if (File.Exists(path)) { File.Replace(text, path, null); } else { File.Move(text, path); } } } internal sealed class YamlFileWatcher { private const int PollMilliseconds = 5000; private readonly YamlFileStore store; private readonly IReadOnlyList<YamlFileSet> sets; private readonly Action<YamlFileSet> reload; private readonly ManualLogSource log; private readonly string modName; private Timer? timer; public YamlFileWatcher(YamlFileStore store, IReadOnlyList<YamlFileSet> sets, Action<YamlFileSet> reload, ManualLogSource log, string modName) { this.store = store; this.sets = sets; this.reload = reload; this.log = log; this.modName = modName; } public void Start() { if (timer == null) { timer = new Timer(delegate { Poll(); }, null, 5000, 5000); } } public void TakeSnapshot(YamlFileSet set) { Dictionary<string, DateTime> dictionary = store.WriteTimes(set); lock (set.Snapshot) { set.Snapshot.Clear(); foreach (KeyValuePair<string, DateTime> item in dictionary) { set.Snapshot[item.Key] = item.Value; } } } private void Poll() { try { foreach (YamlFileSet set in sets.Where(HasChanged)) { if (Interlocked.CompareExchange(ref set.ReloadPending, 1, 0) == 0) { ThreadingHelper.SynchronizingObject.BeginInvoke((Action)delegate { Reload(set); }, null); } } } catch (Exception ex) { log.LogError((object)(modName + ": watching YAML files failed: " + ex.Message)); } } private bool HasChanged(YamlFileSet set) { Dictionary<string, DateTime> dictionary = store.WriteTimes(set); lock (set.Snapshot) { DateTime value; return dictionary.Count != set.Snapshot.Count || dictionary.Any((KeyValuePair<string, DateTime> pair) => !set.Snapshot.TryGetValue(pair.Key, out value) || value != pair.Value); } } private void Reload(YamlFileSet set) { try { reload(set); } catch (Exception arg) { log.LogError((object)$"{modName}: reloading {set.MainFileName} failed: {arg}"); } finally { Interlocked.Exchange(ref set.ReloadPending, 0); } } } internal static class YamlGameHooks { private static class Patches { public static void AfterMenuStart() { LoadHooked(); } public static void AfterNetworkStart(ZNet __instance) { if (__instance.IsServer() && __instance.IsDedicated()) { LoadHooked(); } } public static void AfterRespawnRequest(Game __instance) { object obj = firstSpawnField?.GetValue(__instance); if (obj is bool && (bool)obj) { ApplyHooked(); } } } private static readonly List<YamlFileHub> hooked = new List<YamlFileHub>(); private static readonly FieldInfo? firstSpawnField = AccessTools.Field(typeof(Game), "m_firstSpawn"); private static bool patched; public static void Install(YamlFileHub hub, Harmony harmony, int applyPriority) { //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Expected O, but got Unknown //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Expected O, but got Unknown //IL_00a2: Unknown result type (might be due to invalid IL or missing references) //IL_00a7: Unknown result type (might be due to invalid IL or missing references) //IL_00af: Expected O, but got Unknown if (!hooked.Contains(hub)) { hooked.Add(hub); } if (!patched) { patched = true; harmony.Patch((MethodBase)AccessTools.Method(typeof(FejdStartup), "Start", (Type[])null, (Type[])null), (HarmonyMethod)null, new HarmonyMethod(typeof(Patches), "AfterMenuStart", (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(Patches), "AfterNetworkStart", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); HarmonyMethod val = new HarmonyMethod(typeof(Patches), "AfterRespawnRequest", (Type[])null) { priority = applyPriority }; harmony.Patch((MethodBase)AccessTools.Method(typeof(Game), "RequestRespawn", (Type[])null, (Type[])null), (HarmonyMethod)null, val, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } } private static void LoadHooked() { foreach (YamlFileHub item in hooked) { item.LoadFromHook(); } } private static void ApplyHooked() { foreach (YamlFileHub item in hooked) { item.ApplyFromHook(); } } } internal sealed class YamlGroupTable { private readonly Dictionary<string, List<string>> members = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, List<string>> parents = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase); public IReadOnlyCollection<string> Names => members.Keys; public bool IsGroup(string name) { return members.ContainsKey(name); } public void Add(string group, IEnumerable<string> names) { if (!members.TryGetValue(group, out List<string> value)) { value = new List<string>(); members.Add(group, value); } foreach (string name in names) { value.Add(name); if (!parents.TryGetValue(name, out List<string> value2)) { value2 = new List<string>(); parents.Add(name, value2); } value2.Add(group); } } public IReadOnlyList<string> Resolve(string name) { List<string> list = new List<string>(); HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); Queue<string> queue = new Queue<string>(); queue.Enqueue(name); while (queue.Count > 0) { if (!parents.TryGetValue(queue.Dequeue(), out List<string> value)) { continue; } foreach (string item in value) { if (hashSet.Add(item)) { list.Add(item); queue.Enqueue(item); } } } return list; } public IReadOnlyList<string> Expand(string group) { List<string> result = new List<string>(); HashSet<string> seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase); Expand(group, seen, result); return result; } private void Expand(string name, HashSet<string> seen, List<string> result) { if (!seen.Add(name)) { return; } if (!members.TryGetValue(name, out List<string> value)) { result.Add(name); return; } foreach (string item in value) { Expand(item, seen, result); } } } public abstract class YamlModel { private static readonly IDeserializer Parser = new DeserializerBuilder().Build(); private string? currentFile; public List<string> Errors { get; } = new List<string>(); public List<string> Warnings { get; } = new List<string>(); protected YamlGroupTable Groups { get; } = new YamlGroupTable(); public void Load(string yamlText) { ReadText(yamlText); Verify(); } public void LoadAll(IEnumerable<KeyValuePair<string, string>> files) { foreach (KeyValuePair<string, string> file in files) { currentFile = Path.GetFileName(file.Key); ReadText(file.Value); } currentFile = null; Verify(); } public IReadOnlyList<string> ResolveGroups(string name) { return Groups.Resolve(name); } protected abstract void Read(YamlNode root); protected virtual void Verify() { } protected void ReadGroups(YamlNode root, string key = "groups") { foreach (KeyValuePair<string, YamlNode> entry in root.Get(key).Entries) { if (entry.Value.TryStringList(out List<string> items)) { Groups.Add(entry.Key, items); } } } internal void AddError(string message) { Errors.Add(WithFile(message)); } internal void AddWarning(string message) { Warnings.Add(WithFile(message)); } private string WithFile(string message) { if (currentFile != null) { return currentFile + ": " + message; } return message; } private void ReadText(string yamlText) { if (TryParse(yamlText, out object graph) && graph != null) { if (!(graph is IDictionary<object, object>)) { AddError("the file must be a map of keys at the top level, not " + ((graph is IList<object>) ? "a list" : "a single value")); return; } YamlNode yamlNode = new YamlNode(this, graph, ""); Read(yamlNode); yamlNode.WarnUnknownKeys(); } } private bool TryParse(string yamlText, out object? graph) { try { graph = Parser.Deserialize<object>(yamlText.TrimStart(new char[1] { '\ufeff' })); return true; } catch (Exception ex) { AddError("invalid YAML: " + ex.Message + ((ex.InnerException == null) ? "" : (" (" + ex.InnerException.Message + ")"))); graph = null; return false; } } } internal enum YamlNodeKind { Scalar, List, Map, Null, Missing } internal sealed class YamlNode { private readonly YamlModel model; private readonly object? value; private List<KeyValuePair<string, YamlNode>>? mapEntries; private Dictionary<string, YamlNode>? mapLookup; private List<YamlNode>? listItems; private HashSet<string>? askedKeys; private bool visited; private bool shapeReported; public string Path { get; } public YamlNodeKind Kind { get; } public string? Text { get { if (Kind != YamlNodeKind.Scalar) { return null; } return Convert.ToString(value, CultureInfo.InvariantCulture); } } public int Count => Kind switch { YamlNodeKind.Map => MapEntries.Count, YamlNodeKind.List => ListItems.Count, _ => 0, }; public IReadOnlyList<KeyValuePair<string, YamlNode>> Entries { get { if (!ExpectShape(YamlNodeKind.Map, "a map")) { return Array.Empty<KeyValuePair<string, YamlNode>>(); } visited = true; AskedKeys.UnionWith(MapEntries.Select<KeyValuePair<string, YamlNode>, string>((KeyValuePair<string, YamlNode> e) => e.Key)); return MapEntries; } } public IReadOnlyList<YamlNode> Items { get { if (!ExpectShape(YamlNodeKind.List, "a list")) { return Array.Empty<YamlNode>(); } return ListItems; } } private HashSet<string> AskedKeys => askedKeys ?? (askedKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase)); private List<KeyValuePair<string, YamlNode>> MapEntries { get { if (mapEntries == null) { BuildMap(); } return mapEntries; } } private Dictionary<string, YamlNode> MapLookup { get { if (mapLookup == null) { BuildMap(); } return mapLookup; } } private List<YamlNode> ListItems { get { if (listItems == null) { IList<object> source = (IList<object>)value; listItems = source.Select((object item, int i) => new YamlNode(model, item, $"{Path}[{i}]")).ToList(); } return listItems; } } internal YamlNode(YamlModel model, object? value, string path, YamlNodeKind kind) { this.model = model; this.value = value; Path = path; Kind = kind; } internal YamlNode(YamlModel model, object? value, string path) : this(model, value, path, KindOf(value)) { } public YamlNode Get(string key) { string path = (string.IsNullOrEmpty(Path) ? key : (Path + "." + key)); if (!ExpectShape(YamlNodeKind.Map, "a map")) { return new YamlNode(model, null, path, YamlNodeKind.Missing); } visited = true; AskedKeys.Add(key); if (!MapLookup.TryGetValue(key, out YamlNode result)) { return new YamlNode(model, null, path, YamlNodeKind.Missing); } return result; } public bool Is(string keyword) { if (Kind == YamlNodeKind.Scalar) { return string.Equals(Text.Trim(), keyword, StringComparison.OrdinalIgnoreCase); } return false; } public void Error(string message) { model.AddError(Prefixed(message)); } public void Warn(string message) { model.AddWarning(Prefixed(message)); } public void WarnUnknownKeys() { if (Kind == YamlNodeKind.Map && visited) { List<string> list = (from e in MapEntries select e.Key into k where !AskedKeys.Contains(k) select k).ToList(); if (list.Count > 0) { Warn("unknown keys " + string.Join(", ", list)); } { foreach (KeyValuePair<string, YamlNode> mapEntry in MapEntries) { mapEntry.Value.WarnUnknownKeys(); } return; } } if (Kind == YamlNodeKind.List && listItems != null) { listItems.ForEach(delegate(YamlNode item) { item.WarnUnknownKeys(); }); } } private string Prefixed(string message) { if (!string.IsNullOrEmpty(Path)) { return Path + ": " + message; } return message; } private void BuildMap() { mapEntries = new List<KeyValuePair<string, YamlNode>>(); mapLookup = new Dictionary<string, YamlNode>(StringComparer.OrdinalIgnoreCase); foreach (KeyValuePair<object, object> item in (IDictionary<object, object>)value) { string text = Convert.ToString(item.Key, CultureInfo.InvariantCulture) ?? ""; string path = (string.IsNullOrEmpty(Path) ? text : (Path + "." + text)); YamlNode yamlNode = new YamlNode(model, item.Value, path); if (mapLookup.ContainsKey(text)) { yamlNode.Warn("duplicate key (differs only in case), ignored"); continue; } mapLookup.Add(text, yamlNode); mapEntries.Add(new KeyValuePair<string, YamlNode>(text, yamlNode)); } } private bool ExpectShape(YamlNodeKind wanted, string description) { if (Kind == wanted) { return true; } if (Kind != YamlNodeKind.Missing && !shapeReported) { shapeReported = true; Error((Kind == YamlNodeKind.Null) ? ("expected " + description + " but the key has no value") : ("expected " + description + ", found " + Describe(Kind))); } return false; } private static string Describe(YamlNodeKind kind) { return kind switch { YamlNodeKind.Scalar => "a single value", YamlNodeKind.List => "a list", YamlNodeKind.Map => "a map", _ => "nothing", }; } private static YamlNodeKind KindOf(object? value) { if (value != null) { if (!(value is IDictionary<object, object>)) { if (value is IList<object>) { return YamlNodeKind.List; } return YamlNodeKind.Scalar; } return YamlNodeKind.Map; } return YamlNodeKind.Null; } public bool TryInt(out int result) { return TryScalar<int>(out result); } public bool TryFloat(out float result) { return TryScalar<float>(out result); } public bool TryBool(out bool result) { return TryScalar<bool>(out result); } public bool TryString(out string result) { return TryScalar<string>(out result); } public bool TryEnum<T>(out T result) where T : struct, Enum { return TryScalar<T>(out result); } public bool TryList<T>(out List<T> items) { items = new List<T>(); if (!ExpectShape(YamlNodeKind.List, "a list of " + YamlScalarParser.Plural<T>())) { return false; } bool result = true; foreach (YamlNode listItem in ListItems) { if (listItem.TryScalar<T>(out var result2)) { items.Add(result2); } else { result = false; } } return result; } public bool TryStringList(out List<string> items) { if (Kind == YamlNodeKind.Scalar) { items = new List<string> { Text }; return true; } return TryList(out items); } private bool TryScalar<T>(out T result) { result = default(T); if (!ExpectShape(YamlNodeKind.Scalar, YamlScalarParser.Describe(typeof(T)))) { return false; } if (YamlScalarParser.TryParse(Text, typeof(T), out object result2)) { result = (T)result2; return true; } Error("'" + Text + "' is not " + YamlScalarParser.Describe(typeof(T))); return false; } } internal static class YamlScalarParser { private static readonly string[] TrueWords = new string[4] { "true", "yes", "on", "1" }; private static readonly string[] FalseWords = new string[4] { "false", "no", "off", "0" }; public static bool TryParse(string text, Type type, out object? result) { string text2 = text.Trim(); result = null; if (type == typeof(string)) { result = text; return true; } if (type == typeof(int)) { int result3; bool result2 = int.TryParse(text2, NumberStyles.Integer, CultureInfo.InvariantCulture, out result3); result = result3; return result2; } if (type == typeof(float)) { float result5; bool result4 = float.TryParse(text2, NumberStyles.Float, CultureInfo.InvariantCulture, out result5); result = result5; return result4; } if (!(type == typeof(bool))) { return TryParseEnum(text2, type, out result); } return TryParseBool(text2, out result); } public static string Describe(Type type) { if (type == typeof(int)) { return "an integer"; } if (type == typeof(float)) { return "a number"; } if (type == typeof(bool)) { return "true or false"; } if (!type.IsEnum) { return "text"; } return "one of " + string.Join(", ", Enum.GetNames(type)); } public static string Plural<T>() { if (!(typeof(T) == typeof(int))) { if (!(typeof(T) == typeof(float))) { if (!(typeof(T) == typeof(bool))) { if (!typeof(T).IsEnum) { return "text values"; } return "names (" + string.Join(", ", Enum.GetNames(typeof(T))) + ")"; } return "true/false values"; } return "numbers"; } return "integers"; } private static bool TryParseBool(string text, out object? result) { if (TrueWords.Contains<string>(text, StringComparer.OrdinalIgnoreCase)) { result = true; return true; } result = false; return FalseWords.Contains<string>(text, StringComparer.OrdinalIgnoreCase); } private static bool TryParseEnum(string text, Type type, out object? result) { if (!type.IsEnum) { throw new NotSupportedException("YamlNode cannot read values of type " + type.Name); } string text2 = Enum.GetNames(type).FirstOrDefault((string n) => string.Equals(n, text, StringComparison.OrdinalIgnoreCase)); result = ((text2 == null) ? null : Enum.Parse(type, text2)); return text2 != null; } } } 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 { if (SnapshotChanged()) { string text = ReadOrNull(config.ConfigFilePath); if (text != null && !(text == lastContent)) { lastContent = text; ScheduleCheck(); } } } catch (Exception ex) { ManualLogSource? obj = log; if (obj != null) { obj.LogError((object)("Watching " + fileName + " failed: " + ex.Message)); } } } private bool SnapshotChanged() { Snapshot(out var writeTime, out var length); if (writeTime == lastWriteTime && length == lastLength) { return false; } lastWriteTime = writeTime; lastLength = length; return true; } private void ScheduleCheck() { if (Interlocked.CompareExchange(ref reloading, 1, 0) == 0) { ThreadingHelper.SynchronizingObject.BeginInvoke(new Action(CheckAndReload), null); } } private void CheckAndReload() { try { if (!LoadedValues.Matches(config, lastContent)) { 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 { Resnapshot(); Interlocked.Exchange(ref reloading, 0); } } private void Resnapshot() { Snapshot(out lastWriteTime, out lastLength); lastContent = ReadOrNull(config.ConfigFilePath) ?? lastContent; } 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); } } internal static class LoadedValues { private static readonly PropertyInfo? orphans = typeof(ConfigFile).GetProperty("OrphanedEntries", BindingFlags.Instance | BindingFlags.NonPublic); public static bool Matches(ConfigFile config, string content) { Dictionary<ConfigDefinition, string> dictionary = Parse(content); Dictionary<ConfigDefinition, string> dictionary2 = Loaded(config); if (dictionary2 == null) { return false; } if (dictionary.Count != dictionary2.Count) { return false; } foreach (KeyValuePair<ConfigDefinition, string> item in dictionary) { if (!dictionary2.TryGetValue(item.Key, out var value) || !string.Equals(value, item.Value, StringComparison.Ordinal)) { return false; } } return true; } private static Dictionary<ConfigDefinition, string>? Loaded(ConfigFile config) { if (!(orphans?.GetValue(config) is Dictionary<ConfigDefinition, string> dictionary)) { return null; } Dictionary<ConfigDefinition, string> dictionary2 = new Dictionary<ConfigDefinition, string>(dictionary); foreach (KeyValuePair<ConfigDefinition, ConfigEntryBase> item in config) { dictionary2[item.Key] = item.Value.GetSerializedValue(); } return dictionary2; } private static Dictionary<ConfigDefinition, string> Parse(string content) { //IL_009a: Unknown result type (might be due to invalid IL or missing references) //IL_00b4: Expected O, but got Unknown Dictionary<ConfigDefinition, string> dictionary = new Dictionary<ConfigDefinition, string>(); string text = ""; string[] array = content.Split(new char[1] { '\n' }); for (int i = 0; i < array.Length; i++) { string text2 = array[i].Trim(); if (text2.Length == 0 || text2.StartsWith("#", StringComparison.Ordinal)) { continue; } if (text2.StartsWith("[", StringComparison.Ordinal) && text2.EndsWith("]", StringComparison.Ordinal)) { text = text2.Substring(1, text2.Length - 2); continue; } int num = text2.IndexOf('='); if (num > 0) { dictionary[new ConfigDefinition(text, text2.Substring(0, num).Trim())] = text2.Substring(num + 1).Trim(); } } return dictionary; } } } namespace Charter { internal sealed class Amendments { private readonly Charter charter; private readonly Ledger ledger; private readonly Journal journal; private readonly string rpcName; public Amendments(Charter charter, Ledger ledger, Journal journal) { this.charter = charter; this.ledger = ledger; this.journal = journal; rpcName = "Charter_" + charter.Guid + "_Amend"; } public void Send(IClause clause) { //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Expected O, but got Unknown ZRpc rpc = charter.Receiver.Rpc; if (rpc != null && rpc.IsConnected()) { string currentToml = clause.CurrentToml; ZPackage val = new ZPackage(); val.Write(1); val.Write(charter.Guid); val.Write(clause.Section); val.Write(clause.Key); val.Write(currentToml); rpc.Invoke(rpcName, new object[1] { val }); journal.Info("amendment sent: " + clause.Section + "." + clause.Key + " = " + currentToml); } } public void OnAmend(ZRpc rpc, ZPackage pkg) { try { Receive(rpc, pkg); } catch (Exception arg) { journal.Error($"handling an amendment failed: {arg}"); } } private void Receive(ZRpc rpc, ZPackage pkg) { if (pkg.ReadInt() != 1 || pkg.ReadString() != charter.Guid) { journal.Warning("amendment with a foreign protocol or guid dropped"); return; } string section = pkg.ReadString(); string key = pkg.ReadString(); string toml = pkg.ReadString(); ZNetPeer val = Side.PeerOf(rpc); if (val != null && Side.IsServer) { string text = Accept(val, section, key, toml); if (text != null) { Reject(val, section, key, text); } } } private string? Accept(ZNetPeer peer, string section, string key, string toml) { if (!Stewardship.IsSteward(peer)) { return "you are not a steward"; } IClause clause = ledger.Find(section, key); if (clause == null) { return "unknown clause"; } if (clause.Local) { return "the clause is local"; } try { clause.Entry.SetSerializedValue(toml); } catch (Exception ex) { return "bad value: " + ex.Message; } clause.Entry.ConfigFile.Save(); journal.Info("amendment by " + Side.NameOf(peer) + " accepted: " + section + "." + key + " = " + toml); return null; } private void Reject(ZNetPeer peer, string section, string key, string reason) { journal.Warning("amendment by " + Side.NameOf(peer) + " rejected: " + section + "." + key + ": " + reason); charter.Publisher.SendNotice(peer, ledger.Find(section, key), "amendment of " + section + "." + key + " rejected: " + reason); } } internal sealed class Article<T> : IArticle { private readonly Charter charter; private readonly ArticleKind kind; private T value; public string Name { get; } public bool Standing { get; } public T Value => value; ArticleKind IArticle.Kind => kind; object IArticle.Boxed => value; public event Action? Changed; public Article(Charter charter, string name, T initial, bool standing = false) { kind = ArticleValues.KindOf(typeof(T)); this.charter = charter; Name = name; Standing = standing; value = initial; charter.Ledger.Add(this); } public void Assign(T value) { if (!charter.IsAuthor) { charter.Journal.Warning("article '" + Name + "' cannot be assigned while the server's charter binds"); } else if (!ArticleValues.Same(this.value, value)) { this.value = value; Raise(); charter.Publisher.MarkDirty(this); } } bool IArticle.Accept(object incoming) { if (ArticleValues.Same(value, incoming)) { return false; } value = (T)incoming; return true; } void IArticle.Raise() { Raise(); } private void Raise() { try { this.Changed?.Invoke(); } catch (Exception arg) { charter.Journal.Error($"a Changed handler of article '{Name}' threw: {arg}"); } } } internal interface IArticle { string Name { get; } bool Standing { get; } ArticleKind Kind { get; } object Boxed { get; } bool Accept(object incoming); void Raise(); } internal enum ArticleKind : byte { Text = 1, TextList, Integer, Number, Toggle } internal static class ArticleValues { public static ArticleKind KindOf(Type type) { if (type == typeof(string)) { return ArticleKind.Text; } if (type == typeof(List<string>)) { return ArticleKind.TextList; } if (type == typeof(int)) { return ArticleKind.Integer; } if (type == typeof(float)) { return ArticleKind.Number; } if (type == typeof(bool)) { return ArticleKind.Toggle; } throw new ArgumentException("Charter articles hold string, List<string>, int, float or bool, not " + type.Name); } public static void Write(ZPackage pkg, ArticleKind kind, object? value) { switch (kind) { case ArticleKind.Text: pkg.Write((value as string) ?? ""); break; case ArticleKind.TextList: WriteList(pkg, value as List<string>); break; case ArticleKind.Integer: pkg.Write((value is int num2) ? num2 : 0); break; case ArticleKind.Number: pkg.Write((value is float num3) ? num3 : 0f); break; case ArticleKind.Toggle: { bool flag = default(bool); int num; if (value is bool) { flag = (bool)value; num = 1; } else { num = 0; } pkg.Write((byte)((uint)num & (flag ? 1u : 0u)) != 0); break; } default: throw new ArgumentException($"unknown article kind {kind}"); } } public static object Read(ZPackage pkg, ArticleKind kind) { return kind switch { ArticleKind.Text => pkg.ReadString(), ArticleKind.TextList => ReadList(pkg), ArticleKind.Integer => pkg.ReadInt(), ArticleKind.Number => pkg.ReadSingle(), ArticleKind.Toggle => pkg.ReadBool(), _ => throw new ArgumentException($"unknown article kind {kind}"), }; } public static int Size(ArticleKind kind, object? value) { return kind switch { ArticleKind.Text => (value as string)?.Length ?? 0, ArticleKind.TextList => (value as List<string>)?.Sum((string s) => s?.Length ?? 0) ?? 0, _ => 4, }; } public static bool Same(object? a, object? b) { if (a is List<string> first) { List<string> second = (b as List<string>) ?? new List<string>(); return first.SequenceEqual<string>(second, StringComparer.Ordinal); } if (b is List<string> list) { if (list.Count == 0) { return a == null; } return false; } return object.Equals(a, b); } private static void WriteList(ZPackage pkg, List<string>? list) { pkg.Write(list?.Count ?? 0); foreach (string item in list ?? new List<string>()) { pkg.Write(item ?? ""); } } private static List<string> ReadList(ZPackage pkg) { int num = pkg.ReadInt(); List<string> list = new List<string>(Math.Max(0, num)); for (int i = 0; i < num; i++) { list.Add(pkg.ReadString()); } return list; } } internal sealed class ChangeRouter { private readonly Charter charter; private readonly Ledger ledger; private readonly Journal journal; private readonly HashSet<ConfigFile> watched = new HashSet<ConfigFile>(); public ChangeRouter(Charter charter, Ledger ledger, Journal journal) { this.charter = charter; this.ledger = ledger; this.journal = journal; } public void Watch(ConfigFile file) { if (watched.Add(file)) { file.SettingChanged += OnSettingChanged; } } private void OnSettingChanged(object sender, SettingChangedEventArgs args) { try { if (!ledger.Applying) { Route(args.ChangedSetting); } } catch (Exception arg) { journal.Error($"handling a change of {args.ChangedSetting.Definition} failed: {arg}"); } } private void Route(ConfigEntryBase entry) { IClause clause = ledger.Find(entry); if (clause == null) { return; } clause.RememberOwn(); if (Side.IsServer) { charter.Publisher.MarkDirty(clause); return; } Receiver receiver = charter.Receiver; if (receiver.Bound && !clause.Local) { if (receiver.Steward) { charter.Amendments.Send(clause); return; } journal.Info(clause.Section + "." + clause.Key + " is bound by the server, reverting to " + clause.AuthorToml); clause.ApplyAuthor(); } } } public sealed class Charter { private bool lastBound; private bool lastSteward; public string Guid { get; } public string Title { get; } public string Version { get; } public string OldestAccepted { get; } public bool Mandatory { get; } public bool IsBound { get { if (!Side.IsServer) { return Receiver.Bound; } return Ledger.BindingOn; } } public bool IsAuthor { get { if (!Side.IsServer) { return !Receiver.Bound; } return true; } } public bool MayAmend { get { if (IsBound) { return IsSteward; } return true; } } public bool IsSteward { get { if (!Side.IsServer) { return Receiver.Steward; } return true; } } public DateTime? LastPush { get { if (!Side.IsServer) { return Receiver.LastPush; } return null; } } public int PushedBytes => Receiver.PushedBytes; public static Verbosity Verbosity { get; set; } = Verbosity.Normal; internal Journal Journal { get; } internal Ledger Ledger { get; } internal Receiver Receiver { get; } internal Courier Courier { get; } internal Publisher Publisher { get; } internal Stewardship Stewardship { get; } internal Amendments Amendments { get; } internal ChangeRouter Router { get; } internal ReadOnlyTags Tags { get; } public event Action<bool>? Pushed; public event Action? StewardChanged; public event Action? BindingChanged; public Charter(string guid, string title, string version, string? oldestAccepted = null, bool mandatory = true) { Guid = guid; Title = title; Version = version; OldestAccepted = oldestAccepted ?? version; Mandatory = mandatory; Journal = new Journal(title); Ledger = new Ledger(); Receiver = new Receiver(this, Ledger, Journal); Courier = new Courier(guid, Journal); Publisher = new Publisher(Ledger, Courier, Journal); Stewardship = new Stewardship(Courier, Publisher); Amendments = new Amendments(this, Ledger, Journal); Router = new ChangeRouter(this, Ledger, Journal); Tags = new ReadOnlyTags(); lastSteward = IsSteward; Enrol(); } public Clause<T> Clause<T>(ConfigEntry<T> entry, bool local = false) { Clause<T> clause = new Clause<T>(entry, local, Ledger); Ledger.Add(clause); Router.Watch(((ConfigEntryBase)entry).ConfigFile); return clause; } public void Binding(ConfigEntry<bool> entry) { Clause<bool> clause = Clause<bool>(entry); Ledger.SetBinding(clause, () => entry.Value); } public static void Install(Harmony harmony) { GameHooks.Install(harmony); } internal void Attach(ZNetPeer peer, bool server) { if (server) { peer.m_rpc.Register<ZPackage>("Charter_" + Guid + "_Amend", (Action<ZRpc, ZPackage>)Amendments.OnAmend); } else { peer.m_rpc.Register<ZPackage>("Charter_" + Guid + "_Push", (Action<ZRpc, ZPackage>)Receiver.OnPush); } } internal void Tick(float now) { Publisher.Flush(); Courier.Tick(); Stewardship.Tick(now); Receiver.Poll(); NoteChanges(); } internal void AfterPush(bool first) { NoteChanges(); try { this.Pushed?.Invoke(first); } catch (Exception arg) { Journal.Error($"a Pushed handler threw: {arg}"); } } internal void AfterSessionEnd() { NoteChanges(); } private void Enrol() { GameHooks.Track(this); Family.Register(new FamilyEntry(Guid, Title, Version, OldestAccepted, Mandatory)); Family.SetStatus(Guid, () => Reports.Status(this)); Family.SetDiff(Guid, () => Reports.Diff(this)); Family.SetVerbosity(Guid, delegate(int level) { Verbosity = (Verbosity)level; }); } private void NoteChanges() { bool isBound = IsBound; bool isSteward = IsSteward; Tags.Update(isBound && !isSteward, Ledger.Pushable); if (isBound != lastBound) { lastBound = isBound; Raise(this.BindingChanged, "BindingChanged"); } if (isSteward != lastSteward) { lastSteward = isSteward; Raise(this.StewardChanged, "StewardChanged"); } } private void Raise(Action? handler, string name) { try { handler?.Invoke(); } catch (Exception arg) { Journal.Error($"a {name} handler threw: {arg}"); } } } public sealed class Clause<T> : IClause { private readonly Ledger ledger; private T own; private T author; public ConfigEntry<T> Entry { get; } public bool Local { get; } public T AuthorValue { get { if (!Side.IsServer) { return author; } return Entry.Value; } } public T OwnValue => own; ConfigEntryBase IClause.Entry => (ConfigEntryBase)(object)Entry; string IClause.Section => ((ConfigEntryBase)Entry).Definition.Section; string IClause.Key => ((ConfigEntryBase)Entry).Definition.Key; public string AuthorToml { get; private set; } string IClause.OwnToml => TomlTypeConverter.ConvertToString((object)own, typeof(T)); string IClause.CurrentToml => ((ConfigEntryBase)Entry).GetSerializedValue(); internal Clause(ConfigEntry<T> entry, bool local, Ledger ledger) { Entry = entry; Local = local; this.ledger = ledger; own = entry.Value; author = default(T); AuthorToml = ""; } void IClause.RememberOwn() { own = Entry.Value; } bool IClause.TakeAuthor(string toml) { try { author = TomlTypeConverter.ConvertToValue<T>(toml); AuthorToml = toml; return true; } catch (Exception) { return false; } } void IClause.ApplyAuthor() { ledger.Quietly((ConfigEntryBase)(object)Entry, delegate { Entry.Value = author; }); } void IClause.RestoreOwn() { ledger.Quietly((ConfigEntryBase)(obj