Decompiled source of ForgeKit v0.4.10
plugins/ForgeKit.dll
Decompiled 6 days 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.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using NodeCanvas.DialogueTrees; using UnityEngine; using UnityEngine.AI; using UnityEngine.SceneManagement; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("ForgeKit")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.4.10.0")] [assembly: AssemblyInformationalVersion("0.4.10+83254ebc1e0f377c73c2296a9412cdb0c1c00f84")] [assembly: AssemblyProduct("ForgeKit")] [assembly: AssemblyTitle("ForgeKit")] [assembly: AssemblyMetadata("BuildStamp", "83254ebc 2026-08-28")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.0.0.0")] [module: UnverifiableCode] namespace ForgeKit; public static class BuildScenes { public static string Resolve(string want, out List<string> similar) { List<string> list = new List<string>(); int sceneCountInBuildSettings = SceneManager.sceneCountInBuildSettings; for (int i = 0; i < sceneCountInBuildSettings; i++) { string scenePathByBuildIndex = SceneUtility.GetScenePathByBuildIndex(i); string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(scenePathByBuildIndex); if (string.Equals(fileNameWithoutExtension, want, StringComparison.OrdinalIgnoreCase)) { similar = new List<string>(); return fileNameWithoutExtension; } list.Add(fileNameWithoutExtension); } similar = Suggest.Bidirectional(list, want); return null; } public static void Dump(ManualLogSource log) { int sceneCountInBuildSettings = SceneManager.sceneCountInBuildSettings; log.LogMessage((object)$"[SCENEDUMP] {sceneCountInBuildSettings} scenes in build settings."); for (int i = 0; i < sceneCountInBuildSettings; i++) { string scenePathByBuildIndex = SceneUtility.GetScenePathByBuildIndex(i); log.LogMessage((object)$"[SCENEDUMP] {i}: {scenePathByBuildIndex}"); } } } public static class BuildStamp { public const string Unknown = "unknown"; private const string Key = "BuildStamp"; private static string _local; public static string Local => _local ?? (_local = Read(typeof(BuildStamp).Assembly)); public static string Read(Assembly asm) { if (asm == null) { return "unknown"; } try { foreach (AssemblyMetadataAttribute customAttribute in asm.GetCustomAttributes<AssemblyMetadataAttribute>()) { if (customAttribute.Key == "BuildStamp" && !string.IsNullOrEmpty(customAttribute.Value)) { return customAttribute.Value; } } } catch { } return "unknown"; } } public sealed class CatalogInfo { public string ModGuid; public string ModName; public string ModVersion; public Func<ConfigFile> ConfigSource; } public static class CatalogDump { public const int SchemaVersion = 1; private static readonly Dictionary<string, Action> _writers = new Dictionary<string, Action>(); public static string Dir => Path.Combine(Paths.BepInExRootPath, "forge_catalog"); public static void Announce(CatalogInfo info, CommandRegistry registry, string channelFile, bool firstLineOnly, float pollSeconds) { if (info == null || string.IsNullOrEmpty(info.ModGuid) || registry == null) { return; } lock (_writers) { _writers[info.ModGuid] = delegate { Write(info, registry, channelFile, firstLineOnly, pollSeconds); }; } } public static void Refresh(string guid) { Action value; lock (_writers) { _writers.TryGetValue(guid, out value); } try { value?.Invoke(); } catch { } } public static void RefreshAll() { List<Action> list; lock (_writers) { list = new List<Action>(_writers.Values); } foreach (Action item in list) { try { item(); } catch { } } } private static void Write(CatalogInfo info, CommandRegistry registry, string channelFile, bool firstLineOnly, float pollSeconds) { ForgeJson forgeJson = new ForgeJson(); forgeJson.BeginObj(); forgeJson.Prop("schema", 1L); forgeJson.Name("mod").BeginObj().Prop("guid", info.ModGuid) .Prop("name", info.ModName) .Prop("version", info.ModVersion) .EndObj(); forgeJson.Name("channel").BeginObj().Prop("file", channelFile) .Prop("firstLineOnly", firstLineOnly) .Name("pollSeconds") .Value(pollSeconds) .Prop("responseProtocol", (!firstLineOnly) ? 1 : 0) .EndObj(); forgeJson.Prop("generatedUtc", DateTime.UtcNow.ToString("o")); forgeJson.Name("verbs").BeginArr(); foreach (VerbSpec spec in registry.Specs) { forgeJson.BeginObj(); forgeJson.Prop("verbs", spec.Verbs); forgeJson.Prop("help", spec.Help); if (!string.IsNullOrEmpty(spec.Tag)) { forgeJson.Prop("tag", spec.Tag); } forgeJson.Prop("needsPlayer", spec.NeedsPlayer); if (spec.MasterOnly) { forgeJson.Prop("masterOnly", v: true); } ArgSpec[] array = spec.Args; bool flag = false; if (array == null) { try { array = UsageSpec.Derive(spec.Verbs, spec.Help); } catch { } flag = array != null; } if (array != null) { if (flag) { forgeJson.Prop("argsDerived", v: true); } forgeJson.Name("args").BeginArr(); ArgSpec[] array2 = array; foreach (ArgSpec argSpec in array2) { forgeJson.BeginObj(); forgeJson.Prop("name", argSpec.Name); forgeJson.Prop("type", argSpec.Type); if (argSpec.Optional) { forgeJson.Prop("optional", v: true); } if (argSpec.Choices != null) { forgeJson.Prop("choices", argSpec.Choices); } if (argSpec.Default != null) { forgeJson.Prop("default", argSpec.Default); } forgeJson.EndObj(); } forgeJson.EndArr(); } forgeJson.EndObj(); } forgeJson.EndArr(); ConfigFile val = null; try { val = info.ConfigSource?.Invoke(); } catch { } forgeJson.Name("config").BeginArr(); if (val != null) { List<ConfigDefinition> list = new List<ConfigDefinition>(val.Keys); foreach (ConfigDefinition item in list) { ConfigEntryBase val2; try { val2 = val[item]; } catch { continue; } forgeJson.BeginObj(); forgeJson.Prop("section", item.Section); forgeJson.Prop("key", item.Key); forgeJson.Prop("type", val2.SettingType.Name); string v = null; string v2 = null; try { v = val2.GetSerializedValue(); } catch { } try { v2 = TomlTypeConverter.ConvertToString(val2.DefaultValue, val2.SettingType); } catch { } forgeJson.Prop("value", v); forgeJson.Prop("default", v2); ConfigDescription description = val2.Description; string text = ((description != null) ? description.Description : null); if (!string.IsNullOrEmpty(text)) { forgeJson.Prop("description", text); } if (val2.SettingType.IsEnum) { forgeJson.Prop("choices", Enum.GetNames(val2.SettingType)); } else { ConfigDescription description2 = val2.Description; if (((description2 != null) ? description2.AcceptableValues : null) != null) { string text2 = null; try { text2 = val2.Description.AcceptableValues.ToDescriptionString(); } catch { } if (!string.IsNullOrEmpty(text2)) { forgeJson.Prop("acceptable", text2.TrimStart('#', ' ')); } } } forgeJson.EndObj(); } } forgeJson.EndArr(); forgeJson.Name("customItems").BeginArr(); foreach (KeyValuePair<string, int> item2 in ItemNameIndex.CustomSnapshot()) { forgeJson.BeginObj().Prop("name", item2.Key).Prop("id", item2.Value) .EndObj(); } forgeJson.EndArr(); forgeJson.EndObj(); Directory.CreateDirectory(Dir); string text3 = Path.Combine(Dir, SafeFileName(info.ModGuid) + ".json"); string text4 = text3 + ".tmp"; File.WriteAllText(text4, forgeJson.ToString()); if (File.Exists(text3)) { File.Delete(text3); } File.Move(text4, text3); } private static string SafeFileName(string s) { char[] invalidFileNameChars = Path.GetInvalidFileNameChars(); StringBuilder stringBuilder = new StringBuilder(s.Length); foreach (char c in s) { stringBuilder.Append((Array.IndexOf(invalidFileNameChars, c) >= 0) ? '_' : c); } return stringBuilder.ToString(); } } public static class CfgSkew { public static List<CfgSkewRules.Row> Drifted(ConfigFile cfg, string sectionFilter, out int total) { List<CfgSkewRules.Row> list = new List<CfgSkewRules.Row>(); total = 0; if (cfg == null) { return list; } foreach (ConfigDefinition item in new List<ConfigDefinition>(cfg.Keys)) { if (DevCfg.SectionMatches(sectionFilter, item.Section) || DevCfg.MatchKey(sectionFilter, item.Section, item.Key)) { total++; ConfigEntryBase val = cfg[item]; if (DevCfg.Skew(val, out var shippedDefault)) { list.Add(new CfgSkewRules.Row(item.Section, item.Key, val.GetSerializedValue(), shippedDefault)); } } } return list; } public static List<string> Sweep() { List<string> list = new List<string>(); foreach (KeyValuePair<string, PluginInfo> pluginInfo in Chainloader.PluginInfos) { PluginInfo value = pluginInfo.Value; if (value == null || pluginInfo.Key == null || !pluginInfo.Key.StartsWith("cobalt.", StringComparison.Ordinal)) { continue; } BaseUnityPlugin instance = value.Instance; if ((Object)(object)instance == (Object)null) { continue; } ConfigFile config; try { config = instance.Config; } catch { continue; } if (config != null) { int total; List<CfgSkewRules.Row> drifted = Drifted(config, null, out total); BepInPlugin metadata = value.Metadata; string text = CfgSkewRules.BootLine(((metadata != null) ? metadata.Name : null) ?? pluginInfo.Key, total, drifted); if (text != null) { list.Add(text); } } } return list; } } public static class CfgSkewRules { public readonly struct Row { public readonly string Section; public readonly string Key; public readonly string Live; public readonly string Default; public Row(string section, string key, string live, string def) { Section = section; Key = key; Live = live; Default = def; } } public const string Tag = "[CFGSKEW]"; public const int MaxKeys = 8; public static bool Differs(string live, string def) { if (live != null && def != null) { return !string.Equals(live, def, StringComparison.Ordinal); } return false; } public static string Describe(Row r) { return r.Section + "." + r.Key + "=" + r.Live + " (default " + r.Default + ")"; } public static string BootLine(string mod, int total, IList<Row> drifted, int maxKeys = 8) { if (drifted == null || drifted.Count == 0) { return null; } if (maxKeys < 1) { maxKeys = 1; } StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("[CFGSKEW]").Append(' ').Append(string.IsNullOrEmpty(mod) ? "?" : mod) .Append(": ") .Append(drifted.Count) .Append(" of ") .Append(total) .Append((total == 1) ? " entry differs" : " entries differ") .Append(" from shipped defaults — "); int num = Math.Min(maxKeys, drifted.Count); for (int i = 0; i < num; i++) { if (i > 0) { stringBuilder.Append(", "); } stringBuilder.Append(Describe(drifted[i])); } if (drifted.Count > num) { stringBuilder.Append(", …and ").Append(drifted.Count - num).Append(" more"); } return stringBuilder.ToString(); } } public sealed class CommandChannel { private readonly string _path; private readonly ManualLogSource _log; private readonly CommandRegistry _registry; private readonly float _pollSeconds; private readonly bool _allLines; private readonly ScriptRunner _runner; private float _check; private long _stamp; private long _length = -1L; private readonly CatalogInfo _catalog; private int _catalogWrites; private float _bootTime = -1f; private ForgeOut.Capture _pendingCap; private int _pendingVerbs; private float _pendingDeadline; private const float PendingCaptureMaxSeconds = 120f; public string Path => _path; public CommandChannel(string fileName, ManualLogSource log, CommandRegistry registry, float pollSeconds, bool allLines, bool primeStamp) : this(fileName, log, registry, pollSeconds, allLines, primeStamp, null) { } public CommandChannel(string fileName, ManualLogSource log, CommandRegistry registry, float pollSeconds = 0.5f, bool allLines = true, bool primeStamp = false, CatalogInfo catalog = null) { _catalog = catalog; if (catalog != null) { CatalogDump.Announce(catalog, registry, fileName, !allLines, pollSeconds); ForgeOut.PruneOld(); } _path = System.IO.Path.Combine(Paths.ConfigPath, fileName); _log = log; _registry = registry; _pollSeconds = pollSeconds; _allLines = allLines; if (primeStamp && File.Exists(_path)) { _stamp = File.GetLastWriteTimeUtc(_path).Ticks; _length = new FileInfo(_path).Length; } _runner = new ScriptRunner(log, Run); _registry.Register("script", "Run several verbs with real time between them ('script moveto Bandit 2 ; wait 1 ; swing'; timing: wait <s> | waitframes <n> | waitloaded [s]); one step per frame; a new script replaces the running one.", delegate(string[] args) { _runner.Start(Tail(args)); }); _registry.Register("scriptcancel", "Abort the running script (if any).", delegate { _runner.Cancel(); }); _registry.Register("scriptstatus", "One line: which step the running script is on.", delegate { _runner.Status(); }); } private static string Tail(string[] parts) { if (parts != null && parts.Length >= 2) { return string.Join(" ", parts, 1, parts.Length - 1); } return ""; } private static bool IsScriptControl(string cmd) { int num = cmd.IndexOf(' '); string a = ((num < 0) ? cmd : cmd.Substring(0, num)); if (!string.Equals(a, "scriptcancel", StringComparison.OrdinalIgnoreCase)) { return string.Equals(a, "scriptstatus", StringComparison.OrdinalIgnoreCase); } return true; } public void Tick() { _runner.Pump(); if (_pendingCap != null) { bool flag = Time.unscaledTime >= _pendingDeadline; bool flag2 = _runner.IsRunning || DeferredReport.IsPending; if (!flag2 || flag) { if (flag && flag2) { _log.LogWarning((object)($"[CMD] script response closed after {120f:F0}s " + "while the script is STILL running (or a deferred verb report is still owed) — the rest of its output goes to the log only. (Capture released so the channel isn't wedged; 'scriptstatus' still works.)")); } _pendingCap.Finish(_pendingVerbs); _pendingCap = null; _pendingVerbs = 0; } } if (_catalog != null && _catalogWrites < 2) { if (_bootTime < 0f) { _bootTime = Time.unscaledTime; } if (_catalogWrites == 0) { _catalogWrites = 1; CatalogDump.Refresh(_catalog.ModGuid); } else if (Time.unscaledTime - _bootTime > 60f) { _catalogWrites = 2; CatalogDump.Refresh(_catalog.ModGuid); } } if (!(Time.unscaledTime - _check <= _pollSeconds)) { _check = Time.unscaledTime; Poll(); } } private void Poll() { try { if (!File.Exists(_path)) { return; } long ticks = File.GetLastWriteTimeUtc(_path).Ticks; long length = new FileInfo(_path).Length; if (ticks == _stamp && length == _length) { return; } string[] array = File.ReadAllLines(_path); _stamp = ticks; _length = length; if (_allLines) { string text = ForgeOut.ParseReqId(array); if (_pendingCap != null) { ForgeOut.Capture capture = ((text != null) ? ForgeOut.Begin(_log, text) : null); int num = 0; string[] array2 = array; foreach (string text2 in array2) { string text3 = text2.Trim(); if (text3.Length != 0 && !text3.StartsWith("#") && IsScriptControl(text3)) { Run(text3); num++; } } if (num == 0) { _pendingCap.Suspend(); try { _log.LogWarning((object)"[CMD] refused: a script batch is still running (its response file is still open). Use scriptcancel, or wait for it to finish."); } finally { _pendingCap.Resume(); } } capture?.Finish(num); return; } ForgeOut.Capture capture2 = ((text != null) ? ForgeOut.Begin(_log, text) : null); int num2 = 0; try { string[] array3 = array; foreach (string text4 in array3) { string text5 = text4.Trim(); if (text5.Length != 0 && !text5.StartsWith("#")) { Run(text5); num2++; } } return; } finally { if (capture2 != null && (_runner.IsRunning || DeferredReport.IsPending)) { _pendingCap = capture2; _pendingVerbs = num2; _pendingDeadline = Time.unscaledTime + 120f; } else { capture2?.Finish(num2); } } } string text6 = null; string[] array4 = array; foreach (string text7 in array4) { if (!string.IsNullOrWhiteSpace(text7)) { text6 = text7; break; } } if (!string.IsNullOrWhiteSpace(text6)) { Run(text6.Trim()); } } catch (Exception ex) { _log.LogWarning((object)("[CMD] poll error: " + ex.Message)); } } public void Run(string cmd) { using (ModLog.Requested()) { _log.LogMessage((object)("[CMD] run: " + cmd)); string[] array = cmd.Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries); string verb = ((array.Length != 0) ? array[0] : ""); if (_registry.TryGet(verb, out var run)) { run(array); } else { _registry.PrintHelp(verb); } } } } public sealed class CommandRegistry { private readonly ManualLogSource _log; private readonly Dictionary<string, Action<string[]>> _run = new Dictionary<string, Action<string[]>>(StringComparer.OrdinalIgnoreCase); private readonly List<VerbSpec> _specs = new List<VerbSpec>(); public IReadOnlyList<VerbSpec> Specs => _specs; public CommandRegistry(ManualLogSource log) { _log = log; } public void Register(string verb, string help, Action<string[]> run) { Register(new string[1] { verb }, help, run); } public void Register(string[] verbs, string help, Action<string[]> run) { Register(new VerbSpec { Verbs = verbs, Help = help }, run); } public void Register(VerbSpec spec, Action<string[]> run) { string[] verbs = spec.Verbs; string help = spec.Help; string[] array = verbs; foreach (string v in array) { if (_run.ContainsKey(v)) { _log.LogWarning((object)("[CMD] verb '" + v + "' registered twice — the later registration wins (drop the local copy, or Exclude it from the shared pack).")); } _run[v] = delegate(string[] args) { try { run(args); } catch (Exception arg) { _log.LogError((object)$"[CMD] '{v}' failed: {arg}"); } }; } _specs.Add(spec); } public bool TryGet(string verb, out Action<string[]> run) { return _run.TryGetValue(verb, out run); } public void PrintHelp(string verb) { if (string.IsNullOrEmpty(verb) || string.Equals(verb, "help", StringComparison.OrdinalIgnoreCase)) { _log.LogMessage((object)$"[CMD] {_specs.Count} registered verbs:"); } else { _log.LogWarning((object)$"[CMD] unknown verb '{verb}' — {_specs.Count} registered verbs:"); } foreach (VerbSpec spec in _specs) { _log.LogMessage((object)("[CMD] " + spec.JoinedVerbs + " — " + spec.Help)); } } } public static class Inventories { public readonly struct ConsumeResult { public readonly int Requested; public readonly int Before; public readonly int After; public readonly int Shortfall; public readonly bool Emptied; public readonly bool DestroyRequested; public readonly bool OnNonMasterClient; public int EffectiveAfter { get { if (!Emptied) { return After; } return 0; } } public bool Consumed { get { if (Shortfall == 0) { if (!Emptied) { return After == Before - Requested; } return true; } return false; } } public ConsumeResult(int requested, int before, int after, int shortfall, bool emptied, bool destroyRequested, bool onNonMasterClient) { Requested = requested; Before = before; After = after; Shortfall = shortfall; Emptied = emptied; DestroyRequested = destroyRequested; OnNonMasterClient = onNonMasterClient; } public string Describe() { return $"requested={Requested} before={Before} after={After} shortfall={Shortfall} " + $"emptied={Emptied} destroyRequested={DestroyRequested} nonMaster={OnNonMasterClient}"; } } public static IEnumerable<Item> All(Character player) { foreach (var item in AllByContainer(player)) { yield return item.Item; } } public static IEnumerable<(Item Item, string Where)> AllByContainer(Character player) { CharacterInventory val = (((Object)(object)player != (Object)null) ? player.Inventory : null); if ((Object)(object)val == (Object)null) { yield break; } HashSet<Item> seen = new HashSet<Item>(); Bag bag = val.EquippedBag; Item[] componentsInChildren; if ((Object)(object)val.Pouch != (Object)null) { componentsInChildren = ((Component)val.Pouch).GetComponentsInChildren<Item>(true); foreach (Item val2 in componentsInChildren) { if ((Object)(object)val2 != (Object)null && (object)val2 != bag && val2.RemainingAmount > 0 && !val2.DestroyWanted && seen.Add(val2)) { yield return (Item: val2, Where: "pouch"); } } } if (!((Object)(object)bag != (Object)null)) { yield break; } componentsInChildren = ((Component)bag).GetComponentsInChildren<Item>(true); foreach (Item val3 in componentsInChildren) { if ((Object)(object)val3 != (Object)null && (object)val3 != bag && val3.RemainingAmount > 0 && !val3.DestroyWanted && seen.Add(val3)) { yield return (Item: val3, Where: "bag"); } } } public static ConsumeResult ConsumeOne(Item item, int qty = 1) { int remainingAmount = item.RemainingAmount; if (qty <= 0) { return new ConsumeResult(qty, remainingAmount, remainingAmount, 0, emptied: false, destroyRequested: false, PhotonNetwork.isNonMasterClientInRoom); } int shortfall = item.RemoveQuantity(qty); bool isNonMasterClientInRoom = PhotonNetwork.isNonMasterClientInRoom; bool destroyRequested = false; if (isNonMasterClientInRoom && (!item.HasMultipleUses || item.RemainingAmount <= 0) && (Object)(object)ItemManager.Instance != (Object)null) { ItemManager.Instance.SendDestroyItem(item.UID); item.SetDestroyWanted(); destroyRequested = true; } return new ConsumeResult(qty, remainingAmount, item.RemainingAmount, shortfall, remainingAmount <= qty, destroyRequested, isNonMasterClientInRoom); } } public static class Converge { public static bool To<T>(Func<T> read, T want, Action<T> write, Func<T, T, bool> same = null) { if (read == null || write == null) { return false; } T val = read(); if (same?.Invoke(val, want) ?? EqualityComparer<T>.Default.Equals(val, want)) { return false; } write(want); return true; } } public sealed class Stamp<TTarget, TValue> where TTarget : class { private readonly Func<TTarget, bool> _alive; private readonly Func<TTarget, TValue> _read; private readonly Action<TTarget, TValue> _write; private readonly TValue _neutral; private readonly Func<TValue, bool> _wantsNothing; private readonly Func<TValue, TValue, bool> _same; private TTarget _target; private TValue _value; private bool _warnedForeign; public Action<TTarget, TValue, TValue> OnForeignWriter; public Action<TTarget, TValue> OnStamp; public Action<TTarget, TValue, string> OnWithdraw; public string TargetLostReason = "no longer the stamped target"; public string NothingWantedReason = "nothing is wanted right now"; public TTarget Target => _target; public TValue Value => _value; public Stamp(Func<TTarget, bool> alive, Func<TTarget, TValue> read, Action<TTarget, TValue> write, TValue neutral, Func<TValue, bool> wantsNothing = null, Func<TValue, TValue, bool> same = null) { Stamp<TTarget, TValue> stamp = this; _alive = alive ?? throw new ArgumentNullException("alive"); _read = read ?? throw new ArgumentNullException("read"); _write = write ?? throw new ArgumentNullException("write"); _neutral = neutral; _same = same ?? new Func<TValue, TValue, bool>(EqualityComparer<TValue>.Default.Equals); _wantsNothing = wantsNothing ?? ((Func<TValue, bool>)((TValue v) => stamp._same(v, neutral))); } public void Sync(TTarget target, TValue want) { if (_target != null && _target != target) { Clear(TargetLostReason); } if (target == null || !_alive(target)) { return; } TValue val = _read(target); if (target == _target && !_same(val, _value) && !_warnedForeign) { _warnedForeign = true; OnForeignWriter?.Invoke(target, val, _value); } if (_wantsNothing(want)) { if (target == _target) { Clear(NothingWantedReason); } return; } bool flag = false; if (!_same(val, want)) { _write.Invoke(target, want); OnStamp?.Invoke(target, want); flag = true; } if (flag || target == _target) { _target = target; _value = want; } } public void Clear(string why) { if (_target != null && _alive(_target) && !_same(_read(_target), _neutral)) { _write.Invoke(_target, _neutral); OnWithdraw?.Invoke(_target, _value, why); } _target = null; _value = _neutral; } public void ResetForeignWarning() { _warnedForeign = false; } } public interface ITableSource<TTable> where TTable : class { TTable Table { get; } TTable Reload(); } public sealed class DelegateTableSource<TTable> : ITableSource<TTable> where TTable : class { private readonly Func<TTable> _load; private readonly Action<TTable> _announceReload; private TTable _table; public TTable Table { get { if (_table == null) { _table = _load(); } return _table; } } public DelegateTableSource(Func<TTable> load, Action<TTable> announceReload = null) { _load = load ?? throw new ArgumentNullException("load"); _announceReload = announceReload; } public TTable Reload() { _table = _load(); _announceReload?.Invoke(_table); return _table; } } public sealed class DataAxis<TTable> where TTable : class { private readonly ITableSource<TTable> _source; private readonly Action<Action> _bootValidateHook; private readonly Func<bool> _registryReady; private readonly Action _validate; public TTable Table => _source.Table; public DataAxis(ITableSource<TTable> source, Action<Action> bootValidateHook, Func<bool> registryReady, Action validate) { _source = source ?? throw new ArgumentNullException("source"); if (validate != null && (bootValidateHook == null || registryReady == null)) { throw new ArgumentException("a DataAxis validate requires BOTH a boot hook and a registry gate — the gate-less boot check is the exact bug class this type exists to prevent."); } _bootValidateHook = bootValidateHook; _registryReady = registryReady; _validate = validate; } public void Init() { if (_validate != null) { _bootValidateHook(Validate); } } public TTable Reload() { TTable result = _source.Reload(); if (_validate != null) { Validate(); } return result; } public void Validate() { if (_validate != null && _registryReady()) { _validate(); } } } public static class DeferredReport { private static readonly PendingSet _pending = new PendingSet(); private static readonly DateTime _epoch = DateTime.UtcNow; private static double Now => (DateTime.UtcNow - _epoch).TotalSeconds; public static bool IsPending => _pending.AnyAt(Now); public static IEnumerator Wrap(IEnumerator body, ManualLogSource log, string what) { long id = _pending.Open(Now); return Drive(body, log, what, id); } private static IEnumerator Drive(IEnumerator body, ManualLogSource log, string what, long id) { try { while (true) { object current; try { if (!body.MoveNext()) { break; } current = body.Current; } catch (Exception ex) { log.LogWarning((object)("[CMD] deferred report '" + what + "' threw: " + ex.Message + " — no contract line for this call.")); break; } yield return current; } } finally { _pending.Close(id); } } } public static class EmbeddedRes { public static string Text(Assembly asm, string suffix, string logTag, ManualLogSource log = null) { string text = Array.Find(asm.GetManifestResourceNames(), (string n) => n.EndsWith(suffix, StringComparison.OrdinalIgnoreCase)); if (text == null) { ManualLogSource obj = log ?? Plugin.Log; if (obj != null) { obj.LogWarning((object)(logTag + " embedded " + suffix + " not found.")); } return ""; } using Stream stream = asm.GetManifestResourceStream(text); using StreamReader streamReader = new StreamReader(stream); return streamReader.ReadToEnd(); } public static Texture2D Texture(Assembly asm, string suffix, string logTag, ManualLogSource log = null) { //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_00a2: Expected O, but got Unknown ManualLogSource val = log ?? Plugin.Log; try { string text = Array.Find(asm.GetManifestResourceNames(), (string n) => n.EndsWith(suffix, StringComparison.OrdinalIgnoreCase)); if (text == null) { if (val != null) { val.LogWarning((object)(logTag + " embedded icon '" + suffix + "' missing from the DLL — keeping the donor icon.")); } return null; } byte[] array; using (Stream stream = asm.GetManifestResourceStream(text)) { using MemoryStream memoryStream = new MemoryStream(); stream.CopyTo(memoryStream); array = memoryStream.ToArray(); } Texture2D val2 = new Texture2D(2, 2, (TextureFormat)4, false); if (!ImageConversion.LoadImage(val2, array)) { if (val != null) { val.LogWarning((object)(logTag + " icon '" + suffix + "' failed to decode.")); } return null; } ((Object)val2).hideFlags = (HideFlags)61; return val2; } catch (Exception ex) { if (val != null) { val.LogWarning((object)(logTag + " icon '" + suffix + "' load failed: " + ex.Message)); } return null; } } } public sealed class ForgeJson { private readonly StringBuilder _sb = new StringBuilder(4096); private bool _needComma; public override string ToString() { return _sb.ToString(); } private void Sep() { if (_needComma) { _sb.Append(','); } _needComma = false; } public ForgeJson BeginObj() { Sep(); _sb.Append('{'); return this; } public ForgeJson EndObj() { _sb.Append('}'); _needComma = true; return this; } public ForgeJson BeginArr() { Sep(); _sb.Append('['); return this; } public ForgeJson EndArr() { _sb.Append(']'); _needComma = true; return this; } public ForgeJson Name(string name) { Sep(); WriteString(name); _sb.Append(':'); _needComma = false; return this; } public ForgeJson Value(string v) { Sep(); if (v == null) { _sb.Append("null"); } else { WriteString(v); } _needComma = true; return this; } public ForgeJson Value(bool v) { Sep(); _sb.Append(v ? "true" : "false"); _needComma = true; return this; } public ForgeJson Value(long v) { Sep(); _sb.Append(v.ToString(CultureInfo.InvariantCulture)); _needComma = true; return this; } public ForgeJson Value(double v) { Sep(); if (double.IsNaN(v) || double.IsInfinity(v)) { _sb.Append("null"); } else { _sb.Append(v.ToString("R", CultureInfo.InvariantCulture)); } _needComma = true; return this; } public ForgeJson Prop(string name, string v) { return Name(name).Value(v); } public ForgeJson Prop(string name, bool v) { return Name(name).Value(v); } public ForgeJson Prop(string name, long v) { return Name(name).Value(v); } public ForgeJson Prop(string name, string[] items) { Name(name).BeginArr(); if (items != null) { foreach (string v in items) { Value(v); } } return EndArr(); } private void WriteString(string s) { _sb.Append('"'); foreach (char c in s) { switch (c) { case '"': _sb.Append("\\\""); continue; case '\\': _sb.Append("\\\\"); continue; case '\b': _sb.Append("\\b"); continue; case '\f': _sb.Append("\\f"); continue; case '\n': _sb.Append("\\n"); continue; case '\r': _sb.Append("\\r"); continue; case '\t': _sb.Append("\\t"); continue; } if (c < ' ') { StringBuilder stringBuilder = _sb.Append("\\u"); int num = c; stringBuilder.Append(num.ToString("x4", CultureInfo.InvariantCulture)); } else { _sb.Append(c); } } _sb.Append('"'); } } public static class ForgeOut { public sealed class Capture : ILogListener, IDisposable { private readonly ILogSource _source; private readonly string _reqid; private readonly List<string> _lines = new List<string>(); private bool _done; private bool _suspended; public void Suspend() { _suspended = true; } public void Resume() { _suspended = false; } internal Capture(ManualLogSource source, string reqid) { _source = (ILogSource)(object)source; _reqid = reqid; Logger.Listeners.Add((ILogListener)(object)this); } public void LogEvent(object sender, LogEventArgs e) { //IL_0035: Unknown result type (might be due to invalid IL or missing references) if (_suspended || e.Source != _source) { return; } lock (_lines) { _lines.Add($"[{e.Level,-7}] {e.Data}"); } } public void Finish(int verbsRun) { if (_done) { return; } _done = true; Logger.Listeners.Remove((ILogListener)(object)this); try { Directory.CreateDirectory(Dir); StringBuilder stringBuilder = new StringBuilder(); lock (_lines) { foreach (string line in _lines) { stringBuilder.AppendLine(line); } } stringBuilder.AppendLine($"#forge done reqid={_reqid} verbs={verbsRun}"); string text = Path.Combine(Dir, _reqid + ".txt"); string text2 = text + ".tmp"; File.WriteAllText(text2, stringBuilder.ToString()); if (File.Exists(text)) { try { File.Replace(text2, text, null); return; } catch (IOException) { File.Delete(text); File.Move(text2, text); return; } catch (PlatformNotSupportedException) { File.Delete(text); File.Move(text2, text); return; } } File.Move(text2, text); } catch { } } public void Dispose() { Finish(0); } } public static string Dir => Path.Combine(Paths.BepInExRootPath, "forge_out"); public static string ParseReqId(string[] lines) { if (lines == null) { return null; } foreach (string text in lines) { if (string.IsNullOrWhiteSpace(text)) { continue; } string text2 = text.Trim(); if (!text2.StartsWith("#forge reqid=", StringComparison.OrdinalIgnoreCase)) { return null; } string text3 = text2.Substring("#forge reqid=".Length).Trim(); if (text3.Length == 0 || text3.Length > 64) { return null; } string text4 = text3; foreach (char c in text4) { if (!char.IsLetterOrDigit(c) && c != '-' && c != '_') { return null; } } return text3; } return null; } public static Capture Begin(ManualLogSource source, string reqid) { return new Capture(source, reqid); } public static void PruneOld() { try { if (!Directory.Exists(Dir)) { return; } DateTime dateTime = DateTime.UtcNow.AddHours(-24.0); string[] files = Directory.GetFiles(Dir, "*.txt"); foreach (string path in files) { try { if (File.GetLastWriteTimeUtc(path) < dateTime) { File.Delete(path); } } catch { } } } catch { } } } public static class IdPool { private static readonly Dictionary<int, string> _claimed = new Dictionary<int, string>(); public static readonly int[][] Ranges = new int[1][] { new int[2] { 87000, 87999 } }; public static readonly int[][] Forbidden = new int[1][] { new int[2] { 91007000, 91009999 } }; public static bool InPool(int id) { int[][] ranges = Ranges; foreach (int[] array in ranges) { if (id >= array[0] && id <= array[1]) { return true; } } return false; } public static bool IsForbidden(int id) { int[][] forbidden = Forbidden; foreach (int[] array in forbidden) { if (id >= array[0] && id <= array[1]) { return true; } } return false; } public static bool Claim(int id, string what, Action<string> refuse) { if (IsForbidden(id)) { refuse?.Invoke($"[IDS] {what}: id {id} is from the RETIRED block — those numbers were " + "re-minted and are dead. Not registering it."); return false; } if (!InPool(id)) { refuse?.Invoke($"[IDS] {what}: id {id} is outside the range allocated to these mods " + "(" + RangeText() + "). Registering it would collide with whatever mod DOES own that number, in a save that has already written it down. Not registering it."); return false; } if (_claimed.TryGetValue(id, out var value) && value != what) { refuse?.Invoke($"[IDS] {what}: id {id} was already registered this session by '{value}'. " + "Two things cannot share one id. Not registering the second."); return false; } _claimed[id] = what; return true; } public static bool ClaimShared(int id, string what, Action<string> warn) { if (IsForbidden(id)) { warn?.Invoke($"[IDS] {what}: id {id} is from a retired block — not registering it."); return false; } if (_claimed.TryGetValue(id, out var value) && value != what) { warn?.Invoke($"[IDS] {what}: id {id} was already registered this session by '{value}'. " + "Two things cannot share one id. Not registering the second."); return false; } if (!InPool(id)) { warn?.Invoke($"[IDS] {what}: id {id} is outside this workspace's allocation ({RangeText()}) " + "— assuming it comes from your own. Registering it."); } _claimed[id] = what; return true; } public static IEnumerable<string> Census() { yield return $"[IDS] pool {RangeText()} — {_claimed.Count} id(s) claimed this session"; foreach (KeyValuePair<int, string> item in _claimed) { yield return $"[IDS] {item.Key} {item.Value}"; } } public static void Reset() { _claimed.Clear(); } private static string RangeText() { List<string> list = new List<string>(); int[][] ranges = Ranges; foreach (int[] array in ranges) { list.Add(array[0] + "-" + array[1]); } return string.Join(", ", list.ToArray()); } } public static class ItemNameIndex { private static Dictionary<string, int> _index; private static readonly Dictionary<string, int> _custom = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); public static int Count => Ensure().Count + _custom.Count; public static void RegisterCustom(string displayName, int itemId) { if (!string.IsNullOrEmpty(displayName)) { _custom[displayName.Trim()] = itemId; } } public static List<KeyValuePair<string, int>> CustomSnapshot() { return new List<KeyValuePair<string, int>>(_custom); } public static bool TryResolve(string displayName, out int itemId) { if (string.IsNullOrEmpty(displayName)) { itemId = 0; return false; } string key = displayName.Trim(); if (_custom.TryGetValue(key, out itemId)) { return true; } return Ensure().TryGetValue(key, out itemId); } public static bool TryResolveCatalog(string candidate, out int itemId, out string via) { Dictionary<string, Item> iTEM_PREFABS = ResourcesPrefabManager.ITEM_PREFABS; if (iTEM_PREFABS == null) { itemId = 0; via = null; return false; } foreach (KeyValuePair<string, Item> item in iTEM_PREFABS) { Item value = item.Value; if (!((Object)(object)value == (Object)null)) { string text = (((Object)(object)((Component)value).gameObject != (Object)null) ? ((Object)((Component)value).gameObject).name : null); if (text != null && (string.Equals(text, candidate, StringComparison.OrdinalIgnoreCase) || text.EndsWith("_" + candidate, StringComparison.OrdinalIgnoreCase))) { itemId = value.ItemID; via = "prefab name '" + candidate + "'"; return true; } } } if (TryResolve(candidate, out var itemId2)) { itemId = itemId2; via = "display name '" + candidate + "'"; return true; } itemId = 0; via = null; return false; } public static bool TryResolveArg(string key, Func<Item, bool> filter, string noun, out int itemId, out string via, out string problem) { itemId = 0; via = null; problem = null; string text = key?.Trim(); if (string.IsNullOrEmpty(text)) { problem = "no name or ItemID given."; return false; } if (DevNum.TryInt(text, out itemId)) { via = "numeric id"; return true; } if (filter == null) { if (TryResolveCatalog(text, out itemId, out via)) { return true; } } else if (TryResolveFiltered(text, filter, out itemId, out via)) { return true; } List<string> list = new List<string>(); foreach (KeyValuePair<string, int> item in Suggest(text, 8, filter)) { list.Add($"'{item.Key}' ({item.Value})"); } string text2 = (string.IsNullOrEmpty(noun) ? "matched nothing" : ("matched no " + noun)); problem = "'" + text + "' " + text2 + " — " + ((list.Count > 0) ? ("did you mean: " + string.Join(", ", list.ToArray())) : "no similar display names") + "."; itemId = 0; return false; } private static bool TryResolveFiltered(string candidate, Func<Item, bool> filter, out int itemId, out string via) { itemId = 0; via = null; Dictionary<string, Item> iTEM_PREFABS = ResourcesPrefabManager.ITEM_PREFABS; if (iTEM_PREFABS == null) { return false; } int num = 0; int num2 = 0; foreach (Item value in iTEM_PREFABS.Values) { if (!((Object)(object)value == (Object)null) && filter(value)) { string text = (((Object)(object)((Component)value).gameObject != (Object)null) ? ((Object)((Component)value).gameObject).name : null); if (text != null && (string.Equals(text, candidate, StringComparison.OrdinalIgnoreCase) || text.EndsWith("_" + candidate, StringComparison.OrdinalIgnoreCase)) && (num == 0 || value.ItemID < num)) { num = value.ItemID; } if (!string.IsNullOrEmpty(value.Name) && string.Equals(value.Name.Trim(), candidate, StringComparison.OrdinalIgnoreCase) && (num2 == 0 || value.ItemID < num2)) { num2 = value.ItemID; } } } if (num != 0) { itemId = num; via = "prefab name '" + candidate + "'"; return true; } if (num2 != 0) { itemId = num2; via = "display name '" + candidate + "'"; return true; } return false; } public static List<KeyValuePair<string, int>> Suggest(string fragment, int max = 8, Func<Item, bool> filter = null) { List<KeyValuePair<string, int>> result = new List<KeyValuePair<string, int>>(); if (string.IsNullOrEmpty(fragment)) { return result; } string needle = fragment.Trim(); if (needle.Length == 0) { return result; } result = ForgeKit.Suggest.TwoPass(new IEnumerable<KeyValuePair<string, int>>[2] { Ensure(), _custom }, (KeyValuePair<string, int> kv, Suggest.Pass pass) => ForgeKit.Suggest.Matches(kv.Key, needle, pass) && Passes(kv.Value, filter)); result.Sort(delegate(KeyValuePair<string, int> a, KeyValuePair<string, int> b) { int num = string.Compare(a.Key, b.Key, StringComparison.OrdinalIgnoreCase); return (num == 0) ? a.Value.CompareTo(b.Value) : num; }); if (result.Count > max) { result.RemoveRange(max, result.Count - max); } return result; } private static bool Passes(int itemId, Func<Item, bool> filter) { if (filter == null) { return true; } ResourcesPrefabManager instance = ResourcesPrefabManager.Instance; Item val = ((instance != null) ? instance.GetItemPrefab(itemId) : null); if ((Object)(object)val != (Object)null) { return filter(val); } return false; } private static Dictionary<string, int> Ensure() { if (_index != null) { return _index; } Dictionary<string, int> dictionary = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); Dictionary<string, Item> iTEM_PREFABS = ResourcesPrefabManager.ITEM_PREFABS; if (iTEM_PREFABS == null) { return dictionary; } foreach (Item value2 in iTEM_PREFABS.Values) { if (!((Object)(object)value2 == (Object)null) && !string.IsNullOrEmpty(value2.Name)) { string key = value2.Name.Trim(); if (!dictionary.TryGetValue(key, out var value) || value2.ItemID < value) { dictionary[key] = value2.ItemID; } } } if (dictionary.Count > 0) { _index = dictionary; } return dictionary; } } internal static class KeybindRegistry { internal struct Bound { public string Mod; public string Action; public string ConfigHint; public string MainKey; public string[] Modifiers; } internal const string NoneKey = "None"; private static readonly Dictionary<string, Bound> _claims = new Dictionary<string, Bound>(); internal static int Count => _claims.Count; internal static void Reset() { _claims.Clear(); } internal static bool SameCombo(string aMain, string[] aMods, string bMain, string[] bMods) { if (aMain != bMain) { return false; } List<string> list = new List<string>(aMods ?? new string[0]); List<string> list2 = new List<string>(bMods ?? new string[0]); if (list.Count != list2.Count) { return false; } list.Sort(StringComparer.Ordinal); list2.Sort(StringComparer.Ordinal); for (int i = 0; i < list.Count; i++) { if (list[i] != list2[i]) { return false; } } return true; } internal static string ComboText(string mainKey, string[] modifiers) { StringBuilder stringBuilder = new StringBuilder(); if (modifiers != null) { foreach (string value in modifiers) { stringBuilder.Append(value).Append('+'); } } return stringBuilder.Append(mainKey).ToString(); } internal static string Claim(string mod, string action, string mainKey, string[] modifiers, string configHint) { string text = mod + "|" + action; if (mainKey == "None") { _claims.Remove(text); return null; } _claims[text] = new Bound { Mod = mod, Action = action, ConfigHint = configHint, MainKey = mainKey, Modifiers = (modifiers ?? new string[0]) }; List<Bound> list = Others(mainKey, modifiers, text); if (list.Count == 0) { return null; } StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("[KEYBIND] CONFLICT on " + ComboText(mainKey, modifiers) + ": "); stringBuilder.Append(Describe(_claims[text])); foreach (Bound item in list) { stringBuilder.Append(" AND ").Append(Describe(item)); } stringBuilder.Append(". One keypress fires ALL of them — that is a real gameplay bug, not just noise "); stringBuilder.Append("(Bug 26: opening the spawn menu silently cast Hunt as One). Rebind one of them: "); stringBuilder.Append(RebindAdvice(mainKey, modifiers, text, list)); return stringBuilder.ToString(); } internal static string Report() { if (_claims.Count == 0) { return "[KEYBIND] no keys claimed."; } SortedDictionary<string, List<Bound>> sortedDictionary = new SortedDictionary<string, List<Bound>>(StringComparer.Ordinal); foreach (Bound value2 in _claims.Values) { string key = ComboText(value2.MainKey, value2.Modifiers); if (!sortedDictionary.TryGetValue(key, out var value)) { value = (sortedDictionary[key] = new List<Bound>()); } value.Add(value2); } StringBuilder stringBuilder = new StringBuilder("[KEYBIND] claimed keys:"); int num = 0; foreach (KeyValuePair<string, List<Bound>> item in sortedDictionary) { bool flag = item.Value.Count > 1; if (flag) { num++; } stringBuilder.Append(string.Format("\n {0,-12}{1}", item.Key, flag ? "*** CONFLICT *** " : "")); for (int i = 0; i < item.Value.Count; i++) { stringBuilder.Append((i == 0) ? "" : " AND ").Append(Describe(item.Value[i])); } } stringBuilder.Append($"\n {num} conflict(s) across {sortedDictionary.Count} key(s)."); return stringBuilder.ToString(); } internal static bool IsFree(string mainKey, string[] modifiers) { if (mainKey == "None") { return true; } foreach (KeyValuePair<string, Bound> claim in _claims) { if (SameCombo(claim.Value.MainKey, claim.Value.Modifiers, mainKey, modifiers)) { return false; } } return true; } internal static bool HasConflicts() { foreach (KeyValuePair<string, Bound> claim in _claims) { if (Others(claim.Value.MainKey, claim.Value.Modifiers, claim.Key).Count > 0) { return true; } } return false; } private static List<Bound> Others(string mainKey, string[] modifiers, string selfId) { List<Bound> list = new List<Bound>(); foreach (KeyValuePair<string, Bound> claim in _claims) { if (claim.Key != selfId && SameCombo(claim.Value.MainKey, claim.Value.Modifiers, mainKey, modifiers)) { list.Add(claim.Value); } } return list; } private static string Describe(Bound c) { return c.Mod + " '" + c.Action + "'" + ((c.ConfigHint == null) ? " (HARDCODED — cannot be rebound)" : (" (" + c.ConfigHint + ")")); } private static string RebindAdvice(string mainKey, string[] modifiers, string selfId, List<Bound> others) { List<Bound> list = new List<Bound>(); Bound item = _claims[selfId]; if (item.ConfigHint != null) { list.Add(item); } foreach (Bound other in others) { if (other.ConfigHint != null) { list.Add(other); } } if (list.Count == 0) { return "BOTH are hardcoded — this needs a code fix, there is nothing the player can do about " + ComboText(mainKey, modifiers) + "."; } StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < list.Count; i++) { stringBuilder.Append((i == 0) ? "" : " or ").Append(list[i].Mod + "'s " + list[i].ConfigHint); } return stringBuilder.ToString() + "."; } } public static class Keybinds { private static readonly HashSet<ConfigEntry<KeyboardShortcut>> _subscribed = new HashSet<ConfigEntry<KeyboardShortcut>>(); private static ManualLogSource L => Plugin.Log; private static string Main(KeyboardShortcut c) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) return ((object)((KeyboardShortcut)(ref c)).MainKey/*cast due to .constrained prefix*/).ToString(); } private static string[] Mods(KeyboardShortcut c) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) List<string> list = new List<string>(); foreach (KeyCode modifier in ((KeyboardShortcut)(ref c)).Modifiers) { list.Add(((object)modifier/*cast due to .constrained prefix*/).ToString()); } return list.ToArray(); } public static void Claim(string mod, string action, ConfigEntry<KeyboardShortcut> entry) { //IL_00af: Unknown result type (might be due to invalid IL or missing references) if (entry == null) { ManualLogSource l = L; if (l != null) { l.LogWarning((object)("[KEYBINDS] " + mod + "/" + action + ": no config entry — key NOT registered, so a collision on it cannot be detected.")); } return; } string hint = "[" + ((ConfigEntryBase)entry).Definition.Section + "] " + ((ConfigEntryBase)entry).Definition.Key; Claim(mod, action, entry.Value, hint); if (_subscribed.Add(entry)) { entry.SettingChanged += delegate { //IL_0012: Unknown result type (might be due to invalid IL or missing references) Claim(mod, action, entry.Value, hint); }; } } public static void Claim(string mod, string action, KeyCode key, string configHint) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Unknown result type (might be due to invalid IL or missing references) Keybinds.Claim(mod, action, new KeyboardShortcut(key, Array.Empty<KeyCode>()), configHint); } public static void Claim(string mod, string action, KeyboardShortcut combo, string configHint) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Unknown result type (might be due to invalid IL or missing references) string text = KeybindRegistry.Claim(mod, action, Main(combo), Mods(combo), configHint); if (text != null) { ManualLogSource l = L; if (l != null) { l.LogWarning((object)text); } } } public static string Report() { return KeybindRegistry.Report(); } public static bool IsFree(KeyboardShortcut combo) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) return KeybindRegistry.IsFree(Main(combo), Mods(combo)); } public static bool IsFree(KeyCode key) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) return Keybinds.IsFree(new KeyboardShortcut(key, Array.Empty<KeyCode>())); } public static bool HasConflicts() { return KeybindRegistry.HasConflicts(); } } public static class KitContract { public readonly struct Declaration { public readonly string Consumer; public readonly string KitGuid; public readonly string BuiltAgainst; public Declaration(string consumer, string kitGuid, string builtAgainst) { Consumer = consumer; KitGuid = kitGuid; BuiltAgainst = builtAgainst; } } public const string CompatSinceField = "COMPAT_SINCE"; public const string GuidPrefix = "cobalt."; private static readonly List<Declaration> s_declared = new List<Declaration>(); public static IReadOnlyList<Declaration> Declared => s_declared; public static void Declare(string consumer, string kitGuid, string builtAgainstVersion) { if (string.IsNullOrEmpty(consumer) || string.IsNullOrEmpty(kitGuid)) { return; } for (int i = 0; i < s_declared.Count; i++) { if (s_declared[i].Consumer == consumer && s_declared[i].KitGuid == kitGuid) { return; } } s_declared.Add(new Declaration(consumer, kitGuid, builtAgainstVersion ?? "")); } public static List<KeyValuePair<bool, string>> Report(out bool anyError) { anyError = false; List<KeyValuePair<bool, string>> list = new List<KeyValuePair<bool, string>>(); if (s_declared.Count == 0) { list.Add(new KeyValuePair<bool, string>(key: false, "[CONTRACT] no consumer declared a kit version. Either only kits are installed, or every consumer predates KitContract — the handshake cannot check anything; see [STAMP] below.")); return list; } foreach (Declaration item in s_declared) { string kitName = item.KitGuid; string text = null; string compatSince = null; if (Chainloader.PluginInfos.TryGetValue(item.KitGuid, out var value) && value != null) { BepInPlugin metadata = value.Metadata; kitName = ((metadata != null) ? metadata.Name : null) ?? item.KitGuid; BepInPlugin metadata2 = value.Metadata; text = ((metadata2 == null) ? null : metadata2.Version?.ToString()); BaseUnityPlugin instance = value.Instance; compatSince = ReadConst(((Object)(object)instance != (Object)null) ? ((object)instance).GetType() : null, "COMPAT_SINCE"); } KitContractRules.Verdict v = ((text == null) ? KitContractRules.Verdict.Unknown : KitContractRules.Judge(item.BuiltAgainst, text, compatSince)); bool flag = KitContractRules.IsError(v); anyError |= flag; list.Add(new KeyValuePair<bool, string>(flag, (text == null) ? ("[CONTRACT] " + item.Consumer + " built against " + item.KitGuid + " " + item.BuiltAgainst + " — kit not loaded (BepInEx refused or skipped it; see the chainloader lines above).") : KitContractRules.Describe(item.Consumer, kitName, item.BuiltAgainst, text, compatSince, v))); } return list; } public static string StampCensus(out bool skew) { SortedDictionary<string, List<string>> sortedDictionary = new SortedDictionary<string, List<string>>(StringComparer.Ordinal); foreach (KeyValuePair<string, PluginInfo> pluginInfo in Chainloader.PluginInfos) { if (pluginInfo.Key == null || !pluginInfo.Key.StartsWith("cobalt.", StringComparison.Ordinal)) { continue; } Assembly asm = null; try { PluginInfo value = pluginInfo.Value; BaseUnityPlugin val = ((value != null) ? value.Instance : null); if ((Object)(object)val != (Object)null) { asm = ((object)val).GetType().Assembly; } } catch { } string key = BuildStamp.Read(asm); if (!sortedDictionary.TryGetValue(key, out var value2)) { value2 = (sortedDictionary[key] = new List<string>()); } List<string> list2 = value2; PluginInfo value3 = pluginInfo.Value; object obj2; if (value3 == null) { obj2 = null; } else { BepInPlugin metadata = value3.Metadata; obj2 = ((metadata != null) ? metadata.Name : null); } if (obj2 == null) { obj2 = pluginInfo.Key; } PluginInfo value4 = pluginInfo.Value; object arg; if (value4 == null) { arg = null; } else { BepInPlugin metadata2 = value4.Metadata; arg = ((metadata2 != null) ? metadata2.Version : null); } list2.Add($"{obj2} {arg}"); } int num = 0; foreach (string key2 in sortedDictionary.Keys) { if (key2 != "unknown") { num++; } } skew = num > 1; StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append(skew ? ($"[STAMP] BUILD SKEW: {num} different builds are installed side by side. Kits are shared DLLs — " + "a mod built against one build of a kit and running on another fails at first call, not at load. Install everything from ONE bundle/release:") : $"[STAMP] {sortedDictionary.Count} build(s) installed:"); foreach (KeyValuePair<string, List<string>> item in sortedDictionary) { item.Value.Sort(StringComparer.Ordinal); stringBuilder.Append(string.Format("\n[STAMP] {0,-20} {1}", item.Key, string.Join(", ", item.Value))); } return stringBuilder.ToString(); } public static bool IsAlphaBundleInstall() { try { string path = Path.Combine(Paths.BepInExRootPath, "cobalt-bundles"); return Directory.Exists(path) && Directory.GetFiles(path, "*.json").Length != 0; } catch { return false; } } public static string ToastText(bool contractError, bool stampSkew) { if (contractError) { return "Mod version mismatch: a mod was built against a different kit build. Reinstall all mods from ONE bundle. Details: BepInEx/LogOutput.log [CONTRACT]"; } if (!stampSkew) { return null; } return "Mods from two different alpha bundles are installed. Reinstall from ONE bundle. See [STAMP] in BepInEx/LogOutput.log"; } private static string ReadConst(Type t, string name) { if (t == null) { return null; } try { FieldInfo field = t.GetField(name, BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy); if (field == null) { return null; } object obj = (field.IsLiteral ? field.GetRawConstantValue() : field.GetValue(null)); return obj as string; } catch { return null; } } } public static class KitContractRules { public enum Verdict { Match, KitNewerCompatible, ConsumerBelowFloor, ConsumerAhead, Unknown } public static bool IsError(Verdict v) { if (v != Verdict.ConsumerBelowFloor) { return v == Verdict.ConsumerAhead; } return true; } public static Version Parse(string s) { if (string.IsNullOrEmpty(s)) { return null; } s = s.Trim(); if (s.StartsWith("v", StringComparison.OrdinalIgnoreCase)) { s = s.Substring(1); } if (!Version.TryParse(s, out Version result) || s.IndexOf('-') >= 0 || s.IndexOf('+') >= 0) { return null; } return result; } public static Verdict Judge(string builtAgainst, string running, string compatSince) { Version version = Parse(builtAgainst); Version version2 = Parse(running); if (version == null || version2 == null) { return Verdict.Unknown; } int num = Compare(version, version2); if (num == 0) { return Verdict.Match; } if (num > 0) { return Verdict.ConsumerAhead; } Version version3 = Parse(compatSince); if (version3 != null && Compare(version, version3) < 0) { return Verdict.ConsumerBelowFloor; } return Verdict.KitNewerCompatible; } public static int Compare(Version a, Version b) { return Norm(a).CompareTo(Norm(b)); } private static Version Norm(Version v) { return new Version(Math.Max(v.Major, 0), Math.Max(v.Minor, 0), Math.Max(v.Build, 0), 0); } public static string Describe(string consumer, string kitName, string builtAgainst, string running, string compatSince, Verdict v) { string text = "[CONTRACT] " + consumer + " built against " + kitName + " " + builtAgainst + ", running " + running; return v switch { Verdict.Match => text + " — match.", Verdict.KitNewerCompatible => text + " — newer kit, binary-compatible (floor " + (compatSince ?? "none") + ").", Verdict.ConsumerBelowFloor => text + " — VERSION SKEW: " + kitName + " only binds consumers built against >= " + compatSince + " (COMPAT_SINCE). Public members " + consumer + " was compiled to call no longer exist in this shape; expect MissingMethod/MissingFieldException at first use. Remedy: install " + consumer + " and " + kitName + " from the SAME bundle/release.", Verdict.ConsumerAhead => text + " — VERSION SKEW (consumer AHEAD): " + consumer + " was built against a NEWER " + kitName + " than is installed. " + kitName + " is the stale half — update it. Expect MissingMethodException the first time a newer seam is touched.", _ => text + " — could not compare (unparseable version).", }; } } public static class Lifecycle { private static readonly Dictionary<object, int> _generations = new Dictionary<object, int>(); private static bool _pollThrowNoted; public static bool IsSanePosition(Vector3 p) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) if (p.y > -3000f) { return ((Vector3)(ref p)).sqrMagnitude < 100000000f; } return false; } public static bool TryGetFirstLocalCharacter(out Character player) { player = null; CharacterManager instance = CharacterManager.Instance; if ((Object)(object)instance == (Object)null) { return false; } try { player = instance.GetFirstLocalCharacter(); } catch (NullReferenceException) { return false; } return (Object)(object)player != (Object)null; } public static Character FirstLocalCharacterOrNull() { if (!TryGetFirstLocalCharacter(out var player)) { return null; } return player; } public static int InvalidateWaits(object waitKey) { if (waitKey == null) { return 0; } _generations.TryGetValue(waitKey, out var value); value++; _generations[waitKey] = value; return value; } private static bool IsStale(object waitKey, int myGen) { if (waitKey != null && _generations.TryGetValue(waitKey, out var value)) { return value != myGen; } return false; } public static IEnumerator WhenPlayerReady(Func<Character> getPlayer, Action<Character> onReady, Action<string> onTimeout = null, float timeoutSeconds = 30f, object waitKey = null) { int myGen = ((waitKey != null) ? InvalidateWaits(waitKey) : 0); float t0 = Time.unscaledTime; Character player = null; while (Time.unscaledTime - t0 < timeoutSeconds) { if (IsStale(waitKey, myGen)) { LogSuperseded(waitKey); yield break; } try { player = getPlayer(); } catch (Exception ex) { if (!_pollThrowNoted) { _pollThrowNoted = true; ManualLogSource log = Plugin.Log; if (log != null) { log.LogMessage((object)("[LIFECYCLE] a WhenPlayerReady resolver threw (" + ex.GetType().Name + ") — treated as 'not ready yet' (MP9). Said once per session.")); } } player = null; } if ((Object)(object)player != (Object)null && IsSanePosition(((Component)player).transform.position)) { break; } player = null; yield return (object)new WaitForSecondsRealtime(0.5f); } if (IsStale(waitKey, myGen)) { LogSuperseded(waitKey); } else if ((Object)(object)player == (Object)null) { onTimeout?.Invoke($"player not ready within {timeoutSeconds:F0}s"); } else { onReady(player); } } private static void LogSuperseded(object waitKey) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogMessage((object)$"[LIFECYCLE] superseded a stale WhenPlayerReady wait (key={waitKey})."); } } } public static class LoadGate { public const float DefaultTimeoutSeconds = 180f; internal const float VanillaFixedDelta = 0.022f; private const float SettleSeconds = 2f; private static readonly HashSet<int> _armed = new HashSet<int>(); private static int _nextToken; private static bool _gotoLatched; private static float _gotoLatchedAt; public static bool Armed => _armed.Count > 0; public static void LatchGoto() { _gotoLatched = true; _gotoLatchedAt = Time.unscaledTime; } public static bool IsGotoLatched() { if (!_gotoLatched) { return false; } float num = Time.unscaledTime - _gotoLatchedAt; if (num > 180f) { _gotoLatched = false; return false; } if (num > 2f) { NetworkLevelLoader instance = NetworkLevelLoader.Instance; bool flag = false; try { flag = (Object)(object)instance != (Object)null && instance.IsOverallLoadingDone; } catch { } if (flag) { _gotoLatched = false; return false; } } return true; } public static string FormatGateState(NetworkLevelLoader nll) { if ((Object)(object)nll == (Object)null) { return "n/a (NetworkLevelLoader.Instance null — main menu?)"; } Func<Func<object>, string> func = delegate(Func<object> f) { try { object obj = f(); return (obj == null) ? "null" : obj.ToString(); } catch (Exception ex) { return "threw:" + ex.GetType().Name; } }; return "continueAfter=" + func(() => nll.ContinueAfterLoading) + " gameplayLoading=" + func(() => nll.IsGameplayLoading) + " sceneLoading=" + func(() => nll.IsSceneLoading) + " preping=" + func(() => nll.m_prepingLoadLevel) + " allDone=" + func(() => nll.AllPlayerDoneLoading) + " allReady=" + func(() => nll.AllPlayerReadyToContinue) + " overallDone=" + func(() => nll.IsOverallLoadingDone) + " masterLoadingUI=" + func(() => (Object)(object)MenuManager.Instance != (Object)null && MenuManager.Instance.IsMasterLoadingDisplayed) + " joiningWorld=" + func(() => nll.IsJoiningWorld) + " doneIds=[" + func(() => string.Join(",", ToStrings(nll.m_doneLoadingPlayers))) + "] readyIds=[" + func(() => string.Join(",", ToStrings(nll.m_readyToContinuePlayers))) + "] waitingOthers=" + func(() => nll.m_waitingForOtherPlayers) + " prologuePanel=" + func(() => (Object)(object)MenuManager.Instance != (Object)null && MenuManager.Instance.IsProloguePanelDisplayed) + " scene='" + func(delegate { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) Scene activeScene = SceneManager.GetActiveScene(); return ((Scene)(ref activeScene)).name; }) + "'"; } private static string[] ToStrings(List<int> ids) { if (ids == null) { return new string[0]; } string[] array = new string[ids.Count]; for (int i = 0; i < ids.Count; i++) { array[i] = ids[i].ToString(); } return array; } public static LoadSnapshot Snapshot(NetworkLevelLoader nll) { LoadSnapshot result = default(LoadSnapshot); if ((Object)(object)nll == (Object)null) { return result; } try { result.OverallDone = nll.IsOverallLoadingDone; } catch { } try { result.SaveInProgress = nll.IsSaveInProgress; } catch { } try { result.Preping = nll.m_prepingLoadLevel; } catch { } try { result.GameplayLoading = nll.IsGameplayLoading; } catch { } try { result.SceneLoading = nll.IsSceneLoading; } catch { } try { result.AllDone = nll.AllPlayerDoneLoading; } catch { } try { result.ContinueAfter = nll.ContinueAfterLoading; } catch { } try { result.MasterLoadingUi = (Object)(object)MenuManager.Instance != (Object)null && MenuManager.Instance.IsMasterLoadingDisplayed; } catch { } try { result.ProloguePanelUp = (Object)(object)MenuManager.Instance != (Object)null && MenuManager.Instance.IsProloguePanelDisplayed; } catch { } return result; } public static bool LoadInFlight(NetworkLevelLoader nll, out string why) { why = null; if ((Object)(object)nll == (Object)null) { return false; } try { if (nll.IsSceneLoading) { why = "IsSceneLoading"; return true; } } catch { } try { if (nll.IsGameplayLoading) { why = "IsGameplayLoading"; return true; } } catch { } try { if (nll.m_prepingLoadLevel) { why = "m_prepingLoadLevel"; return true; } } catch { } try { if (!nll.IsOverallLoadingDone) { bool flag = false; try { flag = nll.m_waitingForOtherPlayers; } catch { } why = (flag ? "!IsOverallLoadingDone (m_waitingForOtherPlayers — a peer never checked in; 'unstick force forceready' if you are sure)" : "!IsOverallLoadingDone"); return true; } } catch { } if (IsGotoLatched()) { why = "a previous dev goto is still latched"; return true; } return false; } public static int ArmForVerb(ManualLogSource log, string reason) { return Arm(log, reason, "[LOADGATE]", null, null, null, null, 180f, releasesGotoLatch: true); } public static int Arm(ManualLogSource log, string reason, string tag = "[LOADGATE]", Func<bool> alive = null, Func<bool> gatePhaseReached = null, Action onPassed = null, Func<string> extraFields = null, float timeoutSeconds = 180f, bool releasesGotoLatch = false) { int num = ++_nextToken; _armed.Add(num); Runner.Instance.StartCoroutine(Watch(num, log, reason ?? "?", tag ?? "[LOADGATE]", alive, gatePhaseReached, onPassed, extraFields, timeoutSeconds, releasesGotoLatch)); return num; } public static void Disarm(int token) { _armed.Remove(token); } private static IEnumerator Watch(int token, ManualLogSource log, string reason, string tag, Func<bool> alive, Func<bool> gatePhaseReached, Action onPassed, Func<string> extraFields, float timeoutSeconds, bool releasesGotoLatch) { float armedAt = Time.unscaledTime; float lastReport = armedAt; bool sawLoading = false; bool passed = false; bool warnedMsgQueue = false; bool rescuedTimeScale = false; float msgQueueDownSince = -1f; string phase = "unknown"; List<string> phases = new List<string>(); while (_armed.Contains(token)) { bool flag = false; float num = Time.unscaledTime - armedAt; try { if (alive != null && !alive()) { flag = true; } } catch (Exception ex) { log.LogWarning((object)(tag + " liveness check threw (" + ex.GetType().Name + ") — disarming '" + reason + "'.")); flag = true; } if (!flag) { try { NetworkLevelLoader instance = NetworkLevelLoader.Instance; LoadSnapshot s = Snapshot(instance); string text = LoadPhase.Classify(s); if (text != phase) { phase = text; phases.Add(text); } if (s.GameplayLoading) { sawLoading = true; } bool flag2 = gatePhaseReached?.Invoke() ?? (sawLoading && !s.GameplayLoading && num > 1f); if (!passed && flag2 && LoadPhase.GateGuardHolds(s) && (Object)(object)instance != (Object)null) { passed = true; if (onPassed != null) { onPassed(); } else { instance.SetContinueAfterLoading(); } log.LogMessage((object)$"{tag} passed the continue gate for '{reason}' after {num:F1}s — no keypress needed."); } float timeScale = Time.timeScale; if (!rescuedTimeScale && timeScale < 0.01f && !s.OverallDone) { rescuedTimeScale = true; int gamePausedByPlayer = Global.GamePausedByPlayer; if (gamePausedByPlayer != -1) { PauseMenu.Pause(false); log.LogWarning((object)($"{tag} timeScale was {timeScale:F3} with the pause menu claimed by player {gamePausedByPlayer} " + "during '" + reason + "' — closed it (PauseMenu.Pause(false)); a scaled-time load never advances.")); } else { Time.timeScale = 1f; Time.fixedDeltaTime = 0.022f; log.LogWarning((object)($"{tag} timeScale was {timeScale:F3} (unclaimed) during '{reason}' — restored to 1; " + "a scaled-time load never advances (BlackFade/Invoke are scaled).")); } } bool flag3 = true; try { flag3 = PhotonNetwork.isMessageQueueRunning; } catch { } if (!flag3) { if (msgQueueDownSince < 0f) { msgQueueDownSince = Time.unscaledTime; } if (!warnedMsgQueue && Time.unscaledTime - msgQueueDownSince > 10f) { warnedMsgQueue = true; log.LogWarning((object)(tag + " PhotonNetwork.isMessageQueueRunning has been FALSE for " + $"{Time.unscaledTime - msgQueueDownSince:F0}s during '{reason}' — the loader disables it for the " + "load and re-enables it only at NetworkLevelLoader.cs:1307, so this machine is network-dead until the load gets past that point. A peer will see this box as frozen, not as slow.")); } } else { msgQueueDownSince = -1f; } if (Time.unscaledTime - lastReport >= 5f) { lastReport = Time.unscaledTime; string text2; try { text2 = extraFields?.Invoke(); } catch { text2 = "extraFields:threw"; } float num2 = -1f; try { if ((Object)(object)instance != (Object)null && instance.m_async != null) { num2 = instance.m_async.progress; } } catch { } log.LogMessage((object)($"{tag} phase={phase} t={num:F0}s " + (string.IsNullOrEmpty(text2) ? "" : (text2 + " ")) + FormatGateState(instance) + $" msgQueue={flag3} timeScale={timeScale:F2} asyncProgress={num2:F2} sawLoading={sawLoading}")); } if (s.OverallDone && sawLoading && num > 2f) { log.LogMessage((object)($"{tag} load '{reason}' reached overallDone after {num:F1}s " + "(phases: " + string.Join(" -> ", phases.ToArray()) + ")")); if (releasesGotoLatch) { _gotoLatched = false; } flag = true; } else if (num > timeoutSeconds) { log.LogError((object)($"{tag} ERROR: load '{reason}' still not done after {timeoutSeconds:F0}s — " + "disarming; last phase=" + phase + ". Run 'unstick' for the full state.")); flag = true; } } catch (Exception ex2) { log.LogWarning((object)(tag + " watch poll threw (watch continues): " + ex2.GetType().Name + ": " + ex2.Message)); } } if (flag) { break; } yield return (object)new WaitForSecondsRealtime(0.25f); } _armed.Remove(token); } } public struct LoadSnapshot { public bool OverallDone; public bool SaveInProgress; public bool Preping; public bool GameplayLoading; public bool SceneLoading; public bool AllDone; public bool ContinueAfter; public bool MasterLoadingUi; public bool ProloguePanelUp; } public static class LoadPhase { public const string Done = "done"; public const string Saving = "saving"; public const string Preping = "preping"; public const string Prologue = "prologue"; public const string SceneLoading = "scene-loading"; public const string Fading = "fading"; public const string Gate = "gate"; public const string WaitingPlayers = "waiting-players"; public const string PostGate = "post-gate"; public const string Unknown = "unknown"; public static string Classify(LoadSnapshot s) { if (s.SaveInProgress) { return "saving"; } if (s.Preping) { return "preping"; } if (s.ProloguePanelUp) { return "prologue"; } if (s.OverallDone) { return "done"; } if (s.SceneLoading) { return "scene-loading"; } if (s.GameplayLoading) { return "fading"; } if (s.AllDone && !s.ContinueAfter && s.MasterLoadingUi) { return "gate"; } if (!s.AllDone) { return "waiting-players"; } if (s.ContinueAfter) { return "post-gate"; } return "unknown"; } public static bool GateGuardHolds(LoadSnapshot s) { if (!s.ContinueAfter && !s.GameplayLoading && s.AllDone && s.MasterLoadingUi) { return !s.ProloguePanelUp; } return false; } } public enum LogTier { Quiet, Normal, Verbose, Trace } public static class LogTiers { public const int RankAlways = 0; public const int RankMessage = 1; public const int RankInfo = 2; public const int RankDebug = 3; public static bool Emits(LogTier tier, int rank) { return rank <= (int)tier; } public static bool Emits(LogTier tier, int rank, bool requested) { if (!requested) { return Emits(tier, rank); } return true; } public static bool TryParse(string text, ref LogTier tier) { if (string.IsNullOrEmpty(text)) { return false; } switch (text.Trim().ToLowerInvariant()) { case "quiet": case "q": tier = LogTier.Quiet; return true; case "n": case "default": case "normal": tier = LogTier.Normal; return true; case "v": case "verbose": tier = LogTier.Verbose; return true; case "debug": case "trace": case "t": tier = LogTier.Trace; return true; default: return false; } } public static string Names() { return "Quiet|Normal|Verbose|Trace"; } } public sealed class ModLog { private sealed class RequestedScope : IDisposable { private bool _done; internal RequestedScope() { s_requestedDepth++; } public void Dispose() { if (!_done) { _done = true; if (s_requestedDepth > 0) { s_requestedDepth--; } } } } private readonly ManualLogSource _sink; private static int s_requestedDepth; internal const string Section = "Diag"; internal const string Key = "LogLevel"; public const LogTier ShippedDefault = LogTier.Verbose; private const string Help = "How much this mod writes to the log. Quiet = warnings/errors only. Normal adds boot lines, on-screen notices and self-test results. Verbose (default) adds the per-feature diagnostics — the default is deliberately chatty while these mods are still being debugged, so a bug report arrives with context. Trace adds per-tick detail. NOTE: Trace also needs BepInEx.cfg's [Logging.Disk] LogLevels to include Debug — stock BepInEx drops it. Verbose does not: Info is passed by default."; private static bool s_sinkWarned; public ManualLogSource Sink => _sink; public LogTier Tier { get; set; } public static bool RequestedOutput => s_requestedDepth > 0; public bool TraceOn => LogTiers.Emits(Tier, 3, RequestedOutput); public bool VerboseOn => LogTiers.Emits(Tier, 2, RequestedOutput); public bool MessageOn => LogTiers.Emits(Tier, 1, RequestedOutput); public ModLog(ManualLogSource sink, LogTier tier = LogTier.Verbose) { if (sink == null) { throw new ArgumentNullException("sink"); } _sink = sink; Tier = tier; } public static IDisposable Requested() { return new RequestedScope(); } public void LogFatal(object data) { _sink.LogFatal(data); } public void LogError(object data) { _sink.LogError(data); } public void LogWarning(object data) { _sink.LogWarning(data); } public void LogMessage(object data) { if (LogTiers.Emits(Tier, 1, RequestedOutput)) { _sink.LogMessage(data); } } public void LogInfo(object data) { if (LogTiers.Emits(Tier, 2, RequestedOutput)) { _sink.LogInfo(data); } } public void LogDebug(object data) { if (LogTiers.Emits(Tier, 3, RequestedOutput)) { _sink.LogDebug(data); } } public void Log(LogLevel level, object data) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) if (LogTiers.Emits(Tier, RankOf(level), RequestedOutput)) { _sink.Log(level, data); } } public static int RankOf(LogLevel level) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0003: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) if ((level & 0x20) != 0) { return 3; } if ((level & 0x10) != 0) { return 2; } if ((level & 8) != 0) { return 1; } return 0; } public static LogLevel FloorOf(LogTier tier) { return (LogLevel)(tier switch { LogTier.Trace => 32, LogTier.Verbose => 16, LogTier.Quiet => 4, _ => 8, }); } public static ModLog Ungated(ManualLogSource sink) { if (sink != null) { return new ModLog(sink, LogTier.Trace); } return null; } public static implicit operator ManualLogSource(ModLog log) { return log?._sink; } public static ModLog Bind(BaseUnityPlugin plugin, ManualLogSource sink) { if ((Object)(object)plugin == (Object)null) { throw new ArgumentNullException("plugin"); } ConfigEntry<LogTier> entry = plugin.Config.Bind<LogTier>("Diag", "LogLevel", LogTier.Verbose, "How much this mod writes to the log. Quiet = warnings/errors only. Normal adds boot lines, on-screen notices and self-test results. Verbose (default) adds the per-feature diagnostics — the default is deliberately chatty while these mods are still being debugged, so a bug report arrives with context. Trace adds per-tick detail. NOTE: Trace also needs BepInEx.cfg's [Logging.Disk] LogLevels to include Debug — stock BepInEx drops it. Verbose does not: Info is passed by default."); ModLog log = new ModLog(sink, entry.Value); entry.SettingChanged += delegate { log.Tier = entry.Value; }; WarnIfSinkDrops(sink, entry.Value); return log; } private unsafe static void WarnIfSinkDrops(ManualLogSource sink, LogTier tier) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_007a: 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_007e: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) if (s_sinkWarned || sink == null) { return; } try { LogLevel val = FloorOf(tier); foreach (ILogListener listener in Logger.Listeners) { if (listener == null || ((object)listener).GetType().Name.IndexOf("Disk", StringComparison.OrdinalIgnoreCase) < 0) { continue; } PropertyInfo property = ((object)listener).GetType().GetProperty("DisplayedLogLevel", BindingFlags.Instance | BindingFlags.Public); if (!(property == null)) { LogLevel val2 = (LogLevel)property.GetValue(listener, null); if ((val2 & val) == 0) { s_sinkWarned = true; sink.LogWarning((object)("[LOGLEVEL] Diag.LogLevel = " + tier.ToString() + " needs " + ((object)(*(LogLevel*)(&val))/*cast due to .constrained prefix*/).ToString() + ", but BepInEx's disk log is set to '" + ((object)(*(LogLevel*)(&val2))/*cast due to .constrained prefix*/).ToString() + "'. Those lines are being dropped before they reach the file — add " + ((object)(*(LogLevel*)(&val))/*cast due to .constrained prefix*/).ToString() + " to [Logging.Disk] LogLevels in BepInEx/config/BepInEx.cfg.")); } } break; } } catch { } } } public sealed class NameCandidates { public struct Validation { public string Raw { get; } public IReadOnlyList<string> Known { get; } public IReadOnlyList<string> Unknown { get; } public bool NoneKnown => Known.Count == 0; internal Validation(string raw, IReadOnlyList<string> known, IReadOnlyList<string> unknown) { Raw = raw; Known = known; Unknown = unknown; } } private readonly Func<string> _readRaw; private readonly Func<string, bool> _exists; private readonly Action<string> _onResolved; private readonly Action<string> _onUnresolved; private readonly HashSet<string> _warned = new HashSet<string>(); private string _resolvedKey; private string _resolvedId; private string _validatedKey; private string _missParkedKey; private float _missRetryAt; public const float MissRetrySeconds = 30f; internal static Func<float> Clock; public string Label { get; } public string Raw => _readRaw() ?? ""; public string Cached => _resolvedId; static NameCandidates() { Clock = () => 0f; Clock = () => Time.unscaledTime; } public static bool StatusPrefabExists(string name) { ResourcesPrefabManager instance = ResourcesPrefabManager.Instance; return (Object)(object)((instance != null) ? instance.GetStatusEffectPrefab(name) : null) != (Object)null; } public static bool ItemPrefabExists(string name) { ResourcesPrefabManager instance = ResourcesPrefabManager.Instance; return (Object)(object)((instance != null) ? instance.GetItemPrefab(name) : null) != (Object)null; } public NameCandidates(string label, Func<string> readRaw, Func<string, bool> exists, Action<string> onResolved = null, Action<string> onUnresolved = null) { Label = label; _readRaw = readRaw ?? throw new ArgumentNullException("readRaw"); _exists = exists ?? throw new ArgumentNullException("exists"); _onResolved = onResolved; _onUnresolved = onUnresolved; } public IReadOnlyList<string> Parse() { return Split(Raw); } public static IReadOnlyList<string> Split(string raw) { List<string> list = new List<string>(); if (string.IsNullOrEmpty(raw)) { return list; } string[] array = raw.Split(new char[1] { ',' }); foreach (string text in array) { string text2 = text.Trim(); if (text2.Length > 0 && !list.Contains(text2)) { list.Add(text2); } } return list; } public string Resolve() { string raw = Raw; if (_resolvedId != null && raw == _resolvedKey) { return _resolvedId; } if (_missParkedKey != null) { if (raw == _missParkedKey && Clock() < _missRetryAt) { return null; } _missParkedKey = null; } foreach (string item in Split(raw)) { if (_exists(item)) { _resolvedKey = raw; _resolvedId = item; _onResolved?.Invoke(item); return item; } } _resolvedKey = null; _resolvedId = null; _missParkedKey = raw; _missRetryAt = Clock() + 30f; if (_warned.Add(raw)) { _onUnresolved?.Invoke(raw); } return null; } public bool TryValidate(out Validation result) { string raw = Raw; if (raw == _validatedKey) { result = default(Validation); return false; } _validatedKey = raw; List<string> list = new List<string>(); List<string> list2 = new List<string>(); foreach (string item in Split(raw)) { (_exists(item) ? list : list2).Add(item); } result = new Validation(raw, list, list2); return true; } public T FindFirst<T>(Func<string, T> lookup) where T : class { if (lookup == null) { return null; } foreach (string item in Split(Raw)) { T val = lookup(item); if (val != null) { return val; } } return null; } public void Invalidate() { _resolvedKey = null; _resolvedId = null; _validatedKey = null; _missParkedKey = null; _warned.Clear(); } } public static class Notify { public static ManualLogSource Log; private static ManualLogSource L => Log ?? Plugin.Log; public static void Player(Character character, string message) { L.LogMessage((object)("[NOTIFY] " + message)); if ((Object)(object)character != (Object)null && (Object)(object)character.CharacterUI != (Object)null) { character.CharacterUI.ShowInfoNotification(message); } else { L.LogWarning((object)"[NOTIFY] no CharacterUI to show the toast on (logged only)."); } } } public sealed class PendingSet { public const double AbandonedAfterSeconds = 30.0; private readonly Dictionary<long, double> _open = new Dictionary<long, double>(); private readonly List<long> _scratch = new List<long>(); private long _nextId; public int RawCount => _open.Count; public long Open(double now) { long num = ++_nextId; _open[num] = now; return num; } public void Close(long id) { _open.Remove(id); } public int CountAt(double now) { Reap(now); return _open.Count; } public bool AnyAt(double now) { return CountAt(now) > 0; } private void Reap(double now) { if (_open.Count == 0) { return; } _scratch.Clear(); foreach (KeyValuePair<long, double> item in _open) { if (now - item.Value >= 30.0) { _scratch.Add(item.Key); } } for (int i = 0; i < _scratch.Count; i++) { _open.Remove(_scratch[i]); } } } [BepInPlugin("cobalt.forgekit", "ForgeKit", "0.4.10")] public class Plugin : BaseUnityPlugin { public const string GUID = "cobalt.forgekit"; public const string NAME = "ForgeKit"; public const string VERSION = "0.4.10"; public const string COMPAT_SINCE = "0.4.4"; internal static ManualLogSource Log; private bool _censusLogged; internal void Awake() { Log = ((BaseUnityPlugin)this).Logger; Log.LogMessage((object)"ForgeKit 0.4.10 loaded."); Log.LogMessage((object)("[FORGEKIT] build " + BuildStamp.Local)); } internal void Update() { if (_censusLogged) { return; } _censusLogged = true; ((Behaviour)this).enabled = false; try { Log.LogMessage((object)Keybinds.Report()); } catch (Exception ex) { Log.LogWarning((object)("[KEYBINDS] census failed: " + ex.GetType().Name + ": " + ex.Message + " — the [CONTRACT]/[STAMP] lines below are unaffected.")); } bool anyError = false; bool skew = false; try { foreach (KeyValuePair<bool, string> item in KitContract.Report(out anyError)) { if (item.Key) { Log.LogError((object)item.Value); } else { Log.LogMessage((object)item.Value); } } string text = KitContract.StampCensus(out skew); if (skew) { Log.LogWarning((object)text); } else { Log.LogMessage((object)text); } } catch (Exception ex2) { Log.LogWarning((object)("[CONTRACT] handshake report failed: " + ex2.GetType().Name + ": " + ex2.Message)); } try { foreach (string item2 in CfgSkew.Sweep()) { Log.LogWarning((object)item2); } } catch (Exception ex3) { Log.LogWarning((object)("[CFGSKEW] config-drift census failed: " + ex3.GetType().Name + ": " + ex3.Message + " — the [CONTRACT]/[STAMP] lines above are unaffected.")); } string toast = KitContract.ToastText(anyError, skew && KitContract.IsAlphaBundleInstall()); if (toast != null) { ((MonoBehaviour)this).StartCoroutine(Lifecycle.WhenPlayerReady(Lifecycle.FirstLocalCharacterOrNull, delegate(Character c) { Notify.Player(c, toast); }, null, 1800f)); } } } public static class ParticleProbe { public static void Dump(bool listAll, ManualLogSource log) { //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Invalid comparison between Unknown and I4 //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Invalid comparison between Unknown and I4 //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Invalid comparison between Unknown and I4 //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_0154: Unknown result type (might be due to invalid IL or missing references) ParticleSystem[] array = Resources.FindObjectsOfTypeAll<ParticleSystem>(); List<string> list = new List<string>(); List<string> list2 = new List<string>(); List<string> list3 = new List<string>(); int num = 0; ParticleSystem[] array2 = array; foreach (ParticleSystem val in array2) { if ((Object)(object)val == (Object)null) { continue; } ShapeModule shape; try { shape = val.shape; } catch { continue; } if (!((ShapeModule)(ref shape)).enabled) { continue; } ParticleSystemShapeType shapeType = ((ShapeModule)(ref shape)).shapeType; if ((int)shapeType == 6 || (int)shapeType == 13 || (int)shapeType == 14) { string source; bool baked; Mesh val2 = MeshOf(shape, shapeType, out source, out baked); float num2 = MeshArea(val2); string arg = (((Object)(object)val2 == (Object)null) ? "NULL" : ((Object)val2).name); int num3 = ((!((Object)(object)val2 == (Object)null)) ? val2.vertexCount : 0); bool isPlaying = val.isPlaying; if (isPlaying) { num++; } bool flag = (Object)(object)val2 == (Object)null || num3 == 0 || (num2 >= 0f && num2 <= 1E-07f); bool flag2 = !flag && isPlaying && num2 < 0f; if (baked) { Object.Destroy((Object)(object)val2); } string item = " " + Path(((Component)val).transform) + "\n" + $" shape={shapeType} mesh={arg} ({source}) " + $"verts={num3} " + "area=" + ((num2 < 0f) ? "UNREADABLE" : num2.ToString("F6")) + " " + $"playing={isPlaying} activeInHierarchy={((Component)val).gameObject.activeInHierarchy} " + "emissionEnabled=" + EmissionOn(val) + " emissionRate=" + Rate(val); if (flag) { list.Add(item); } else if (flag2) { list2.Add(item); } if (listAll) { list3.Add(item); } } } StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append($"[PSDUMP] {array.Length} ParticleSystem(s) in memory; {num} of the mesh-shaped ones are PLAYING.\n"); if (list.Count == 0) { stringBuilder.Append("[PSDUMP] no PROVABLY zero-area mesh emitter found.\n"); if (list2.Count == 0) { stringBuilder.Append("[PSDUMP] (If output_log.txt IS still spamming, the emitter's mesh is being invalidated at RUNTIME —\n re-run psdump WHILE the spam is flowing, and check the DDOL scene / a scene mid-unload.)\n"); } } else { stringBuilder.Append($"[PSDUMP] {list.Count} ZERO-AREA MESH EMITTER(S) — these are the spam. A PLAYING one logs every frame:\n"); foreach (string item2 in list) { stringBuilder.Append(item2).Append('\n'); } } if (list2.Count > 0) { stringBuilder.Append($"[PSDUMP] {list2.Count} SUSPECT(S) — playing, mesh-shaped, but the mesh is not readable so its area\n" + " cannot be verified from script. Do NOT read this as clean:\n"); foreach (string item3 in list2) { stringBuilder.Append(item3).Append('\n'); } } if (listAll && list3.Count > 0) { stringBuilder.Append($"[PSDUMP] all {list3.Count} mesh-shaped system(s):\n"); foreach (string item4 in list3) { stringBuilder.Append(item4).Append('\n'); } } log.LogMessage((object)stringBuilder.ToString()); } private unsafe static Mesh MeshOf(ShapeModule shape, ParticleSystemShapeType t, out string source, out bool baked) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Invalid comparison between Unknown and I4 //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Invalid comparison between Unknown and I4 //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Invalid comparison between Unknown and I4 //IL_0101: Unknown result type (might be due to invalid IL or missing references) //IL_0108: Expected O, but got Unknown source = ((object)(*(ParticleSystemShapeType*)(&t))/*cast due to .constrained prefix*/).ToString(); baked = false; if ((int)t != 6) { if ((int)t != 13) { if ((int)t == 14) { SkinnedMeshRenderer skinnedMeshRenderer = ((ShapeModule)(ref shape)).skinnedMeshRenderer; if ((Object)(object)skinnedMeshRenderer == (Object)null) { source = "SkinnedMeshRenderer=NULL (shape was never bound to a renderer)"; return null; } int num = ((skinnedMeshRenderer.bones != null) ? skinnedMeshRenderer.bones.Length : 0); int num2 = 0; if (skinnedMeshRenderer.bones != null) { Transform[] bones = skinnedMeshRenderer.bones; foreach (Transform val in bones) { if ((Object)(object)val == (Object)null) { num2++; } } } source = $"SkinnedMeshRenderer '{((Object)skinnedMeshRenderer).name}' bones={num} dead={num2}"; try { Mesh val2 = new Mesh(); skinnedMeshRenderer.BakeMesh(val2); source += " [baked]"; baked = true; return val2; } catch { return skinnedMeshRenderer.sharedMesh; } } return null; } MeshRenderer meshRenderer = ((ShapeModule)(ref shape)).meshRenderer; if ((Object)(object)meshRenderer == (Object)null) { source = "MeshRenderer=NULL"; return null; } source = "MeshRenderer '" + ((Object)meshRenderer).name + "'"; MeshFilter component = ((Component)meshRenderer).GetComponent<MeshFilter>(); if (!((Object)(object)component == (Object)null)) { return component.sharedMesh; } return null; } return ((ShapeModule)(ref shape)).mesh; } private static float MeshArea(Mesh mesh) { //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_0066: 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) //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)mesh == (Object)null || mesh.vertexCount == 0) { return 0f; } Vector3[] vertices; int[] triangles; try { if (!mesh.isReadable) { return -1f; } vertices = mesh.vertices; triangles = mesh.triangles; } catch { return -1f; } float num = 0f; for (int i = 0; i + 2 < triangles.Length; i += 3) { float num2 = num; Vector3 val = Vector3.Cross(vertices[triangles[i + 1]] - vertices[triangles[i]], vertices[triangles[i + 2]] - vertices[triangles[i]]); num = num2 + ((Vector3)(ref val)).magnitude * 0.5f; } return num; } private static string Rate(ParticleSystem ps) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) try { EmissionModule emission = ps.emission; MinMaxCurve rateOverTime = ((EmissionModule)(ref emission)).rateOverTime; return ((MinMaxCurve)(ref rateOverTime)).constant.ToString("F1"); } catch { return "?"; } } private static string EmissionOn(ParticleSystem ps) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) try { EmissionModule emission = ps.emission; return ((EmissionModule)(ref emission)).enabled.ToString(); } catch { return "?"; } } private static string Path(Transform t) { //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_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing