Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of Beacon v0.1.14
plugins/Beacon/Beacon.Core.dll
Decompiled a week agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Text; using System.Threading; using System.Threading.Tasks; using ExtraPotions.Beacon.Secrets; using ExtraPotions.Beacon.Servers; using ExtraPotions.Beacon.Text; using Microsoft.CodeAnalysis; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("ExtraPotions")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyCopyright("Copyright (C) 2026 expDARE (ExtraPotions Meadworks)")] [assembly: AssemblyFileVersion("0.1.14.0")] [assembly: AssemblyInformationalVersion("0.1.14+4312080b663b50769f0cf52f9bd002c67886c90a")] [assembly: AssemblyProduct("Beacon.Core")] [assembly: AssemblyTitle("Beacon.Core")] [assembly: AssemblyVersion("0.1.14.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace ExtraPotions.Beacon { public static class BeaconCore { public const string PluginGuid = "com.meadworks.beacon"; public const string ProductName = "Beacon"; public const string ProductVersion = "0.1.14"; public const string Family = "Runestone"; public const string TokenNamespace = "beacon"; public const string TokenPrefix = "meadworks_beacon_"; public const string DataFolder = "Meadworks/beacon"; public const string ServerListFile = "servers.json"; public const string WindowsSecretFile = "secrets.dat"; public const string SupportedGameVersion = "1.0.17"; } } namespace ExtraPotions.Beacon.Text { public static class MiniJson { private const int MaxDepth = 32; public static object? Parse(string text) { if (text == null) { throw new ArgumentNullException("text"); } int i = 0; object? result = Value(text, ref i, 0); Skip(text, ref i); if (i != text.Length) { throw new FormatException("Trailing characters after JSON value."); } return result; } public static string Quote(string value) { StringBuilder stringBuilder = new StringBuilder(value.Length + 2).Append('"'); foreach (char c in value) { switch (c) { case '"': stringBuilder.Append("\\\""); continue; case '\\': stringBuilder.Append("\\\\"); continue; case '\n': stringBuilder.Append("\\n"); continue; case '\r': stringBuilder.Append("\\r"); continue; case '\t': stringBuilder.Append("\\t"); continue; } if (c < ' ') { StringBuilder stringBuilder2 = stringBuilder.Append("\\u"); int num = c; stringBuilder2.Append(num.ToString("x4", CultureInfo.InvariantCulture)); } else { stringBuilder.Append(c); } } return stringBuilder.Append('"').ToString(); } private static void Skip(string s, ref int i) { while (i < s.Length && (s[i] == ' ' || s[i] == '\t' || s[i] == '\n' || s[i] == '\r')) { i++; } } private static object? Value(string s, ref int i, int depth) { if (depth > 32) { throw new FormatException("JSON nested too deeply."); } Skip(s, ref i); if (i >= s.Length) { throw new FormatException("Unexpected end of JSON."); } switch (s[i]) { case '{': return Obj(s, ref i, depth); case '[': return Arr(s, ref i, depth); case '"': return Str(s, ref i); default: if (Word(s, ref i, "true")) { return true; } if (Word(s, ref i, "false")) { return false; } if (Word(s, ref i, "null")) { return null; } return Num(s, ref i); } } private static bool Word(string s, ref int i, string word) { if (string.CompareOrdinal(s, i, word, 0, word.Length) != 0) { return false; } i += word.Length; return true; } private static Dictionary<string, object?> Obj(string s, ref int i, int depth) { Dictionary<string, object> dictionary = new Dictionary<string, object>(StringComparer.Ordinal); i++; Skip(s, ref i); if (i < s.Length && s[i] == '}') { i++; return dictionary; } while (true) { Skip(s, ref i); if (i >= s.Length || s[i] != '"') { throw new FormatException("Expected a property name."); } string key = Str(s, ref i); Skip(s, ref i); if (i >= s.Length || s[i] != ':') { throw new FormatException("Expected ':'."); } i++; dictionary[key] = Value(s, ref i, depth + 1); Skip(s, ref i); if (i >= s.Length || s[i] != ',') { break; } i++; } if (i < s.Length && s[i] == '}') { i++; return dictionary; } throw new FormatException("Expected ',' or '}'."); } private static List<object?> Arr(string s, ref int i, int depth) { List<object> list = new List<object>(); i++; Skip(s, ref i); if (i < s.Length && s[i] == ']') { i++; return list; } while (true) { list.Add(Value(s, ref i, depth + 1)); Skip(s, ref i); if (i >= s.Length || s[i] != ',') { break; } i++; } if (i < s.Length && s[i] == ']') { i++; return list; } throw new FormatException("Expected ',' or ']'."); } private static string Str(string s, ref int i) { StringBuilder stringBuilder = new StringBuilder(); i++; while (i < s.Length) { char c = s[i++]; switch (c) { case '"': return stringBuilder.ToString(); default: stringBuilder.Append(c); continue; case '\\': break; } if (i >= s.Length) { break; } switch (s[i++]) { case '"': stringBuilder.Append('"'); break; case '\\': stringBuilder.Append('\\'); break; case '/': stringBuilder.Append('/'); break; case 'b': stringBuilder.Append('\b'); break; case 'f': stringBuilder.Append('\f'); break; case 'n': stringBuilder.Append('\n'); break; case 'r': stringBuilder.Append('\r'); break; case 't': stringBuilder.Append('\t'); break; case 'u': { if (i + 4 > s.Length || !int.TryParse(s.Substring(i, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var result)) { throw new FormatException("Bad \\u escape."); } stringBuilder.Append((char)result); i += 4; break; } default: throw new FormatException("Bad escape."); } } throw new FormatException("Unterminated string."); } private static double Num(string s, ref int i) { int num = i; while (i < s.Length && "+-0123456789.eE".IndexOf(s[i]) >= 0) { i++; } if (i == num || !double.TryParse(s.Substring(num, i - num), NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { throw new FormatException("Unexpected character in JSON."); } return result; } } } namespace ExtraPotions.Beacon.Servers { public static class FavoriteMove { public static bool Resolve(IReadOnlyList<int> visibleToFull, int fullCount, int selected, int offset, out int from, out int to) { from = -1; to = -1; if (visibleToFull == null || (offset != 1 && offset != -1)) { return false; } int num = selected + offset; if (selected < 0 || selected >= visibleToFull.Count || num < 0 || num >= visibleToFull.Count) { return false; } from = visibleToFull[selected]; to = visibleToFull[num]; if (from < 0 || from >= fullCount || to < 0 || to >= fullCount || from == to) { from = -1; to = -1; return false; } return true; } } public enum AddressKind { Host, Steam, PlayFab } public sealed class ServerAddress : IEquatable<ServerAddress> { public const int DefaultPort = 2456; public AddressKind Kind { get; } public string Host { get; } public int Port { get; } private ServerAddress(AddressKind kind, string host, int port) { Kind = kind; Host = host; Port = port; } public static ServerAddress Parse(string text) { if (!TryParse(text, out ServerAddress address)) { throw new FormatException("Not a server address."); } return address; } public static bool TryParse(string? text, out ServerAddress? address) { address = null; string text2 = (text ?? "").Trim(); if (text2.Length == 0) { return false; } string text3 = text2.ToLowerInvariant(); if (text3.StartsWith("steam:", StringComparison.Ordinal)) { string text4 = text2.Substring(6); if (text4.Length == 0 || text4.Length > 20 || !AllDigits(text4)) { return false; } address = new ServerAddress(AddressKind.Steam, text4, 0); return true; } if (text3.StartsWith("playfab:", StringComparison.Ordinal)) { string text5 = text2.Substring(8); if (text5.Length == 0 || text5.Length > 64 || !AllAlphaNumeric(text5)) { return false; } address = new ServerAddress(AddressKind.PlayFab, text5.ToUpperInvariant(), 0); return true; } int port = 2456; string text6; if (text3[0] == '[') { int num = text3.IndexOf(']'); if (num < 2) { return false; } text6 = text3.Substring(1, num - 1); string text7 = text3.Substring(num + 1); if (text7.Length > 0 && (text7[0] != ':' || !TryPort(text7.Substring(1), out port))) { return false; } if (text6.IndexOf(':') < 0) { return false; } } else { int num2 = Count(text3, ':'); if (num2 > 1) { text6 = text3; } else if (num2 == 1) { int num3 = text3.IndexOf(':'); text6 = text3.Substring(0, num3); if (!TryPort(text3.Substring(num3 + 1), out port)) { return false; } } else { text6 = text3; } } if (!ValidHost(text6)) { return false; } address = new ServerAddress(AddressKind.Host, text6, port); return true; } public override string ToString() { return Kind switch { AddressKind.Steam => "steam:" + Host, AddressKind.PlayFab => "playfab:" + Host, _ => ((Host.IndexOf(':') >= 0) ? ("[" + Host + "]") : Host) + ":" + Port.ToString(CultureInfo.InvariantCulture), }; } public bool Equals(ServerAddress? other) { if ((object)other != null) { return string.Equals(ToString(), other.ToString(), StringComparison.Ordinal); } return false; } public override bool Equals(object? obj) { return Equals(obj as ServerAddress); } public override int GetHashCode() { return StringComparer.Ordinal.GetHashCode(ToString()); } public static bool operator ==(ServerAddress? a, ServerAddress? b) { return a?.Equals(b) ?? ((object)b == null); } public static bool operator !=(ServerAddress? a, ServerAddress? b) { return !(a == b); } private static bool TryPort(string text, out int port) { if (int.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out port) && port >= 1) { return port <= 65535; } return false; } private static int Count(string s, char c) { int num = 0; for (int i = 0; i < s.Length; i++) { if (s[i] == c) { num++; } } return num; } private static bool AllDigits(string s) { foreach (char c in s) { if (c < '0' || c > '9') { return false; } } return true; } private static bool AllAlphaNumeric(string s) { foreach (char c in s) { if ((c < '0' || c > '9') && (c < 'a' || c > 'z') && (c < 'A' || c > 'Z')) { return false; } } return true; } private static bool ValidHost(string host) { if (host.Length == 0 || host.Length > 253) { return false; } bool flag = host.IndexOf(':') >= 0; foreach (char c in host) { bool num; if (!flag) { if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || c == '.' || c == '-') { continue; } num = c == '_'; } else { if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || c == ':') { continue; } num = c == '.'; } if (!num) { return false; } } return true; } } public sealed class ServerEntry { public string Name { get; } public ServerAddress Address { get; } public IReadOnlyList<ServerAddress> Aliases { get; } public string SecretRef { get; } public bool IsDefault { get; internal set; } public IEnumerable<ServerAddress> AllAddresses => new ServerAddress[1] { Address }.Concat(Aliases); internal ServerEntry(string name, ServerAddress address, IReadOnlyList<ServerAddress> aliases, string secretRef) { Name = name; Address = address; Aliases = aliases; SecretRef = secretRef; } public bool Matches(ServerAddress address) { if (!(Address == address)) { return Aliases.Any((ServerAddress a) => a == address); } return true; } } public sealed class ServerRemoval { public ServerEntry Entry { get; } public int Index { get; } public bool WasDefault { get; } internal ServerRemoval(ServerEntry entry, int index) { Entry = entry; Index = index; WasDefault = entry.IsDefault; } } public sealed class ServerList { private readonly List<ServerEntry> entries = new List<ServerEntry>(); private readonly Func<string> newReference; private int revision; public IReadOnlyList<ServerEntry> Entries => entries; public int Count => entries.Count; public int Revision => revision; public ServerEntry? Default => entries.FirstOrDefault((ServerEntry e) => e.IsDefault); public ServerList(Func<string>? newReference = null) { this.newReference = newReference ?? ((Func<string>)(() => "s" + Guid.NewGuid().ToString("N"))); } public ServerEntry? Find(ServerAddress address) { if (!(address == null)) { return entries.FirstOrDefault((ServerEntry e) => e.Matches(address)); } return null; } public ServerEntry? FindByName(string name) { return entries.FirstOrDefault((ServerEntry e) => string.Equals(e.Name, (name ?? "").Trim(), StringComparison.OrdinalIgnoreCase)); } public ServerEntry Add(string name, ServerAddress address, IEnumerable<ServerAddress>? aliases = null) { return Insert(name, address, aliases, null); } internal ServerEntry Insert(string name, ServerAddress address, IEnumerable<ServerAddress>? aliases, string? secretRef) { string text = (name ?? "").Trim(); if (text.Length == 0) { throw new ArgumentException("A server needs a name.", "name"); } if (address == null) { throw new ArgumentNullException("address"); } if (FindByName(text) != null) { throw new ArgumentException("A server named '" + text + "' is already saved.", "name"); } List<ServerAddress> list = (aliases ?? Enumerable.Empty<ServerAddress>()).Where((ServerAddress a) => a != null && a != address).Distinct().ToList(); foreach (ServerAddress item in new ServerAddress[1] { address }.Concat(list)) { if (Find(item) != null) { throw new ArgumentException("Address " + item?.ToString() + " already belongs to another saved server.", "address"); } } string reference = secretRef ?? NewUniqueReference(); if (entries.Any((ServerEntry e) => e.SecretRef == reference)) { throw new ArgumentException("Duplicate secret reference.", "secretRef"); } ServerEntry serverEntry = new ServerEntry(text, address, list.AsReadOnly(), reference); entries.Add(serverEntry); revision++; return serverEntry; } public bool Remove(string name) { ServerEntry serverEntry = FindByName(name); if (serverEntry == null || !entries.Remove(serverEntry)) { return false; } revision++; return true; } public ServerRemoval? RemoveForUndo(string name) { ServerEntry serverEntry = FindByName(name); if (serverEntry == null) { return null; } ServerRemoval result = new ServerRemoval(serverEntry, entries.IndexOf(serverEntry)); entries.Remove(serverEntry); revision++; return result; } public ServerEntry Restore(ServerRemoval removal) { if (removal == null) { throw new ArgumentNullException("removal"); } ServerEntry entry = removal.Entry; ServerEntry serverEntry = Insert(entry.Name, entry.Address, entry.Aliases, entry.SecretRef); Move(serverEntry.Name, removal.Index); if (removal.WasDefault && Default == null) { SetDefault(serverEntry.Name); } return serverEntry; } public ServerEntry Edit(string name, string nextName, ServerAddress address, IEnumerable<ServerAddress>? aliases = null) { ServerEntry original = FindByName(name) ?? throw new ArgumentException("Unknown saved server.", "name"); string trimmed = (nextName ?? "").Trim(); if (trimmed.Length == 0) { throw new ArgumentException("A server needs a name.", "nextName"); } if (address == null) { throw new ArgumentNullException("address"); } List<ServerAddress> list = (aliases ?? Enumerable.Empty<ServerAddress>()).Where((ServerAddress a) => a != null && a != address).Distinct().ToList(); List<ServerEntry> remaining = entries.Where((ServerEntry e) => e != original).ToList(); if (remaining.Any((ServerEntry e) => string.Equals(e.Name, trimmed, StringComparison.OrdinalIgnoreCase))) { throw new ArgumentException("Duplicate server name.", "nextName"); } if (new ServerAddress[1] { address }.Concat(list).Any((ServerAddress a) => remaining.Any((ServerEntry e) => e.Matches(a)))) { throw new ArgumentException("Address already belongs to another server.", "address"); } ServerEntry serverEntry = new ServerEntry(trimmed, address, list.AsReadOnly(), original.SecretRef) { IsDefault = original.IsDefault }; entries[entries.IndexOf(original)] = serverEntry; revision++; return serverEntry; } public void SetDefault(string? name) { ServerEntry serverEntry = ((name == null) ? null : FindByName(name)); if (name != null && serverEntry == null) { throw new ArgumentException("No saved server named '" + name + "'.", "name"); } foreach (ServerEntry entry in entries) { entry.IsDefault = entry == serverEntry; } revision++; } public void Move(string name, int index) { ServerEntry item = FindByName(name) ?? throw new ArgumentException("No saved server named '" + name + "'.", "name"); entries.Remove(item); entries.Insert(Math.Max(0, Math.Min(entries.Count, index)), item); revision++; } private string NewUniqueReference() { for (int i = 0; i < 16; i++) { string r = newReference(); if (entries.All((ServerEntry e) => e.SecretRef != r)) { return r; } } throw new InvalidOperationException("Could not create a unique secret reference."); } } public static class ServerListCodec { public const int Schema = 1; public static IReadOnlyList<string> FieldNames { get; } = Array.AsReadOnly(new string[7] { "schema", "servers", "name", "address", "aliases", "default", "ref" }); public static string Write(ServerList list) { if (list == null) { throw new ArgumentNullException("list"); } StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("{\n \"schema\": ").Append(1.ToString(CultureInfo.InvariantCulture)).Append(",\n \"servers\": ["); for (int i = 0; i < list.Count; i++) { ServerEntry serverEntry = list.Entries[i]; stringBuilder.Append((i == 0) ? "\n" : ",\n"); stringBuilder.Append(" {\"name\": ").Append(MiniJson.Quote(serverEntry.Name)).Append(", \"address\": ") .Append(MiniJson.Quote(serverEntry.Address.ToString())) .Append(", \"aliases\": [") .Append(string.Join(", ", serverEntry.Aliases.Select((ServerAddress a) => MiniJson.Quote(a.ToString())))) .Append(']') .Append(", \"default\": ") .Append(serverEntry.IsDefault ? "true" : "false") .Append(", \"ref\": ") .Append(MiniJson.Quote(serverEntry.SecretRef)) .Append('}'); } stringBuilder.Append((list.Count == 0) ? "]\n}\n" : "\n ]\n}\n"); return stringBuilder.ToString(); } public static ServerList Read(string text) { ServerList serverList = new ServerList(); if (string.IsNullOrWhiteSpace(text)) { return serverList; } Dictionary<string, object> obj = (MiniJson.Parse(text) as Dictionary<string, object>) ?? throw new FormatException("servers.json: expected an object."); if (!obj.TryGetValue("schema", out var value) || !(value is double num) || num != 1.0) { throw new FormatException("servers.json: unknown schema."); } if (!obj.TryGetValue("servers", out var value2) || value2 == null) { return serverList; } List<object> obj2 = (value2 as List<object>) ?? throw new FormatException("servers.json: 'servers' must be an array."); string text2 = null; bool flag = default(bool); foreach (object item in obj2) { if (!(item is Dictionary<string, object> dictionary)) { throw new FormatException("servers.json: a server must be an object."); } string text3 = Str(dictionary, "name") ?? throw new FormatException("servers.json: a server has no name."); ServerAddress address = Address(Str(dictionary, "address")); List<ServerAddress> list = new List<ServerAddress>(); if (dictionary.TryGetValue("aliases", out var value3) && value3 is List<object> list2) { foreach (object item2 in list2) { list.Add(Address(item2 as string)); } } string text4 = Str(dictionary, "ref"); if (!SecretReference.IsValid(text4)) { throw new FormatException("servers.json: server '" + text3 + "' has no valid ref."); } try { serverList.Insert(text3, address, list, text4); } catch (ArgumentException ex) { throw new FormatException("servers.json: " + ex.Message); } int num2; if (dictionary.TryGetValue("default", out var value4)) { if (value4 is bool) { flag = (bool)value4; num2 = 1; } else { num2 = 0; } } else { num2 = 0; } if (((uint)num2 & (flag ? 1u : 0u)) != 0) { text2 = text3; } } if (text2 != null) { serverList.SetDefault(text2); } return serverList; } private static string? Str(Dictionary<string, object?> o, string key) { if (!o.TryGetValue(key, out object value)) { return null; } return value as string; } private static ServerAddress Address(string? text) { if (!ServerAddress.TryParse(text, out ServerAddress address)) { throw new FormatException("servers.json: invalid address."); } return address; } } } namespace ExtraPotions.Beacon.Secrets { public readonly struct SecretReadResult { private readonly string? secret; public bool Found { get; } public string Secret => secret ?? ""; public SecretReadResult(bool found, string secret) { Found = found; this.secret = (found ? secret : ""); } } public interface IAsyncSecretStore : ISecretStore { Task<bool> RefreshAsync(CancellationToken cancellation = default(CancellationToken)); Task<bool> StoreAsync(string reference, string secret, CancellationToken cancellation = default(CancellationToken)); Task<SecretReadResult> ReadAsync(string reference, CancellationToken cancellation = default(CancellationToken)); Task<bool> ForgetAsync(string reference, CancellationToken cancellation = default(CancellationToken)); Task<int> ForgetAllAsync(CancellationToken cancellation = default(CancellationToken)); } public sealed class DpapiSecretStore : ISecretStore { private struct DataBlob { public int Size; public IntPtr Data; public static DataBlob From(byte[] bytes) { DataBlob result = new DataBlob { Size = bytes.Length, Data = Marshal.AllocHGlobal(Math.Max(1, bytes.Length)) }; Marshal.Copy(bytes, 0, result.Data, bytes.Length); return result; } public void Free(bool clear) { if (!(Data == IntPtr.Zero)) { if (clear) { Zero(Data, Size); } Marshal.FreeHGlobal(Data); Data = IntPtr.Zero; } } } private const int UiForbidden = 1; private static readonly byte[] Entropy = Encoding.UTF8.GetBytes("com.meadworks.beacon/secrets/1"); private readonly Func<string?> readFile; private readonly Action<string> writeFile; private readonly SortedDictionary<string, string> blobs = new SortedDictionary<string, string>(StringComparer.Ordinal); public static bool IsSupported => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); public SecretBackend Backend => SecretBackend.WindowsDataProtection; public SecretAvailability Availability { get { if (!IsSupported) { return SecretAvailability.Unavailable; } return SecretAvailability.Available; } } public SecretFailure LastFailure { get; private set; } public DpapiSecretStore(Func<string?> readFile, Action<string> writeFile) { this.readFile = readFile ?? throw new ArgumentNullException("readFile"); this.writeFile = writeFile ?? throw new ArgumentNullException("writeFile"); Load(); } public bool Store(string reference, string secret) { SecretReference.Require(reference, secret); LastFailure = SecretFailure.None; if (!IsSupported) { LastFailure = SecretFailure.Unavailable; return false; } byte[] bytes = Encoding.UTF8.GetBytes(secret); try { byte[] array = Transform(bytes, protect: true); if (array == null) { LastFailure = SecretFailure.ProtectFailed; return false; } blobs[reference] = Convert.ToBase64String(array); Save(); return true; } finally { Array.Clear(bytes, 0, bytes.Length); } } public bool TryRead(string reference, out string secret) { SecretReference.Require(reference); secret = ""; LastFailure = SecretFailure.None; if (!blobs.TryGetValue(reference, out string value)) { return false; } if (!IsSupported) { LastFailure = SecretFailure.Unavailable; return false; } byte[] array = Transform(Convert.FromBase64String(value), protect: false); if (array == null) { LastFailure = SecretFailure.StoredElsewhere; return false; } try { secret = Encoding.UTF8.GetString(array); return true; } finally { Array.Clear(array, 0, array.Length); } } public bool Contains(string reference) { if (SecretReference.IsValid(reference)) { return blobs.ContainsKey(reference); } return false; } public bool Forget(string reference) { SecretReference.Require(reference); if (!blobs.Remove(reference)) { return false; } Save(); return true; } public int ForgetAll() { int count = blobs.Count; blobs.Clear(); Save(); return count; } private void Load() { string[] array = (readFile() ?? "").Split('\n'); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); int num = text.IndexOf('='); if (num > 0) { string text2 = text.Substring(0, num); string text3 = text.Substring(num + 1); if (SecretReference.IsValid(text2) && IsBase64(text3)) { blobs[text2] = text3; } } } } private void Save() { StringBuilder stringBuilder = new StringBuilder(); foreach (KeyValuePair<string, string> blob in blobs) { stringBuilder.Append(blob.Key).Append('=').Append(blob.Value) .Append('\n'); } writeFile(stringBuilder.ToString()); } private static bool IsBase64(string text) { if (text.Length == 0) { return false; } try { Convert.FromBase64String(text); return true; } catch (FormatException) { return false; } } private static byte[]? Transform(byte[] input, bool protect) { DataBlob dataIn = default(DataBlob); DataBlob entropy = default(DataBlob); DataBlob dataOut = default(DataBlob); try { dataIn = DataBlob.From(input); entropy = DataBlob.From(Entropy); if (!(protect ? CryptProtectData(ref dataIn, null, ref entropy, IntPtr.Zero, IntPtr.Zero, 1, ref dataOut) : CryptUnprotectData(ref dataIn, IntPtr.Zero, ref entropy, IntPtr.Zero, IntPtr.Zero, 1, ref dataOut)) || dataOut.Data == IntPtr.Zero) { return null; } byte[] array = new byte[dataOut.Size]; Marshal.Copy(dataOut.Data, array, 0, dataOut.Size); return array; } finally { dataIn.Free(clear: true); entropy.Free(clear: false); if (dataOut.Data != IntPtr.Zero) { Zero(dataOut.Data, dataOut.Size); LocalFree(dataOut.Data); } } } private static void Zero(IntPtr data, int size) { for (int i = 0; i < size; i++) { Marshal.WriteByte(data, i, 0); } } [DllImport("crypt32.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern bool CryptProtectData(ref DataBlob dataIn, string? description, ref DataBlob entropy, IntPtr reserved, IntPtr prompt, int flags, ref DataBlob dataOut); [DllImport("crypt32.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern bool CryptUnprotectData(ref DataBlob dataIn, IntPtr description, ref DataBlob entropy, IntPtr reserved, IntPtr prompt, int flags, ref DataBlob dataOut); [DllImport("kernel32.dll")] private static extern IntPtr LocalFree(IntPtr memory); } internal static class LibSecretNative { [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate uint Hash(IntPtr text); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate int Equal(IntPtr a, IntPtr b); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void Free(IntPtr value); private struct ListNode { internal IntPtr Data; internal IntPtr Next; internal IntPtr Previous; } private sealed class Call : IDisposable { internal IntPtr Schema; internal IntPtr Attributes; internal IntPtr Cancellable; private CancellationTokenRegistration registration; internal Call(string? reference, CancellationToken token) { token.ThrowIfCancellationRequested(); try { IntPtr intPtr = g_hash_table_new_full(hash, equal, free, null); try { g_hash_table_insert(intPtr, g_strdup("ref"), IntPtr.Zero); Schema = secret_schema_newv("com.meadworks.beacon", 0, intPtr); } finally { g_hash_table_unref(intPtr); } Attributes = g_hash_table_new_full(hash, equal, free, free); if (reference != null) { g_hash_table_insert(Attributes, g_strdup("ref"), g_strdup(reference)); } Cancellable = g_cancellable_new(); if (Schema == IntPtr.Zero || Attributes == IntPtr.Zero || Cancellable == IntPtr.Zero) { throw new KeyringUnavailableException(); } registration = token.Register(delegate { g_cancellable_cancel(Cancellable); }); } catch { Dispose(); throw; } } public void Dispose() { registration.Dispose(); if (Cancellable != IntPtr.Zero) { g_object_unref(Cancellable); Cancellable = IntPtr.Zero; } if (Attributes != IntPtr.Zero) { g_hash_table_unref(Attributes); Attributes = IntPtr.Zero; } if (Schema != IntPtr.Zero) { secret_schema_unref(Schema); Schema = IntPtr.Zero; } } } private const string Secret = "libsecret-1.so.0"; private const string Glib = "libglib-2.0.so.0"; private const string Gio = "libgio-2.0.so.0"; private const string Gobject = "libgobject-2.0.so.0"; private const int SearchAll = 2; private static readonly Hash hash = g_str_hash; private static readonly Equal equal = g_str_equal; private static readonly Free free = g_free; internal static bool Store(string reference, string password, CancellationToken token) { using Call call = new Call(reference, token); IntPtr error; int num = secret_password_storev_sync(call.Schema, call.Attributes, "default", "Meadworks Beacon server password", password, call.Cancellable, out error); Check(error, token); return num != 0; } internal static SecretReadResult Read(string reference, CancellationToken token) { using Call call = new Call(reference, token); IntPtr error; IntPtr intPtr = secret_password_lookupv_sync(call.Schema, call.Attributes, call.Cancellable, out error); try { Check(error, token); return (intPtr == IntPtr.Zero) ? default(SecretReadResult) : new SecretReadResult(found: true, Marshal.PtrToStringUTF8(intPtr) ?? ""); } finally { if (intPtr != IntPtr.Zero) { secret_password_free(intPtr); } } } internal static bool Forget(string reference, CancellationToken token) { using Call call = new Call(reference, token); IntPtr error; int num = secret_password_clearv_sync(call.Schema, call.Attributes, call.Cancellable, out error); Check(error, token); return num != 0; } internal static HashSet<string> References(CancellationToken token) { using Call call = new Call(null, token); IntPtr error; IntPtr intPtr = secret_password_searchv_sync(call.Schema, call.Attributes, 2, call.Cancellable, out error); HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); try { Check(error, token); IntPtr intPtr2 = intPtr; while (intPtr2 != IntPtr.Zero) { ListNode listNode = Marshal.PtrToStructure<ListNode>(intPtr2); IntPtr intPtr3 = secret_retrievable_get_attributes(listNode.Data); try { string text = Marshal.PtrToStringUTF8(g_hash_table_lookup(intPtr3, "ref")); if (SecretReference.IsValid(text)) { hashSet.Add(text); } } finally { if (intPtr3 != IntPtr.Zero) { g_hash_table_unref(intPtr3); } } intPtr2 = listNode.Next; } return hashSet; } finally { IntPtr intPtr4 = intPtr; while (intPtr4 != IntPtr.Zero) { ListNode listNode2 = Marshal.PtrToStructure<ListNode>(intPtr4); g_object_unref(listNode2.Data); intPtr4 = listNode2.Next; } if (intPtr != IntPtr.Zero) { g_list_free(intPtr); } } } private static void Check(IntPtr error, CancellationToken token) { if (error != IntPtr.Zero) { g_error_free(error); } token.ThrowIfCancellationRequested(); if (error != IntPtr.Zero) { throw new KeyringUnavailableException(); } } [DllImport("libsecret-1.so.0", CallingConvention = CallingConvention.Cdecl)] private static extern IntPtr secret_schema_newv([MarshalAs(UnmanagedType.LPUTF8Str)] string name, int flags, IntPtr types); [DllImport("libsecret-1.so.0", CallingConvention = CallingConvention.Cdecl)] private static extern void secret_schema_unref(IntPtr schema); [DllImport("libsecret-1.so.0", CallingConvention = CallingConvention.Cdecl)] private static extern int secret_password_storev_sync(IntPtr schema, IntPtr attrs, [MarshalAs(UnmanagedType.LPUTF8Str)] string collection, [MarshalAs(UnmanagedType.LPUTF8Str)] string label, [MarshalAs(UnmanagedType.LPUTF8Str)] string password, IntPtr cancel, out IntPtr error); [DllImport("libsecret-1.so.0", CallingConvention = CallingConvention.Cdecl)] private static extern IntPtr secret_password_lookupv_sync(IntPtr schema, IntPtr attrs, IntPtr cancel, out IntPtr error); [DllImport("libsecret-1.so.0", CallingConvention = CallingConvention.Cdecl)] private static extern int secret_password_clearv_sync(IntPtr schema, IntPtr attrs, IntPtr cancel, out IntPtr error); [DllImport("libsecret-1.so.0", CallingConvention = CallingConvention.Cdecl)] private static extern IntPtr secret_password_searchv_sync(IntPtr schema, IntPtr attrs, int flags, IntPtr cancel, out IntPtr error); [DllImport("libsecret-1.so.0", CallingConvention = CallingConvention.Cdecl)] private static extern IntPtr secret_retrievable_get_attributes(IntPtr item); [DllImport("libsecret-1.so.0", CallingConvention = CallingConvention.Cdecl)] private static extern void secret_password_free(IntPtr secret); [DllImport("libglib-2.0.so.0", CallingConvention = CallingConvention.Cdecl)] private static extern IntPtr g_hash_table_new_full(Hash hash, Equal equal, Free keyFree, Free? valueFree); [DllImport("libglib-2.0.so.0", CallingConvention = CallingConvention.Cdecl)] private static extern int g_hash_table_insert(IntPtr table, IntPtr key, IntPtr value); [DllImport("libglib-2.0.so.0", CallingConvention = CallingConvention.Cdecl)] private static extern IntPtr g_hash_table_lookup(IntPtr table, [MarshalAs(UnmanagedType.LPUTF8Str)] string key); [DllImport("libglib-2.0.so.0", CallingConvention = CallingConvention.Cdecl)] private static extern void g_hash_table_unref(IntPtr table); [DllImport("libglib-2.0.so.0", CallingConvention = CallingConvention.Cdecl)] private static extern uint g_str_hash(IntPtr value); [DllImport("libglib-2.0.so.0", CallingConvention = CallingConvention.Cdecl)] private static extern int g_str_equal(IntPtr a, IntPtr b); [DllImport("libglib-2.0.so.0", CallingConvention = CallingConvention.Cdecl)] private static extern IntPtr g_strdup([MarshalAs(UnmanagedType.LPUTF8Str)] string value); [DllImport("libglib-2.0.so.0", CallingConvention = CallingConvention.Cdecl)] private static extern void g_free(IntPtr value); [DllImport("libglib-2.0.so.0", CallingConvention = CallingConvention.Cdecl)] private static extern void g_error_free(IntPtr error); [DllImport("libglib-2.0.so.0", CallingConvention = CallingConvention.Cdecl)] private static extern void g_list_free(IntPtr list); [DllImport("libgio-2.0.so.0", CallingConvention = CallingConvention.Cdecl)] private static extern IntPtr g_cancellable_new(); [DllImport("libgio-2.0.so.0", CallingConvention = CallingConvention.Cdecl)] private static extern void g_cancellable_cancel(IntPtr value); [DllImport("libgobject-2.0.so.0", CallingConvention = CallingConvention.Cdecl)] private static extern void g_object_unref(IntPtr value); } public sealed class LinuxKeyringStore : IAsyncSecretStore, ISecretStore, IDisposable { private readonly ISecretStore session = MemorySecretStore.SessionOnly(); private readonly ISecretStore disabled = new DisabledSecretStore(); private LinuxFallback selectedFallback; private long fallbackRevision; private readonly SemaphoreSlim gate = new SemaphoreSlim(1); private readonly CancellationTokenSource lifetime = new CancellationTokenSource(); private readonly HashSet<string> references = new HashSet<string>(StringComparer.Ordinal); private volatile bool unavailable; private volatile bool disposed; private ISecretStore FallbackStore { get { if (selectedFallback != LinuxFallback.Disabled) { return session; } return disabled; } } public SecretBackend Backend { get { lock (references) { return (!unavailable) ? SecretBackend.LinuxKeyring : FallbackStore.Backend; } } } public SecretAvailability Availability { get { lock (references) { return unavailable ? FallbackStore.Availability : SecretAvailability.Available; } } } public long FallbackRevision { get { lock (references) { return fallbackRevision; } } } public SecretFailure LastFailure { get; private set; } public LinuxKeyringStore(LinuxFallback selectedFallback) { SetFallback(selectedFallback); } public void SetFallback(LinuxFallback policy) { if (policy != LinuxFallback.SessionOnly && policy != LinuxFallback.Disabled) { throw new ArgumentOutOfRangeException("policy"); } lock (references) { if (!disposed && selectedFallback != policy) { selectedFallback = policy; fallbackRevision++; } } } public bool IsReadCurrent(long revision) { lock (references) { return !disposed && (!unavailable || (selectedFallback == LinuxFallback.SessionOnly && fallbackRevision == revision)); } } public bool Contains(string reference) { if (!SecretReference.IsValid(reference) || disposed) { return false; } lock (references) { return unavailable ? FallbackStore.Contains(reference) : references.Contains(reference); } } public bool Store(string reference, string secret) { SecretReference.Require(reference, secret); return false; } public bool TryRead(string reference, out string secret) { SecretReference.Require(reference); secret = ""; return false; } public bool Forget(string reference) { SecretReference.Require(reference); return false; } public int ForgetAll() { return 0; } public Task<bool> RefreshAsync(CancellationToken cancellation = default(CancellationToken)) { return Run(delegate(CancellationToken token) { HashSet<string> other = LibSecretNative.References(token); lock (references) { if (disposed || token.IsCancellationRequested) { return false; } references.Clear(); references.UnionWith(other); } return true; }, () => false, canceled: false, cancellation); } public Task<bool> StoreAsync(string reference, string secret, CancellationToken cancellation = default(CancellationToken)) { SecretReference.Require(reference, secret); if (secret.IndexOf('\0') >= 0) { throw new ArgumentException("A password cannot contain a NUL character.", "secret"); } return Run(delegate(CancellationToken token) { bool flag = LibSecretNative.Store(reference, secret, token); if (flag) { lock (references) { if (disposed || token.IsCancellationRequested) { return false; } references.Add(reference); } } return flag; }, delegate { lock (references) { return FallbackStore.Store(reference, secret); } }, canceled: false, cancellation); } public Task<SecretReadResult> ReadAsync(string reference, CancellationToken cancellation = default(CancellationToken)) { SecretReference.Require(reference); return Run((CancellationToken token) => LibSecretNative.Read(reference, token), delegate { lock (references) { string secret; return FallbackStore.TryRead(reference, out secret) ? new SecretReadResult(found: true, secret) : default(SecretReadResult); } }, default(SecretReadResult), cancellation); } public Task<bool> ForgetAsync(string reference, CancellationToken cancellation = default(CancellationToken)) { SecretReference.Require(reference); return Run(delegate(CancellationToken token) { bool flag = LibSecretNative.Forget(reference, token); if (flag) { lock (references) { references.Remove(reference); } } return flag; }, delegate { lock (references) { return FallbackStore.Forget(reference); } }, canceled: false, cancellation); } public Task<int> ForgetAllAsync(CancellationToken cancellation = default(CancellationToken)) { return Run(delegate(CancellationToken token) { HashSet<string> hashSet = LibSecretNative.References(token); int num = 0; foreach (string item in hashSet) { if (LibSecretNative.Forget(item, token)) { num++; lock (references) { references.Remove(item); } } } return num; }, delegate { lock (references) { return FallbackStore.ForgetAll(); } }, 0, cancellation); } private Task<T> Run<T>(Func<CancellationToken, T> operation, Func<T> onUnavailable, T canceled, CancellationToken cancellation) { long revision; lock (references) { if (disposed || cancellation.IsCancellationRequested) { return Task.FromResult(canceled); } revision = fallbackRevision; } return Task.Run(async delegate { CancellationTokenSource linked = CancellationTokenSource.CreateLinkedTokenSource(cancellation, lifetime.Token); try { bool entered = false; try { await gate.WaitAsync(linked.Token).ConfigureAwait(continueOnCapturedContext: false); entered = true; if (disposed || linked.IsCancellationRequested) { return canceled; } if (unavailable) { return Fallback(); } using CancellationTokenSource cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(new CancellationToken[1] { linked.Token }); cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(5.0)); try { T result = operation(cancellationTokenSource.Token); if (disposed || linked.IsCancellationRequested) { return canceled; } LastFailure = SecretFailure.None; return result; } catch (Exception ex) when (ex is DllNotFoundException || ex is EntryPointNotFoundException || ex is BadImageFormatException || ex is KeyringUnavailableException || ex is OperationCanceledException) { if (linked.IsCancellationRequested) { return canceled; } unavailable = true; LastFailure = SecretFailure.Unavailable; return Fallback(); } } catch (OperationCanceledException) { return canceled; } finally { if (entered) { gate.Release(); } } } finally { if (linked != null) { ((IDisposable)linked).Dispose(); } } }); T Fallback() { lock (references) { if (disposed || P_0.linked.IsCancellationRequested || revision != fallbackRevision) { return canceled; } return onUnavailable(); } } } public void Dispose() { if (disposed) { return; } disposed = true; lifetime.Cancel(); lock (references) { references.Clear(); session.ForgetAll(); } } } internal sealed class KeyringUnavailableException : Exception { } public sealed class SecretRedactor { public const string Mask = "***"; private readonly Dictionary<string, int> secrets = new Dictionary<string, int>(StringComparer.Ordinal); public void Track(string? secret) { if (!string.IsNullOrEmpty(secret)) { secrets[secret] = ((!secrets.TryGetValue(secret, out var value)) ? 1 : (value + 1)); } } public void Untrack(string? secret) { if (!string.IsNullOrEmpty(secret) && secrets.TryGetValue(secret, out var value)) { if (value > 1) { secrets[secret] = value - 1; } else { secrets.Remove(secret); } } } public void Clear() { secrets.Clear(); } public string Clean(string? message) { if (string.IsNullOrEmpty(message)) { return ""; } string text = message; foreach (string item in secrets.Keys.OrderByDescending((string s) => s.Length)) { text = text.Replace(item, "***"); } return text; } } public enum SecretAvailability { Available, SessionOnly, Unavailable } public enum SecretBackend { WindowsDataProtection, LinuxKeyring, SessionOnly, Disabled, Memory } public enum SecretPlatform { Windows, Linux, Other } public enum LinuxFallback { SessionOnly, Disabled } public enum SecretFailure { None, StoredElsewhere, ProtectFailed, Unavailable } public interface ISecretStore { SecretBackend Backend { get; } SecretAvailability Availability { get; } SecretFailure LastFailure { get; } bool Store(string reference, string secret); bool TryRead(string reference, out string secret); bool Contains(string reference); bool Forget(string reference); int ForgetAll(); } public static class SecretReference { public static bool IsValid(string? reference) { if (string.IsNullOrEmpty(reference) || reference.Length > 64) { return false; } foreach (char c in reference) { if ((c < '0' || c > '9') && (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && c != '-' && c != '_') { return false; } } return true; } public static void Require(string reference, string secret) { if (!IsValid(reference)) { throw new ArgumentException("Invalid secret reference.", "reference"); } if (string.IsNullOrEmpty(secret)) { throw new ArgumentException("An empty secret is not stored.", "secret"); } } public static void Require(string reference) { if (!IsValid(reference)) { throw new ArgumentException("Invalid secret reference.", "reference"); } } } public sealed class MemorySecretStore : ISecretStore { private readonly Dictionary<string, string> secrets = new Dictionary<string, string>(StringComparer.Ordinal); public SecretBackend Backend { get; } public SecretAvailability Availability { get; } public SecretFailure LastFailure => SecretFailure.None; public MemorySecretStore(SecretBackend backend = SecretBackend.Memory, SecretAvailability availability = SecretAvailability.Available) { Backend = backend; Availability = availability; } public static MemorySecretStore SessionOnly() { return new MemorySecretStore(SecretBackend.SessionOnly, SecretAvailability.SessionOnly); } public bool Store(string reference, string secret) { SecretReference.Require(reference, secret); secrets[reference] = secret; return true; } public bool TryRead(string reference, out string secret) { SecretReference.Require(reference); if (secrets.TryGetValue(reference, out string value)) { secret = value; return true; } secret = ""; return false; } public bool Contains(string reference) { if (SecretReference.IsValid(reference)) { return secrets.ContainsKey(reference); } return false; } public bool Forget(string reference) { SecretReference.Require(reference); return secrets.Remove(reference); } public int ForgetAll() { int count = secrets.Count; secrets.Clear(); return count; } } public sealed class DisabledSecretStore : ISecretStore { public SecretBackend Backend => SecretBackend.Disabled; public SecretAvailability Availability => SecretAvailability.Unavailable; public SecretFailure LastFailure { get; private set; } public bool Store(string reference, string secret) { SecretReference.Require(reference, secret); LastFailure = SecretFailure.Unavailable; return false; } public bool TryRead(string reference, out string secret) { SecretReference.Require(reference); secret = ""; return false; } public bool Contains(string reference) { return false; } public bool Forget(string reference) { return false; } public int ForgetAll() { return 0; } } public static class SecretStores { public const LinuxFallback DefaultLinuxFallback = LinuxFallback.SessionOnly; public static SecretBackend Choose(SecretPlatform platform, bool keyringUsable, LinuxFallback fallback) { if (platform == SecretPlatform.Windows) { return SecretBackend.WindowsDataProtection; } if (platform == SecretPlatform.Linux && keyringUsable) { return SecretBackend.LinuxKeyring; } if (fallback != LinuxFallback.Disabled) { return SecretBackend.SessionOnly; } return SecretBackend.Disabled; } public static string StorageLineKey(SecretBackend backend) { return backend switch { SecretBackend.WindowsDataProtection => "storage_windows", SecretBackend.LinuxKeyring => "storage_keyring", SecretBackend.Disabled => "storage_disabled", _ => "storage_session", }; } } } namespace ExtraPotions.Beacon.Localization { public readonly struct TextKey { public string Key { get; } public int Arguments { get; } public TextKey(string key, int arguments) { Key = key; Arguments = arguments; } } public static class BeaconTextKeys { public const string StepAsideJoin = "step_aside_join"; public const string StorageWindows = "storage_windows"; public const string StorageKeyring = "storage_keyring"; public const string StorageSession = "storage_session"; public const string StorageDisabled = "storage_disabled"; public const string SecretStoreFailed = "secret_store_failed"; public const string SecretStoredElsewhere = "secret_stored_elsewhere"; public const string OfferSave = "offer_save"; public const string PasswordSaved = "password_saved"; public const string PasswordForgotten = "password_forgotten"; public const string PasswordsForgottenAll = "passwords_forgotten_all"; public const string AutoJoinCountdown = "autojoin_countdown"; public const string AutoJoinCancelled = "autojoin_canceled"; public const string AutoJoinUnknownServer = "autojoin_unknown_server"; public const string Reconnecting = "reconnecting"; public const string ReconnectLocked = "reconnect_locked"; public const string FavoriteRemoved = "favorite_removed"; public const string ImportNone = "import_none"; public const string ImportDone = "import_done"; public const string ImportPasswordNotStored = "import_password_not_stored"; public const string ImportSourcePlain = "import_source_plain"; public const string ImportBlankDone = "import_blank_done"; public const string CommandHelp = "cmd_help"; public const string CommandStatus = "cmd_status"; public const string CommandUsage = "cmd_usage"; public const string CommandUnknownVerb = "cmd_unknown_verb"; public const string CommandConfirmBlank = "cmd_confirm_blank"; public const string CommandJoinUnknown = "cmd_join_unknown"; public const string CommandJoinNotReady = "cmd_join_not_ready"; public const string CommandJoinStarted = "cmd_join_started"; public const string ReconnectCancelled = "reconnect_canceled"; public const string ReconnectStopped = "reconnect_stopped"; public const string CommandCancelNone = "cmd_cancel_none"; public const string AutoJoinUnknownCharacter = "autojoin_unknown_character"; public const string CardAutoJoin = "card_autojoin"; public const string CardReconnect = "card_reconnect"; public const string CardCancelHint = "card_cancel_hint"; public static IReadOnlyList<TextKey> All { get; } = Array.AsReadOnly(new TextKey[102] { new TextKey("step_aside_join", 1), new TextKey("storage_windows", 0), new TextKey("storage_keyring", 0), new TextKey("storage_session", 0), new TextKey("storage_disabled", 0), new TextKey("secret_store_failed", 0), new TextKey("secret_stored_elsewhere", 1), new TextKey("offer_save", 1), new TextKey("password_saved", 1), new TextKey("password_forgotten", 1), new TextKey("passwords_forgotten_all", 1), new TextKey("autojoin_countdown", 2), new TextKey("autojoin_canceled", 0), new TextKey("autojoin_unknown_server", 1), new TextKey("reconnecting", 2), new TextKey("reconnect_locked", 0), new TextKey("favorite_removed", 1), new TextKey("import_none", 0), new TextKey("import_done", 2), new TextKey("import_password_not_stored", 1), new TextKey("import_source_plain", 0), new TextKey("import_blank_done", 1), new TextKey("cmd_help", 0), new TextKey("cmd_status", 3), new TextKey("cmd_usage", 0), new TextKey("cmd_unknown_verb", 1), new TextKey("cmd_confirm_blank", 0), new TextKey("cmd_join_unknown", 1), new TextKey("cmd_join_not_ready", 0), new TextKey("cmd_join_started", 1), new TextKey("reconnect_canceled", 0), new TextKey("reconnect_stopped", 1), new TextKey("cmd_cancel_none", 0), new TextKey("autojoin_unknown_character", 1), new TextKey("card_autojoin", 2), new TextKey("card_reconnect", 3), new TextKey("card_cancel_hint", 0), new TextKey("ui_servers", 0), new TextKey("ui_previous", 0), new TextKey("ui_next", 0), new TextKey("ui_add", 0), new TextKey("ui_edit", 0), new TextKey("ui_join", 0), new TextKey("ui_remove", 0), new TextKey("ui_up", 0), new TextKey("ui_down", 0), new TextKey("ui_default", 0), new TextKey("ui_close", 0), new TextKey("ui_forget", 0), new TextKey("ui_forget_all", 0), new TextKey("ui_confirm", 0), new TextKey("ui_cancel", 0), new TextKey("ui_save", 0), new TextKey("ui_name", 0), new TextKey("ui_address", 0), new TextKey("ui_aliases", 0), new TextKey("ui_password", 0), new TextKey("ui_password_hint", 0), new TextKey("ui_stored", 0), new TextKey("ui_empty", 0), new TextKey("ui_read_only", 0), new TextKey("ui_step_aside", 0), new TextKey("ui_write_failed", 0), new TextKey("ui_invalid", 0), new TextKey("ui_forget_all_confirm", 0), new TextKey("ui_alias_count", 1), new TextKey("ui_remove_confirm", 1), new TextKey("ui_forget_confirm", 1), new TextKey("ui_import", 0), new TextKey("import_preview_again", 0), new TextKey("import_write_failed", 0), new TextKey("import_password_pending", 0), new TextKey("ui_import_apply", 0), new TextKey("ui_import_blank", 0), new TextKey("ui_import_blank_confirm", 0), new TextKey("ui_import_results", 0), new TextKey("ui_undo", 0), new TextKey("ui_undo_conflict", 0), new TextKey("ui_favorite_save_failed", 0), new TextKey("ui_help_servers", 0), new TextKey("ui_help_previous", 0), new TextKey("ui_help_next", 0), new TextKey("ui_help_add", 0), new TextKey("ui_help_edit", 0), new TextKey("ui_help_join", 0), new TextKey("ui_help_remove", 0), new TextKey("ui_help_up", 0), new TextKey("ui_help_down", 0), new TextKey("ui_help_default", 0), new TextKey("ui_help_close", 0), new TextKey("ui_help_forget", 0), new TextKey("ui_help_forget_all", 0), new TextKey("ui_help_import", 0), new TextKey("ui_help_select", 0), new TextKey("ui_help_cancel", 0), new TextKey("ui_help_save", 0), new TextKey("ui_help_name", 0), new TextKey("ui_help_address", 0), new TextKey("ui_help_aliases", 0), new TextKey("ui_help_save_password", 0), new TextKey("ui_help_undo", 0), new TextKey("ui_help_import_apply", 0) }); } } namespace ExtraPotions.Beacon.Joining { public enum AutoJoinSkip { None, Disabled, LaunchConnect, AlreadyRan, NoServer, UnknownServer } public readonly struct AutoJoinDecision { public AutoJoinSkip Skip { get; } public ServerEntry? Target { get; } public AutoJoinDecision(AutoJoinSkip skip, ServerEntry? target) { Skip = skip; Target = target; } } public static class AutoJoinRules { public static AutoJoinDecision Decide(bool enabled, string? serverName, ServerList list, bool launchedWithConnect, bool alreadyRan) { if (list == null) { throw new ArgumentNullException("list"); } if (!enabled) { return new AutoJoinDecision(AutoJoinSkip.Disabled, null); } if (launchedWithConnect) { return new AutoJoinDecision(AutoJoinSkip.LaunchConnect, null); } if (alreadyRan) { return new AutoJoinDecision(AutoJoinSkip.AlreadyRan, null); } string text = (serverName ?? "").Trim(); if (text.Length == 0) { ServerEntry serverEntry = list.Default; if (serverEntry != null) { return new AutoJoinDecision(AutoJoinSkip.None, serverEntry); } return new AutoJoinDecision(AutoJoinSkip.NoServer, null); } ServerEntry serverEntry2 = list.FindByName(text); if (serverEntry2 != null) { return new AutoJoinDecision(AutoJoinSkip.None, serverEntry2); } return new AutoJoinDecision(AutoJoinSkip.UnknownServer, null); } public static bool LaunchJoins(IReadOnlyList<string> args) { if (args == null) { throw new ArgumentNullException("args"); } for (int i = 0; i < args.Count - 1; i++) { switch (args[i]) { case "+connect": case "+connect_lobby": case "-joinserverwithcharacter": case "-joincode": return true; } } return false; } public static int PickCharacter(IReadOnlyList<string> names, IReadOnlyList<string> files, string? wanted, int selected) { if (names == null) { throw new ArgumentNullException("names"); } if (files == null) { throw new ArgumentNullException("files"); } if (names.Count == 0) { return -1; } string text = (wanted ?? "").Trim(); if (text.Length == 0) { if (selected < 0 || selected >= names.Count) { return 0; } return selected; } for (int i = 0; i < names.Count; i++) { if (string.Equals((names[i] ?? "").Trim(), text, StringComparison.OrdinalIgnoreCase)) { return i; } } for (int j = 0; j < files.Count && j < names.Count; j++) { if (string.Equals((files[j] ?? "").Trim(), text, StringComparison.OrdinalIgnoreCase)) { return j; } } return -1; } } public enum CountdownState { Counting, Join, Cancelled } public sealed class AutoJoinCountdown { public const int MaxSeconds = 30; private float remaining; private CountdownState state; public int SecondsShown => (int)Math.Ceiling(Math.Max(0f, remaining)); public CountdownState State => state; public AutoJoinCountdown(int seconds) { remaining = Math.Max(0, Math.Min(30, seconds)); } public CountdownState Tick(float deltaSeconds) { if (state != CountdownState.Counting) { return state; } remaining -= Math.Max(0f, deltaSeconds); if (remaining <= 0f) { state = CountdownState.Join; } return state; } public void Cancel() { if (state == CountdownState.Counting) { state = CountdownState.Cancelled; } } } public enum DisconnectKind { Unexpected, Kicked, Banned, PlayerQuit, WrongPassword, VersionMismatch, ServerFull, ConnectFailed, Refused, Unknown } public enum ReconnectDecision { Reconnect, Off, Locked, NotAfterThis, OutOfAttempts } public static class ReconnectRules { public const int MinAttempts = 1; public const int MaxAttempts = 10; public static ReconnectDecision Decide(bool enabled, bool lockedByServer, DisconnectKind kind, int attemptsSoFar, int maxAttempts) { if (!enabled) { return ReconnectDecision.Off; } if (lockedByServer) { return ReconnectDecision.Locked; } if (kind != DisconnectKind.Unexpected) { return ReconnectDecision.NotAfterThis; } int num = Math.Max(1, Math.Min(10, maxAttempts)); if (attemptsSoFar < num) { return ReconnectDecision.Reconnect; } return ReconnectDecision.OutOfAttempts; } public static int DelaySeconds(int attemptIndex) { if (attemptIndex > 0) { if (attemptIndex < 4) { return 5 << attemptIndex; } return 60; } return 5; } } public readonly struct CardLine { public string Key { get; } public IReadOnlyList<object> Arguments { get; } public CardLine(string key, params object[] arguments) { Key = key; Arguments = Array.AsReadOnly(arguments ?? Array.Empty<object>()); } } public static class JoinCard { public static IReadOnlyList<CardLine> Countdown(string serverName, int seconds) { return new CardLine[2] { new CardLine("card_autojoin", serverName ?? "", Math.Max(0, seconds)), new CardLine("card_cancel_hint") }; } public static IReadOnlyList<CardLine> Reconnect(int attempt, int limit, int seconds) { return new CardLine[2] { new CardLine("card_reconnect", attempt, limit, Math.Max(0, seconds)), new CardLine("card_cancel_hint") }; } } public enum JoinAction { None, SendStored, AskPlayer, ReconnectAndAsk, StoreTyped, OfferSave } public sealed class JoinAttempt { private readonly bool hasStored; private readonly bool skipPrompt; private readonly bool offerToSave; private readonly bool alreadyOffered; private readonly bool storedRejected; private bool sentStored; private bool typed; public ServerEntry? Entry { get; } public JoinAttempt(ServerEntry? entry, bool hasStored, bool skipPrompt, bool offerToSave, bool alreadyOffered) : this(entry, hasStored, skipPrompt, offerToSave, alreadyOffered, storedRejected: false) { } private JoinAttempt(ServerEntry? entry, bool hasStored, bool skipPrompt, bool offerToSave, bool alreadyOffered, bool storedRejected) { Entry = entry; this.hasStored = hasStored; this.skipPrompt = skipPrompt; this.offerToSave = offerToSave; this.alreadyOffered = alreadyOffered; this.storedRejected = storedRejected; } public JoinAction OnPasswordRequested() { if (Entry != null && hasStored && skipPrompt && !storedRejected) { sentStored = true; return JoinAction.SendStored; } return JoinAction.AskPlayer; } public void OnPlayerTyped() { typed = true; } public JoinAction OnRejected() { if (sentStored) { sentStored = false; return JoinAction.ReconnectAndAsk; } typed = false; return JoinAction.None; } public JoinAction OnAccepted() { if (!typed) { return JoinAction.None; } if (Entry != null) { return JoinAction.StoreTyped; } if (!offerToSave || alreadyOffered) { return JoinAction.None; } return JoinAction.OfferSave; } public JoinAttempt AfterRejection() { return new JoinAttempt(Entry, hasStored, skipPrompt, offerToSave, alreadyOffered, storedRejected: true); } } public enum GameStatus { None, Connecting, Connected, ErrorVersion, ErrorDisconnected, ErrorConnectFailed, ErrorPassword, ErrorAlreadyConnected, ErrorBanned, ErrorFull, ErrorPlatformExcluded, ErrorCrossplayPrivilege, ErrorKicked, Unknown } public static class ConnectionOutcomes { public static GameStatus Parse(string? name) { string text = (name ?? "").Trim(); if (text.Length == 0 || !char.IsLetter(text[0])) { return GameStatus.Unknown; } if (!Enum.TryParse<GameStatus>(text, ignoreCase: false, out var result)) { return GameStatus.Unknown; } return result; } public static DisconnectKind Classify(GameStatus status, bool wasConnected) { return status switch { GameStatus.Connected => DisconnectKind.PlayerQuit, GameStatus.ErrorPassword => DisconnectKind.WrongPassword, GameStatus.ErrorKicked => DisconnectKind.Kicked, GameStatus.ErrorBanned => DisconnectKind.Banned, GameStatus.ErrorVersion => DisconnectKind.VersionMismatch, GameStatus.ErrorFull => DisconnectKind.ServerFull, GameStatus.ErrorAlreadyConnected => DisconnectKind.Refused, GameStatus.ErrorPlatformExcluded => DisconnectKind.Refused, GameStatus.ErrorCrossplayPrivilege => DisconnectKind.Refused, GameStatus.ErrorDisconnected => (!wasConnected) ? DisconnectKind.ConnectFailed : DisconnectKind.Unexpected, GameStatus.ErrorConnectFailed => (!wasConnected) ? DisconnectKind.ConnectFailed : DisconnectKind.Unexpected, _ => DisconnectKind.Unknown, }; } } public static class JoinTargets { public static ServerAddress? FromHost(string? host, int port) { string text = (host ?? "").Trim(); if (text.Length == 0 || port < 1 || port > 65535) { return null; } int num = 0; string text2 = text; for (int i = 0; i < text2.Length; i++) { if (text2[i] == ':') { num++; } } if (num == 1) { return null; } string text3 = port.ToString(CultureInfo.InvariantCulture); if (!ServerAddress.TryParse((num > 1 && text[0] != '[') ? ("[" + text + "]:" + text3) : (text + ":" + text3), out ServerAddress address)) { return null; } return address; } public static ServerAddress? FromSteam(ulong id) { if (id == 0L || !ServerAddress.TryParse("steam:" + id.ToString(CultureInfo.InvariantCulture), out ServerAddress address)) { return null; } return address; } public static ServerAddress? FromPlayFab(string? id) { if (!ServerAddress.TryParse("playfab:" + (id ?? "").Trim(), out ServerAddress address)) { return null; } return address; } public static bool TryGetSteamId(ServerAddress address, out ulong id) { id = 0uL; if (address != null && address.Kind == AddressKind.Steam && ulong.TryParse(address.Host, NumberStyles.None, CultureInfo.InvariantCulture, out id)) { return id != 0; } return false; } } public readonly struct JoinOptions { public bool SkipPrompt { get; } public bool OfferToSave { get; } public JoinOptions(bool skipPrompt, bool offerToSave) { SkipPrompt = skipPrompt; OfferToSave = offerToSave; } } public enum MenuStep { None, RejoinForPassword } public readonly struct MenuOutcome { public DisconnectKind Kind { get; } public ServerAddress? Target { get; } public MenuStep Step { get; } public bool WasConnected { get; } public MenuOutcome(DisconnectKind kind, ServerAddress? target, MenuStep step, bool wasConnected) { Kind = kind; Target = target; Step = step; WasConnected = wasConnected; } } public sealed class JoinCoordinator { private readonly HashSet<ServerAddress> offered = new HashSet<ServerAddress>(); private JoinAttempt? attempt; private ServerAddress? rejoinForPassword; private bool requested; public ServerAddress? Target { get; private set; } public ServerEntry? Entry => attempt?.Entry; public bool Connected { get; private set; } public bool AwaitingConnection { get { if (Target != null && attempt != null) { return !Connected; } return false; } } public void OnJoinStarted(ServerAddress? target, ServerList list, Func<ServerEntry, bool> hasStored, JoinOptions options) { if (list == null) { throw new ArgumentNullException("list"); } if (hasStored == null) { throw new ArgumentNullException("hasStored"); } if (target != null && target == Target && attempt != null && !requested && !Connected) { return; } Target = target; Connected = false; requested = false; if (target == null) { attempt = null; rejoinForPassword = null; return; } ServerEntry serverEntry = list.Find(target); JoinAttempt joinAttempt = new JoinAttempt(serverEntry, serverEntry != null && hasStored(serverEntry), options.SkipPrompt, options.OfferToSave, offered.Contains(target)); if (rejoinForPassword == target) { joinAttempt = joinAttempt.AfterRejection(); } rejoinForPassword = null; attempt = joinAttempt; } public JoinAction OnPasswordRequested() { if (attempt == null || Connected) { return JoinAction.None; } requested = true; return attempt.OnPasswordRequested(); } public void OnStoredUnreadable() { if (attempt != null) { attempt = attempt.AfterRejection(); } } public void OnPlayerTyped() { attempt?.OnPlayerTyped(); } public JoinAction OnConnected() { if (attempt == null || Connected) { return JoinAction.None; } Connected = true; JoinAction num = attempt.OnAccepted(); if (num == JoinAction.OfferSave) { offered.Add(Target); } return num; } public MenuOutcome OnReturnedToMenu(GameStatus status) { ServerAddress target = Target; bool connected = Connected; JoinAttempt joinAttempt = attempt; Target = null; attempt = null; Connected = false; requested = false; if (target == null) { return new MenuOutcome(DisconnectKind.Unknown, null, MenuStep.None, wasConnected: false); } DisconnectKind disconnectKind = ConnectionOutcomes.Classify(status, connected); if (disconnectKind == DisconnectKind.WrongPassword && joinAttempt != null && joinAttempt.OnRejected() == JoinAction.ReconnectAndAsk) { rejoinForPassword = target; return new MenuOutcome(disconnectKind, target, MenuStep.RejoinForPassword, connected); } return new MenuOutcome(disconnectKind, target, MenuStep.None, connected); } public void Reset() { offered.Clear(); attempt = null; rejoinForPassword = null; Target = null; Connected = false; requested = false; } } public enum MenuBackAction { None, CloseDialog, DeclineSaveOffer, ClosePanel, CancelWait } public struct MenuBackState { public bool Escape; public bool Typing; public bool TypedLastFrame; public bool DialogOpen; public bool DialogTyping; public bool SaveOfferOpen; public bool PanelOpen; public bool WaitPending; } public static class MenuBack { public static MenuBackAction Decide(MenuBackState state) { if (!state.Escape || state.Typing || state.TypedLastFrame) { return MenuBackAction.None; } if (state.DialogOpen) { if (!state.DialogTyping) { return MenuBackAction.CloseDialog; } return MenuBackAction.None; } if (state.SaveOfferOpen) { return MenuBackAction.DeclineSaveOffer; } if (state.PanelOpen) { return MenuBackAction.ClosePanel; } if (!state.WaitPending) { return MenuBackAction.None; } return MenuBackAction.CancelWait; } } public sealed class ReconnectServerPolicy { private bool denied; public ServerAddress? Target { get; private set; } public void Begin(ServerAddress? target) { Target = target; denied = false; } public void Observe(ServerAddress? target, bool authenticated, bool allowsReconnect) { if (authenticated && !(target == null) && !(target != Target)) { denied = !allowsReconnect; } } public bool LockedFor(ServerAddress? target) { if (denied && target != null) { return target == Target; } return false; } public void Reset() { Target = null; denied = false; } } public enum ReconnectStep { None, Scheduled, Stopped, Locked } public readonly struct ReconnectPlan { public ReconnectStep Step { get; } public ReconnectDecision Decision { get; } public int Attempt { get; } public int Limit { get; } public int DelaySeconds { get; } public ReconnectPlan(ReconnectStep step, ReconnectDecision decision, int attempt, int limit, int delaySeconds) { Step = step; Decision = decision; Attempt = attempt; Limit = limit; DelaySeconds = delaySeconds; } } public sealed class ReconnectSession { public const double StableSeconds = 60.0; private int attempts; private double connectedAt = -1.0; private double due = -1.0; public ServerAddress? Target { get; private set; } public bool Pending => due >= 0.0; public int SecondsLeft(double now) { if (!(due < 0.0)) { return (int)Math.Ceiling(Math.Max(0.0, due - now)); } return 0; } public void OnJoinStarted(ServerAddress? joining) { if (Target != null && joining != Target) { Cancel(); } } public void OnConnected(double now) { connectedAt = now; } public ReconnectPlan OnDisconnected(ServerAddress target, DisconnectKind kind, bool wasConnected, bool enabled, bool lockedByServer, int maxAttempts, double now) { if (target == null) { throw new ArgumentNullException("target"); } due = -1.0; if (Target != target) { Target = target; attempts = 0; } if (wasConnected && connectedAt >= 0.0 && now - connectedAt >= 60.0) { attempts = 0; } connectedAt = -1.0; DisconnectKind disconnectKind = ((kind != DisconnectKind.ConnectFailed || attempts <= 0) ? kind : DisconnectKind.Unexpected); int limit = Math.Max(1, Math.Min(10, maxAttempts)); ReconnectDecision reconnectDecision = ReconnectRules.Decide(enabled, lockedByServer, disconnectKind, attempts, maxAttempts); if (reconnectDecision == ReconnectDecision.Reconnect) { int num = ReconnectRules.DelaySeconds(attempts); attempts++; due = now + (double)num; return new ReconnectPlan(ReconnectStep.Scheduled, reconnectDecision, attempts, limit, num); } bool flag = attempts > 0; Cancel(); if (reconnectDecision == ReconnectDecision.Locked && disconnectKind == DisconnectKind.Unexpected) { return new ReconnectPlan(ReconnectStep.Locked, reconnectDecision, 0, limit, 0); } return new ReconnectPlan(flag ? ReconnectStep.Stopped : ReconnectStep.None, reconnectDecision, 0, limit, 0); } public bool Tick(double now) { if (due < 0.0 || now < due) { return false; } due = -1.0; return true; } public bool Cancel() { bool result = due >= 0.0 || attempts > 0; due = -1.0; attempts = 0; connectedAt = -1.0; Target = null; return result; } } } namespace ExtraPotions.Beacon.Integration { public enum BeaconModule { Join } public readonly struct InstalledPlugin { public string Guid { get; } public string Name { get; } public InstalledPlugin(string guid, string name) { Guid = guid ?? ""; Name = name ?? ""; } } public sealed class ModuleSource { public string Guid { get; } public string Name { get; } public BeaconModule Module { get; } public IReadOnlyList<string> PluginNames { get; } internal ModuleSource(string guid, string name, BeaconModule module, params string[] pluginNames) { Guid = guid; Name = name; Module = module; PluginNames = pluginNames; } public bool Matches(InstalledPlugin plugin) { if (!string.Equals(plugin.Guid, Guid, StringComparison.Ordinal)) { return PluginNames.Any((string n) => string.Equals(n, plugin.Name, StringComparison.OrdinalIgnoreCase)); } return true; } } public readonly struct StepAsideNotice { public BeaconModule Module { get; } public string SourceName { get; } public string SourceGuid { get; } public string Key => KeyOf(Module); public StepAsideNotice(BeaconModule module, string sourceName, string sourceGuid) { Module = module; SourceName = sourceName; SourceGuid = sourceGuid; } public static string KeyOf(BeaconModule module) { return "step_aside_" + module.ToString().ToLowerInvariant(); } } public sealed class ModuleStepAsideDecision { private readonly HashSet<BeaconModule> aside; public IReadOnlyList<ModuleSource> Present { get; } public bool Any => aside.Count > 0; public IReadOnlyList<StepAsideNotice> Notices { get; } internal ModuleStepAsideDecision(IReadOnlyList<ModuleSource> present) { Present = present; aside = new HashSet<BeaconModule>(present.Select((ModuleSource p) => p.Module)); Notices = (from p in present orderby p.Module select new StepAsideNotice(p.Module, p.Name, p.Guid)).ToList(); } public bool SteppedAside(BeaconModule module) { return aside.Contains(module); } } public static class ModuleStepAside { public static readonly ModuleSource AutoConnector = new ModuleSource("MidnightsFX.Autoconnect", "AutoConnector", BeaconModule.Join, "AutoConnector"); public static IReadOnlyList<ModuleSource> Sources { get; } = Array.AsReadOnly(new ModuleSource[1] { AutoConnector }); public static ModuleStepAsideDecision Decide(IEnumerable<InstalledPlugin> installed) { if (installed == null) { throw new ArgumentNullException("installed"); } List<InstalledPlugin> list = installed.ToList(); return new ModuleStepAsideDecision(Sources.Where((ModuleSource s) => list.Any(s.Matches)).ToList()); } } } namespace ExtraPotions.Beacon.Conversion { public enum ImportProblem { None, MissingAddress, InvalidAddress } public sealed class ImportedServer { public string Name { get; } public string? AddressText { get; } public ServerAddress? Address { get; } public IReadOnlyList<ServerAddress> Aliases { get; } public ImportProblem Problem { get; } public bool HasPassword => Password != null; public string? Password { get; } internal ImportedServer(string name, string? addressText, IReadOnlyList<string> aliasTexts, string? password) { Name = name; AddressText = addressText; Password = (string.IsNullOrEmpty(password) ? null : password); ServerAddress address; if (string.IsNullOrWhiteSpace(addressText)) { Problem = ImportProblem.MissingAddress; } else if (!ServerAddress.TryParse(addressText, out address)) { Problem = ImportProblem.InvalidAddress; } else { Address = address; } Aliases = (from t in aliasTexts select (!ServerAddress.TryParse(t, out ServerAddress address2)) ? null : address2 into x where x != null select (x)).ToList(); } public override string ToString() { return Name + " (" + (Address?.ToString() ?? "no address") + "), " + (HasPassword ? "password present" : "no password"); } } public enum ImportOutcome { Add, SameName, AddressTaken, Invalid } public sealed class ImportPlanItem { public ImportedServer Server { get; } public ImportOutcome Outcome { get; } internal ImportPlanItem(ImportedServer server, ImportOutcome outcome) { Server = server; Outcome = outcome; } public override string ToString() { return Outcome.ToString() + ": " + Server; } } public sealed class ImportResult { public int Added { get; } public int Skipped { get; } public IReadOnlyList<string> PasswordNotStored { get; } internal ImportResult(int added, int skipped, IReadOnlyList<string> passwordNotStored) { Added = added; Skipped = skipped; PasswordNotStored = passwordNotStored; } } public static class AutoConnectorImport { public const string SourceGuid = "MidnightsFX.Autoconnect"; public const string SourceFile = "AutoConnector/servers.yaml"; private const string AddressKey = "ServerAddress"; private const string AliasesKey = "ServerAliases"; private const string PassKey = "ServerPass"; public static IReadOnlyList<ImportedServer> Parse(string yaml) { List<ImportedServer> result = new List<ImportedServer>(); string name = null; string address = null; string pass = null; string listKey = null; List<string> aliases = new List<string>(); string[] array = (yaml ?? "").Replace("\r\n", "\n").Split('\n'); for (int i = 0; i < array.Length; i++) { string text = array[i].TrimEnd('\r', ' ', '\t'); string text2 = text.TrimStart(' '); if (text2.Length == 0 || text2[0] == '#') { continue; } if (text.Length - text2.Length == 0 && text2[0] != '-') { Flush(); name = TopKey(text2); } else { if (name == null) { continue; } if (text2.StartsWith("- ", StringComparison.Ordinal) || text2 == "-") { if (listKey == "ServerAliases") { aliases.Add(Scalar(text2.Substring(1))); } continue; } int num = text2.IndexOf(':'); if (num <= 0) { continue; } string text3 = text2.Substring(0, num).Trim(); string text4 = text2.Substring(num + 1); listKey = text3; switch (text3) { case "ServerAddress": address = Scalar(text4); break; case "ServerPass": pass = Scalar(text4); break; case "ServerAliases": { string text5 = text4.Trim(); if (text5.StartsWith("[", StringComparison.Ordinal) && text5.EndsWith("]", StringComparison.Ordinal)) { aliases.AddRange(from s in text5.Substring(1, text5.Length - 2).Split(',').Select(Scalar) where s.Length > 0 select s); } break; } } } } Flush(); return result; void Flush() { if (name != null) { result.Add(new ImportedServer(name, address, aliases.ToList(), pass)); } name = (address = (pass = (listKey = null))); aliases.Clear(); } } public static IReadOnlyList<ImportPlanItem> Plan(ServerList existing, IEnumerable<ImportedServer> servers) { if (existing == null) { throw new ArgumentNullException("existing"); } HashSet<string> hashSet = new HashSet<string>(existing.Entries.Select((ServerEntry e) => e.Name), StringComparer.OrdinalIgnoreCase); HashSet<ServerAddress> hashSet2 = new HashSet<ServerAddress>(existing.Entries.SelectMany((ServerEntry e) => e.AllAddresses)); List<ImportPlanItem> list = new List<ImportPlanItem>(); foreach (ImportedServer server in servers) { ImportOutcome outcome; if (server.Address == null || server.Name.Trim().Length == 0) { outcome = ImportOutcome.Invalid; } else if (hashSet.Contains(server.Name.Trim())) { outcome = ImportOutcome.SameName; } else if (new ServerAddress[1] { server.Address }.Concat(server.Aliases).Any(hashSet2.Contains)) { outcome = ImportOutcome.AddressTaken; } else { outcome = ImportOutcome.Add; hashSet.Add(server.Name.Trim()); foreach (ServerAddress item in new ServerAddress[1] { server.Address }.Concat(server.Aliases)) { hashSet2.Add(item); } } list.Add(new ImportPlanItem(server, outcome)); } return list; } public static IReadOnlyList<string> Preview(IEnumerable<ImportPlanItem> plan) { return plan.Select(delegate(ImportPlanItem p) { ImportedServer server = p.Server; string text = ((server.Aliases.Count == 0) ? "" : (" (+" + server.Aliases.Count + ((server.Aliases.Count == 1) ? " alias)" : " aliases)"))); string text2 = p.Outcome switch { ImportOutcome.Add => "import", ImportOutcome.SameName => "skip, a server with this name is saved", ImportOutcome.AddressTaken => "skip, this address is saved", _ => (server.Problem == ImportProblem.MissingAddress) ? "skip, no address" : "skip, invalid address", }; return server.Name + " " + (server.Address?.ToString() ?? "-") + text + ", " + (server.HasPassword ? "password present" : "no password") + ": " + text2; }).ToList(); } public static ImportResult Apply(ServerList list, ISecretStore store, IEnumerable<ImportPlanItem> plan) { if (list == null) { throw new ArgumentNullException("list"); } if (store == null) { throw new ArgumentNullException("store"); } int num = 0; int num2 = 0; List<string> list2 = new List<string>(); foreach (ImportPlanItem item in plan) { if (item.Outcome != ImportOutcome.Add) { num2++; continue; } ImportedServer server = item.Server; ServerEntry serverEntry; try { serverEntry = list.Add(server.Name, server.Address, server.Aliases); } catch (ArgumentException) { num2++; continue; } num++; if (server.Password != null && !store.Store(serverEntry.SecretRef, server.Password)) { list2.Add(serverEntry.Name); } } return new ImportResult(num, num2, list2); } public static string BlankPasswords(string yaml, out int blanked) { blanked = 0; string text = yaml ?? ""; StringBuilder stringBuilder = new StringBuilder(text.Length); int num = 0; while (num <= text.Length) { int num2 = text.IndexOf('\n', num); int num3 = ((num2 < 0) ? text.Length : num2); string text2 = text.Substring(num, num3 - num); bool num4 = text2.EndsWith("\r", StringComparison.Ordinal); string text3 = (num4 ? text2.Substring(0, text2.Length - 1) : text2); string text4 = text3.TrimStart(' '); if (text3.Length > text4.Length && text4.StartsWith("ServerPass:", StringComparison.Ordinal) && Scalar(text4.Substring("ServerPass".Length + 1)).Length > 0) { text3 = text3.Substring(0, text3.Length - text4.Length) + "ServerPass: ''"; blanked++; } stringBuilder.Append(text3); if (num4) { stringBuilder.Append('\r'); } if (num2 < 0) { break; } stringBuilder.Append('\n'); num = num2 + 1; } return stringBuilder.ToString(); } private static string TopKey(string body) { if (body[0] == '\'' || body[0] == '"') { return Scalar(body); } for (int i = 0; i < body.Length; i++) { if (body[i] == ':' && (i + 1 == body.Length || body[i + 1] == ' ')) { return body.Substring(0, i).Trim(); } } return body.Trim(); } private static string Scalar(string raw) { string text = raw.Trim(); if (text.Length == 0) { return ""; } if (text[0] == '\'') { StringBuilder stringBuilder = new StringBuilder(); for (int i = 1; i < text.Length; i++) { if (text[i] != '\'') { stringBuilder.Append(text[i]); continue; } if (i + 1 >= text.Length || text[i + 1] != '\'') { break; } stringBuilder.Append('\''); i++; } return stringBuilder.ToString(); } if (text[0] == '"') { StringBuilder stringBuilder2 = new StringBuilder(); for (int j = 1; j < text.Length; j++) { char c = text[j]; if (c == '"') { break; } if (c == '\\' && j + 1 < text.Length) { char c2 = text[++j]; stringBuilder2.Append(c2 switch { 't' => '\t', 'n' => '\n', _ => c2, }); } else { stringBuilder2.Append(c); } } return stringBuilder2.ToString(); } if (text[0] == '#') { return ""; } int num = text.IndexOf(" #", StringComparison.Ordinal); return ((num < 0) ? text : text.Substring(0, num)).Trim(); } } } namespace ExtraPotions.Beacon.Config { public enum SettingScope { Local, ServerAuthoritative } public abstract class BeaconSetting { public string Section { get; } public string Key { get; } public string Id => Section + "." + Key; public SettingScope Scope { get; } public string Description { get; } public bool Hotkey { get; } public bool ServerLockable { get; } public abstract Type ValueType { get; } public abstract object DefaultValue { get; } public abstract object? Minimum { get; } public abstract object? Maximum { get; } private protected BeaconSetting(string section, string key, string description, bool hotkey, bool serverLockable, SettingScope scope) { if (string.IsNullOrWhiteSpace(section) || section.Trim() != section) { throw new ArgumentException("Expected a trimmed section name.", "section"); } if (string.IsNullOrWhiteSpace(key) || key.Trim() != key) { throw new ArgumentException("Expected a trimmed key.", "key"); } if (string.IsNullOrWhiteSpace(description)) { throw new ArgumentException("Expected a description.", "description"); } Section = section; Key = key; Description = description; Hotkey = hotkey; ServerLockable = serverLockable; Scope = scope; } public override string ToString() { return Id; } } public sealed class BeaconSetting<T> : BeaconSetting { private readonly Func<T, T> normalize; public T Default { get; } public override Type ValueType => typeof(T); public override object DefaultValue => Default; public override object? Minimum { get; } public override object? Maximum { get; } internal BeaconSetting(string section, string key, T defaultValue, string description, Func<T, T> normalize, object? minimum = null, object? maximum = null, bool hotkey = false, bool serverLockable = false, SettingScope scope = SettingScope.Local) : base(section, key, description, hotkey, serverLockable, scope) { Default = defaultValue; this.normalize = normalize; Minimum = minimum; Maximum = maximum; } public T Normalize(T value) { return normalize(value); } } public static class BeaconSettings { public static class General { public const string Section = "General"; public static readonly BeaconSetting<bool> AddToFavorites = Toggle("General", "Add To Favorites", value: true, "Adds every saved server to the Favorite tab of the Join Game screen."); public static readonly BeaconSetting<bool> SkipPasswordPrompt = Toggle("General", "Skip Password Prompt", value: true, "Sends a saved server's stored password when the game asks for it, so the prompt is not shown."); public static readonly BeaconSetting<bool> OfferToSavePasswords = Toggle("General", "Offer To Save Passwords", value: true, "After a server accepts a password you typed, asks once per server whether Beacon should save it."); public static readonly BeaconSetting<bool> RemoveEntryWithFavorite = Toggle("General", "Remove Entry With Favorite", value: true, "Removing a saved server from the game's favorites also removes it from Beacon (with an undo message)."); public static readonly BeaconSetting<string> PanelKey = Text("General", "Panel Key", "Keyboard shortcut that opens the Beacon panel. Empty: only the Join Game screen button opens it.", hotkey: true); } public static class AutoJoin { public const string Section = "Auto Join"; public static readonly BeaconSetting<bool> Enabled = Toggle("Auto Join", "Auto Join", value: false, "Joins a saved server automatically after the game reaches the main menu, after a short countdown you can cancel."); public static readonly BeaconSetting<string> Server = Text("Auto Join", "Auto Join Server", "Name of the saved server to join. Empty: the server marked as default."); public static readonly BeaconSetting<string> Character = Text("Auto Join", "Auto Join Character", "Character to join with. Empty: the last character used."); public static readonly BeaconSetting<int> CountdownSeconds = Integer("Auto Join", "Auto Join Countdown Seconds", 5, 0, 30, "Seconds of countdown before the automatic join; Escape or the pad's B button cancels."); } public static class Reconnect { public const string Section = "Reconnect"; public static readonly BeaconSetting<bool> Enabled = Toggle("Reconnect", "Reconnect On Disconnect", value: false, "Reconnects after an unexpected disconnect. Never after a kick or ban. A server running Beacon may turn this off.", lockable: true); public static readonly BeaconSetting<int> Attempts = Integer("Reconnect", "Reconnect Attempts", 3, 1, 10, "Most reconnect attempts after one disconnect."); } public static class Passwords { public const string Section = "Passwords"; public static readonly BeaconSetting<LinuxFallback> LinuxFallback = new BeaconSetting<LinuxFallback>("Passwords", "Linux Fallback", ExtraPotions.Beacon.Secrets.LinuxFallback.SessionOnly, "On Linux without a usable keyring: SessionOnly remembers passwords until the game closes; Disabled never stores them. On Linux a change applies right away. On systems other than Windows and Linux, restart the game after changing it.", (LinuxFallback v) => Enum.IsDefined(typeof(LinuxFallback), v) ? v : ExtraPotions.Beacon.Secrets.LinuxFallback.SessionOnly); } public static class Server { public static readonly BeaconSetting<bool> AllowReconnect = new BeaconSetting<bool>("Server", "Allow Automatic Reconnect", defaultValue: true, "Server policy: false prevents Beacon clients from automatically reconnecting after an unexpected disconnect. True leaves each player's preference unchanged.", (bool v) => v, null, null, hotkey: false, serverLockable: false, SettingScope.ServerAuthoritative); } public static IReadOnlyList<BeaconSetting> All { get; } = Array.AsReadOnly(new BeaconSetting[13] { General.AddToFavorites, General.SkipPasswordPrompt, General.OfferToSavePasswords, General.RemoveEntryWithFavorite, General.PanelKey, AutoJoin.Enabled, AutoJoin.Server, AutoJoin.Character, AutoJoin.CountdownSeconds, Reconnect.Enabled, Reconnect.Attempts, Passwords.LinuxFallback, Server.AllowReconnect }); private static BeaconSetting<bool> Toggle(string section, string key, bool value, string description, bool lockable = false) { return new BeaconSetting<bool>(section, key, value, description, (bool v) => v, null, null, hotkey: false, lockable); } private static BeaconSetting<int> Integer(string section, string key, int value, int min, int max, string description) { return new BeaconSetting<int>(section, key, value, description, (int v) => Math.Max(min, Math.Min(max, v)), min, max); } private static BeaconSetting<string> Text(string section, string key, string description, bool hotkey = false) { return new BeaconSetting<string>(section, key, "", description, (string v) => (v ?? "").Trim(), null, null, hotkey); } } }
plugins/Beacon/Beacon.dll
Decompiled a week ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security; using System.Security.Cryptography; using System.Security.Permissions; using System.Text; using System.Threading; using System.Threading.Tasks; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using ExtraPotions.Beacon.Config; using ExtraPotions.Beacon.Conversion; using ExtraPotions.Beacon.Integration; using ExtraPotions.Beacon.Joining; using ExtraPotions.Beacon.Secrets; using ExtraPotions.Beacon.Servers; using ExtraPotions.Meadworks.Crucible; using ExtraPotions.Meadworks.Crucible.UI; using HarmonyLib; using Jotunn.Entities; using Jotunn.Managers; using Jotunn.Utils; using Microsoft.CodeAnalysis; using TMPro; using UnityEngine; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: IgnoresAccessChecksTo("assembly_utils")] [assembly: IgnoresAccessChecksTo("assembly_valheim")] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("ExtraPotions")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyCopyright("Copyright (C) 2026 expDARE (ExtraPotions Meadworks)")] [assembly: AssemblyFileVersion("0.1.14.0")] [assembly: AssemblyInformationalVersion("0.1.14+4312080b663b50769f0cf52f9bd002c67886c90a")] [assembly: AssemblyProduct("Beacon")] [assembly: AssemblyTitle("Beacon")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.1.14.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace ExtraPotions.Beacon.Runtime { public sealed class BeaconCommand : ConsoleCommand { private static string? previewSourceHash; private static string? previewListHash; private static string? blankSourceHash; public override string Name => "beacon"; public override string Help => "Beacon server favorites: status, join <name>, cancel, import preview, import apply, import blank-source confirm."; private static string SourcePath => Path.Combine(Paths.ConfigPath, "AutoConnector/servers.yaml".Replace('/', Path.DirectorySeparatorChar)); internal static (string? Source, string? List) PreviewStamp => (Source: previewSourceHash, List: previewListHash); internal static bool CanBlankSource { get { if (blankSourceHash == null || BeaconImportSecrets.Busy) { return false; } try { string text = ReadSource(); return text != null && blankSourceHash == Fingerprint(text); } catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException) { return false; } } } private static string Fingerprint(string text) { using SHA256 sHA = SHA256.Create(); return Convert.ToBase64String(sHA.ComputeHash(Encoding.UTF8.GetBytes(text))); } public override void Run(string[] args) { try { string[] array = Execute(args ?? Array.Empty<string>()); for (int i = 0; i < array.Length; i++) { Print(array[i]); } } catch (Exception ex) { Print("Beacon command failed: " + ex.GetType().Name); } } private static void Print(string line) { Console instance = Console.instance; if (instance != null) { instance.Print(BeaconSecrets.Redactor.Clean(line)); } } private static string[] Execute(string[] args) { string text = ((args.Length == 0) ? "status" : args[0].ToLowerInvariant()); string text2 = ((args.Length > 1) ? args[1].ToLowerInvariant() : ""); MeadworksLog log = Plugin.Log; return text switch { "help" => new string[1] { BeaconText.Text("cmd_help") }, "status" => new string[1] { Status() }, "join" => new string[1] { Join(string.Join(" ", args.Skip(1))) }, "cancel" => new string[1] { BeaconAutoJoin.Cancel(log) ?? BeaconText.Text("cmd_cancel_none") }, "import" => text2 switch { "preview" => Preview(), "apply" => Apply(log), "blank-source" => Blank(args.Length > 2 && args[2].Equals("confirm", StringComparison.OrdinalIgnoreCase)), _ => new string[1] { BeaconText.Text("cmd_usage") }, }, _ => new string[1] { BeaconText.Format("cmd_unknown_verb", text) }, }; } private static string Status() { //IL_005b: Unknown result type (might be due to invalid IL or missing references) ISecretStore store = BeaconSecrets.Store; int num = BeaconServers.List.Entries.Count((ServerEntry e) => store.Contains(e.SecretRef)); return BeaconText.Format("cmd_status", BeaconServers.List.Count, num, BeaconText.Text(SecretStores.StorageLineKey(store.Backend))); } private static string Join(string name) { if (BeaconImportSecrets.Busy) { return BeaconText.Text("import_password_pending"); } ServerEntry val = BeaconServers.List.FindByName(name.Trim()); if (val == null) { return BeaconText.Format("cmd_join_unknown", name.Trim()); } if (!BeaconJoin.Active) { return BeaconText.Format("step_aside_join", BeaconModules.StepAside.Notices.Select((StepAsideNotice n) => ((StepAsideNotice)(ref n)).SourceName).FirstOrDefault() ?? "AutoConnector"); } if (!BeaconMenu.Join(val.Address, null)) { return BeaconText.Text("cmd_join_not_ready"); } return BeaconText.Format("cmd_join_started", val.Name); } private static string? ReadSource() { if (!File.Exists(SourcePath)) { return null; } return File.ReadAllText(SourcePath, Encoding.UTF8); } internal static string[] Preview() { string text = ReadSource(); if (text == null) { return new string[1] { BeaconText.Text("import_none") }; } IReadOnlyList<ImportedServer> readOnlyList = AutoConnectorImport.Parse(text); foreach (ImportedServer item in readOnlyList) { BeaconSecrets.Redactor.Track(item.Password); } try { string[] result = ((IEnumerable<string>)AutoConnectorImport.Preview((IEnumerable<ImportPlanItem>)AutoConnectorImport.Plan(BeaconServers.List, (IEnumerable<ImportedServer>)readOnlyList))).Select((Func<string, string>)BeaconSecrets.Redactor.Clean).ToArray(); previewSourceHash = Fingerprint(text); previewListHash = Fingerprint(ServerListCodec.Write(BeaconServers.List)); return result; } finally { foreach (ImportedServer item2 in readOnlyList) { BeaconSecrets.Redactor.Untrack(item2.Password); } } } internal static string[] Apply(MeadworksLog? log) { string text = ReadSource(); if (text == null || log == null) { return new string[1] { BeaconText.Text("import_none") }; } if (BeaconImportSecrets.Busy) { return new string[1] { BeaconText.Text("import_password_pending") }; } if (previewSourceHash != Fingerprint(text) || previewListHash != Fingerprint(ServerListCodec.Write(BeaconServers.List))) { return new string[1] { BeaconText.Text("import_preview_again") }; } IReadOnlyList<ImportedServer> readOnlyList = AutoConnectorImport.Parse(text); foreach (ImportedServer item in readOnlyList) { BeaconSecrets.Redactor.Track(item.Password); } try { IReadOnlyList<ImportPlanItem> plan = AutoConnectorImport.Plan(BeaconServers.List, (IEnumerable<ImportedServer>)readOnlyList); ImportResult result = null; if (!BeaconServers.Amend(delegate(ServerList candidate) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Expected O, but got Unknown result = AutoConnectorImport.Apply(candidate, (ISecretStore)new DisabledSecretStore(), (IEnumerable<ImportPlanItem>)plan); }, log)) { return new string[1] { BeaconText.Text("import_write_failed") }; } blankSourceHash = previewSourceHash; previewSourceHash = (previewListHash = null); foreach (ImportPlanItem item2 in plan.Where((ImportPlanItem p) => (int)p.Outcome == 0 && p.Server.Password != null)) { ServerEntry val = BeaconServers.List.FindByName(item2.Server.Name); if (val != null) { BeaconImportSecrets.Start(val.SecretRef, val.Name, item2.Server.Password); } } return new string[1] { BeaconText.Format("import_done", result.Added, result.Skipped) }.Concat((!BeaconImportSecrets.Busy) ? ((IEnumerable<string>)Array.Empty<string>()) : ((IEnumerable<string>)new string[1] { BeaconText.Text("import_password_pending") })).Concat(new string[2] { BeaconText.Text("import_source_plain"), BeaconText.Text("cmd_confirm_blank") }).Select((Func<string, string>)BeaconSecrets.Redactor.Clean) .ToArray(); } finally { foreach (ImportedServer item3 in readOnlyList) { BeaconSecrets.Redactor.Untrack(item3.Password); } } } internal static string[] Blank(bool confirmed) { if (!BeaconImportSecrets.Busy) { string text = ReadSource(); if (text != null) { if (confirmed) { if (blankSourceHash != null && !(blankSourceHash != Fingerprint(text))) { int num = default(int); string text2 = AutoConnectorImport.BlankPasswords(text, ref num); if (num > 0) { BeaconServers.WriteAtomically(SourcePath, text2); } ResetPreview(); return new string[1] { BeaconText.Format("import_blank_done", num) }; } return new string[1] { BeaconText.Text("import_preview_again") }; } return new string[1] { BeaconText.Text("cmd_confirm_blank") }; } return new string[1] { BeaconText.Text("import_none") }; } return new string[1] { BeaconText.Text("import_password_pending") }; } internal static void ResetPreview() { previewSourceHash = (previewListHash = (blankSourceHash = null)); } } public sealed class BeaconConfig { private readonly ConfigFile file; private readonly Dictionary<BeaconSetting, ConfigEntryBase> entries = new Dictionary<BeaconSetting, ConfigEntryBase>(); private readonly BeaconSyncReceipt receipt = new BeaconSyncReceipt(); private long policyGeneration = -1L; internal ReconnectServerPolicy ReconnectPolicy { get; } = new ReconnectServerPolicy(); public ConfigFile File => file; private BeaconConfig(ConfigFile file) { //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Expected O, but got Unknown this.file = file; bool saveOnConfigSet = file.SaveOnConfigSet; file.SaveOnConfigSet = false; try { foreach (BeaconSetting item in BeaconSettings.All) { BindSetting(item); } } finally { file.SaveOnConfigSet = saveOnConfigSet; } file.Save(); SynchronizationManager.OnConfigurationSynchronized += OnSynchronized; MeadworksBlockedOptions.Track("com.meadworks.beacon", Entry((BeaconSetting)(object)Server.AllowReconnect), (ConfigScope)1, true); } public static BeaconConfig Bind(ConfigFile file, MeadworksLog log) { if (file == null) { throw new ArgumentNullException("file"); } if (log == null) { throw new ArgumentNullException("log"); } BeaconConfig result = new BeaconConfig(file); log.Verbose("Configuration bound: " + (BeaconSettings.All.Count - 1) + " Local settings, one server policy, no cheats."); return result; } public ConfigEntryBase Entry(BeaconSetting setting) { if (!entries.TryGetValue(setting ?? throw new ArgumentNullException("setting"), out ConfigEntryBase value)) { throw new ArgumentException("Not a Beacon setting.", "setting"); } return value; } public T Get<T>(BeaconSetting<T> setting) { //IL_0032: Unknown result type (might be due to invalid IL or missing references) if (((BeaconSetting)setting).Hotkey) { throw new ArgumentException(((BeaconSetting)setting).Id + " is a keyboard shortcut; use Shortcut.", "setting"); } ConfigEntry<T> val = (ConfigEntry<T>)(object)Entry((BeaconSetting)(object)setting); return setting.Normalize(((int)((BeaconSetting)setting).Scope == 0) ? val.Value : MeadworksConfig.Effective<T>(val, "com.meadworks.beacon", (ConfigScope)1, setting.Default, true)); } public ConfigEntry<KeyboardShortcut> Shortcut(BeaconSetting<string> setting) { if (!((BeaconSetting)setting).Hotkey) { throw new ArgumentException(((BeaconSetting)setting).Id + " is not a keyboard shortcut.", "setting"); } return (ConfigEntry<KeyboardShortcut>)(object)Entry((BeaconSetting)(object)setting); } internal void BeginReconnectConnection(ServerAddress? target) { receipt.Reset(); policyGeneration = -1L; ReconnectPolicy.Begin(target); } internal void RefreshReconnectPolicy() { //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Invalid comparison between Unknown and I4 //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Invalid comparison between Unknown and I4 //IL_008b: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Invalid comparison between Unknown and I4 //IL_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_00b6: Invalid comparison between Unknown and I4 //IL_00a1: Unknown result type (might be due to invalid IL or missing references) //IL_00a3: Invalid comparison between Unknown and I4 //IL_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_00dd: Invalid comparison between Unknown and I4 ServerAddress target = ReconnectPolicy.Target; if (target == (ServerAddress)null || !Plugin.IsReady || (Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsServer() || (int)ZNet.GetConnectionStatus() != 2) { return; } SessionSnapshot session = Plugin.Session; if ((int)session.Role != 3) { return; } if (session.AuthorityReady) { if (policyGeneration >= 0 && policyGeneration != session.Generation) { ReconnectPolicy.Begin(target); } policyGeneration = session.Generation; } ServerProduct val = MeadworksAuthority.ServerProductOf("com.meadworks.beacon"); if ((int)MeadworksAuthority.Server == 2 || (session.AuthorityReady && (int)val == 2)) { ReconnectPolicy.Observe(target, true, true); } else if ((int)val == 1 && receipt.CurrentGeneration == session.Generation && (int)MeadworksAuthority.Decide("com.meadworks.beacon", (ConfigScope)1, true).Source == 1) { ReconnectPolicy.Observe(target, true, Get<bool>(Server.AllowReconnect)); } } private void OnSynchronized(object sender, ConfigurationSynchronizationEventArgs args) { if (args.UpdatedPluginGUIDs != null && args.UpdatedPluginGUIDs.Contains("com.meadworks.beacon")) { receipt.Observe(); RefreshReconnectPolicy(); } } public void Shutdown() { SynchronizationManager.OnConfigurationSynchronized -= OnSynchronized; MeadworksBlockedOptions.Untrack(Entry((BeaconSetting)(object)Server.AllowReconnect)); receipt.Reset(); policyGeneration = -1L; ReconnectPolicy.Reset(); entries.Clear(); } private void BindSetting(BeaconSetting setting) { //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) ConfigEntryBase value; if (setting.Hotkey) { string text = (string)setting.DefaultValue; value = (ConfigEntryBase)(object)MeadworksConfig.Bind<KeyboardShortcut>(file, setting.Section, setting.Key, (text.Length == 0) ? KeyboardShortcut.Empty : KeyboardShortcut.Deserialize(text), setting.Description, (ConfigScope)0, (AcceptableValueBase)null); } else if (!(setting is BeaconSetting<bool> setting2)) { if (!(setting is BeaconSetting<int> val)) { if (!(setting is BeaconSetting<string> setting3)) { if (!(setting is BeaconSetting<LinuxFallback> setting4)) { throw new NotSupportedException(setting.Id + ": unsupported value type " + setting.ValueType.Name + "."); } value = (ConfigEntryBase)(object)Bind<LinuxFallback>(setting4, null); } else { value = (ConfigEntryBase)(object)Bind<string>(setting3, null); } } else { value = (ConfigEntryBase)(object)Bind<int>(val, (AcceptableValueBase?)(object)new AcceptableValueRange<int>((int)((BeaconSetting)val).Minimum, (int)((BeaconSetting)val).Maximum)); } } else { value = (ConfigEntryBase)(object)Bind<bool>(setting2, null); } entries.Add(setting, value); } private ConfigDescription Describe(BeaconSetting setting, AcceptableValueBase? acceptable, string? text = null) { //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Expected O, but got Unknown //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Expected O, but got Unknown //IL_00a6: Unknown result type (might be due to invalid IL or missing references) //IL_00ac: Expected O, but got Unknown ConfigDependency val = null; if ((object)setting == AutoJoin.Server || (object)setting == AutoJoin.Character || (object)setting == AutoJoin.CountdownSeconds) { val = new ConfigDependency((Func<bool>)(() => Get<bool>(AutoJoin.Enabled)), (Func<string>)(() => "Requires " + ((BeaconSetting)AutoJoin.Enabled).Section + ": " + ((BeaconSetting)AutoJoin.Enabled).Key + "."), (Func<bool>)null); } if ((object)setting == Reconnect.Attempts) { val = new ConfigDependency((Func<bool>)(() => Get<bool>(Reconnect.Enabled)), (Func<string>)(() => "Requires " + ((BeaconSetting)Reconnect.Enabled).Section + ": " + ((BeaconSetting)Reconnect.Enabled).Key + "."), (Func<bool>)null); } return new ConfigDescription(text ?? setting.Description, acceptable, (val == null) ? Array.Empty<object>() : new object[1] { val }); } private ConfigEntry<T> Bind<T>(BeaconSetting<T> setting, AcceptableValueBase? acceptable) { //IL_0022: Unknown result type (might be due to invalid IL or missing references) return MeadworksConfig.Bind<T>(file, ((BeaconSetting)setting).Section, ((BeaconSetting)setting).Key, setting.Default, Describe((BeaconSetting)(object)setting, acceptable), (ConfigScope)(((int)((BeaconSetting)setting).Scope != 0) ? 1 : 0)); } } internal sealed class BeaconSyncReceipt { private ZNet network; private ZRpc server; private string epoch = ""; private long world; internal long CurrentGeneration { get { //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Invalid comparison between Unknown and I4 if (!Plugin.IsReady || (Object)(object)network == (Object)null || server == null || !server.IsConnected() || network != ZNet.instance || network.IsServer() || server != network.GetServerRPC()) { return -1L; } SessionSnapshot session = Plugin.Session; if ((int)session.Role != 3 || !session.AuthorityReady || session.WorldId == 0L || string.IsNullOrEmpty(session.SessionId)) { return -1L; } if (epoch.Length == 0) { epoch = session.SessionId; world = session.WorldId; } if (!(epoch == session.SessionId) || world != session.WorldId) { return -1L; } return session.Generation; } } internal void Reset() { network = null; server = null; epoch = ""; world = 0L; } internal void Observe() { //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Invalid comparison between Unknown and I4 network = ZNet.instance; server = (((Object)(object)network != (Object)null && !network.IsServer()) ? network.GetServerRPC() : null); epoch = ""; world = 0L; if (server != null && server.IsConnected() && Plugin.IsReady) { SessionSnapshot session = Plugin.Session; if ((int)session.Role == 3 && !string.IsNullOrEmpty(session.SessionId)) { epoch = session.SessionId; world = session.WorldId; } } } } internal static class BeaconConversion { private sealed class Report { internal readonly string[] Lines; internal readonly (string? Source, string? List) Stamp; internal Report(string[] lines, (string?, string?) stamp) { Lines = lines; Stamp = stamp; } public override string ToString() { return string.Join("\n", Lines); } } private static IDisposable? registration; private static string SourcePath => Path.Combine(Paths.ConfigPath, "AutoConnector/servers.yaml".Replace('/', Path.DirectorySeparatorChar)); public static void Register() { //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Expected O, but got Unknown if (registration == null) { registration = MeadworksConversion.Register(new ConversionRegistration("beacon.autoconnector", "com.meadworks.beacon", "MidnightsFX.Autoconnect", "AutoConnector", (Func<bool>)(() => Plugin.Ready && !BeaconImportSecrets.Busy && File.Exists(SourcePath)), (Func<object>)(() => new Report(BeaconCommand.Preview(), BeaconCommand.PreviewStamp)), (Func<object, IReadOnlyDictionary<string, string>, bool, object>)Apply)); } } private static object Apply(object document, IReadOnlyDictionary<string, string> choices, bool backup) { if (!(document is Report report)) { throw new ArgumentException("Wrong Beacon import preview."); } if (report.Stamp.Source != null) { (string, string) stamp = report.Stamp; (string, string) previewStamp = BeaconCommand.PreviewStamp; if (!(stamp.Item1 != previewStamp.Item1) && !(stamp.Item2 != previewStamp.Item2)) { if (backup && File.Exists(BeaconServers.FilePath)) { File.Copy(BeaconServers.FilePath, BeaconServers.FilePath + ".bak", overwrite: true); } return new Report(BeaconCommand.Apply(Plugin.Log), default((string, string))); } } return new Report(new string[1] { BeaconText.Text("import_preview_again") }, default((string, string))); } public static void Reset() { registration?.Dispose(); registration = null; } } public static class BeaconModules { private const string TranslationProduct = "beacon"; private static ModuleStepAsideDecision? stepAside; private static long noticedSession = -1L; public static ModuleStepAsideDecision StepAside => stepAside ?? ModuleStepAside.Decide(Enumerable.Empty<InstalledPlugin>()); public static void Decide(MeadworksLog log) { //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_00a3: Unknown result type (might be due to invalid IL or missing references) //IL_00b7: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: Unknown result type (might be due to invalid IL or missing references) if (!Plugin.Services.StepAside.Declared("com.meadworks.beacon").Any()) { MeadworksStepAside.Declare("com.meadworks.beacon", "Beacon", ((IEnumerable<ModuleSource>)ModuleStepAside.Sources).Select((Func<ModuleSource, SourceMod>)((ModuleSource s) => new SourceMod(s.Guid, s.Name)))); } stepAside = ModuleStepAside.Decide(((IEnumerable<PluginInfo>)Chainloader.PluginInfos.Values).Select((Func<PluginInfo, InstalledPlugin>)((PluginInfo p) => new InstalledPlugin(p.Metadata.GUID, p.Metadata.Name)))); foreach (StepAsideNotice notice in stepAside.Notices) { StepAsideNotice current = notice; log.Info("Beacon " + ((object)((StepAsideNotice)(ref current)).Module/*cast due to .constrained prefix*/).ToString().ToLowerInvariant() + " module steps aside: " + ((StepAsideNotice)(ref current)).SourceName + " is installed."); } } public static bool Active(BeaconModule module) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) if (Plugin.Ready) { return !StepAside.SteppedAside(module); } return false; } public static void Tick(MeadworksLog log) { //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0052: 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_0080: Unknown result type (might be due to invalid IL or missing references) //IL_0096: Expected O, but got Unknown if (!Plugin.Ready || stepAside == null || !stepAside.Any) { return; } SessionSnapshot session = Plugin.Session; if (noticedSession == session.Generation) { return; } noticedSession = session.Generation; foreach (StepAsideNotice notice in stepAside.Notices) { StepAsideNotice current = notice; MeadworksStepAside.ReportModule("com.meadworks.beacon", ((object)((StepAsideNotice)(ref current)).Module/*cast due to .constrained prefix*/).ToString().ToLowerInvariant(), new SourceMod(((StepAsideNotice)(ref current)).SourceGuid, ((StepAsideNotice)(ref current)).SourceName), "beacon", ((StepAsideNotice)(ref current)).Key); } } public static void Reset() { stepAside = null; noticedSession = -1L; } } public static class BeaconText { private static MeadworksTranslations? catalog; public static ProductTranslations? Translations { get; private set; } public static void Initialize(BepInPlugin metadata, ManualLogSource log) { //IL_0098: Unknown result type (might be due to invalid IL or missing references) //IL_00a2: Expected O, but got Unknown Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); Assembly assembly = typeof(BeaconText).Assembly; foreach (string item in MeadworksLanguages.Supported) { using Stream stream = assembly.GetManifestResourceStream("Beacon.localization." + item + ".json"); if (stream != null) { using StreamReader streamReader = new StreamReader(stream); dictionary.Add(item, streamReader.ReadToEnd()); } } Translations = new ProductTranslations("beacon", (IReadOnlyDictionary<string, string>)dictionary); catalog = MeadworksTranslations.Register(metadata, Translations, (string)null, log); } public static string Text(string key) { MeadworksTranslations? obj = catalog; return ((obj != null) ? obj.Text(key) : null) ?? key; } public static string Format(string key, params object[] arguments) { MeadworksTranslations? obj = catalog; return ((obj != null) ? obj.Format(key, arguments) : null) ?? key; } public static void Tick() { MeadworksTranslations? obj = catalog; if (obj != null) { obj.Update(); } } public static void Shutdown() { MeadworksTranslations? obj = catalog; if (obj != null) { obj.Dispose(); } catalog = null; Translations = null; } public static void Centre(string text, MeadworksLog log) { //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Expected O, but got Unknown if (MeadworksHeadless.AllowsUi && (Object)(object)MessageHud.instance != (Object)null) { ProductRegistration source = Plugin.Source; if (source != null && source.IsActive && CrucibleUiPlugin.Active) { KitNotices.Show(source, new KitNotice(text, (KitNoticeKind)1, "", 0, (Sprite)null, true, false, true)); } else { MessageHud.instance.ShowMessage((MessageType)2, text, 0, (Sprite)null, false, true); } } else { log.Info(text); } } } public static class BeaconAutoJoin { private static readonly ReconnectSession reconnect = new ReconnectSession(); private static AutoJoinCountdown? countdown; private static ServerEntry? countdownServer; private static int countdownCharacter = -1; private static bool autoJoinDecided; private static bool subscribed; private static int reconnectAttempt; private static int reconnectLimit; private static double Now => Time.realtimeSinceStartup; public static bool Waiting { get { if (countdown == null) { return reconnect.Pending; } return true; } } public static void Initialize() { if (!subscribed) { BeaconJoin.JoinStarted += OnJoinStarted; BeaconJoin.ReturnedToMenu += OnMenu; BeaconJoin.Connected += OnConnected; subscribed = true; } } public static string? Cancel(MeadworksLog? log) { autoJoinDecided = true; string text = null; if (countdown != null) { countdown.Cancel(); countdown = null; countdownServer = null; text = BeaconText.Text("autojoin_canceled"); } else if (reconnect.Cancel()) { text = BeaconText.Text("reconnect_canceled"); } if (text != null && log != null) { log.Info(text); } BeaconJoinCard.Remove(); return text; } public static void Tick(MeadworksLog log) { if (!BeaconJoin.Active || Plugin.Settings == null) { return; } Plugin.Settings.RefreshReconnectPolicy(); if ((countdown != null || reconnect.Pending) && CancelPressed()) { Cancel(log); } if (BeaconImportSecrets.Busy) { return; } if (reconnect.Pending && BeaconMenu.Ready && reconnect.Tick(Now)) { ServerAddress target = reconnect.Target; if (target == (ServerAddress)null || !BeaconMenu.Join(target, null)) { reconnect.Cancel(); } } if (!autoJoinDecided && BeaconMenu.Ready && !BeaconJoin.Joining && !reconnect.Pending) { StartCountdown(log); } if (countdown != null) { TickCountdown(); } if (countdown != null && countdownServer != null) { BeaconJoinCard.Show(JoinCard.Countdown(countdownServer.Name, countdown.SecondsShown)); } else if (reconnect.Pending) { BeaconJoinCard.Show(JoinCard.Reconnect(reconnectAttempt, reconnectLimit, reconnect.SecondsLeft(Now))); } else { BeaconJoinCard.Remove(); } } private static void StartCountdown(MeadworksLog log) { //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Invalid comparison between Unknown and I4 //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Invalid comparison between Unknown and I4 //IL_0088: Unknown result type (might be due to invalid IL or missing references) //IL_008d: Unknown result type (might be due to invalid IL or missing references) //IL_0112: Unknown result type (might be due to invalid IL or missing references) //IL_011c: Expected O, but got Unknown autoJoinDecided = true; BeaconConfig settings = Plugin.Settings; string text = settings.Get<string>(AutoJoin.Server); AutoJoinDecision val = AutoJoinRules.Decide(settings.Get<bool>(AutoJoin.Enabled), text, BeaconServers.List, AutoJoinRules.LaunchJoins((IReadOnlyList<string>)Environment.GetCommandLineArgs()), false); if ((int)((AutoJoinDecision)(ref val)).Skip == 5) { log.Info(BeaconText.Format("autojoin_unknown_server", text.Trim())); return; } if ((int)((AutoJoinDecision)(ref val)).Skip != 0 || ((AutoJoinDecision)(ref val)).Target == null) { if ((int)((AutoJoinDecision)(ref val)).Skip != 1) { log.Verbose("Beacon auto-join skipped: " + ((object)((AutoJoinDecision)(ref val)).Skip/*cast due to .constrained prefix*/).ToString() + "."); } return; } string text2 = settings.Get<string>(AutoJoin.Character); (IReadOnlyList<string> Names, IReadOnlyList<string> Files, int Selected) tuple = BeaconMenu.Characters(); IReadOnlyList<string> item = tuple.Names; IReadOnlyList<string> item2 = tuple.Files; int item3 = tuple.Selected; int num = AutoJoinRules.PickCharacter(item, item2, text2, item3); if (num < 0) { log.Info(BeaconText.Format("autojoin_unknown_character", text2.Trim())); return; } countdown = new AutoJoinCountdown(settings.Get<int>(AutoJoin.CountdownSeconds)); countdownServer = ((AutoJoinDecision)(ref val)).Target; countdownCharacter = num; log.Info(BeaconText.Format("autojoin_countdown", ((AutoJoinDecision)(ref val)).Target.Name, countdown.SecondsShown)); } private static void TickCountdown() { //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Invalid comparison between Unknown and I4 if ((int)countdown.Tick(Time.unscaledDeltaTime) == 1) { ServerEntry val = countdownServer; int value = countdownCharacter; countdown = null; countdownServer = null; if (val != null) { BeaconMenu.Join(val.Address, value); } } } private static bool CancelPressed() { if (MeadworksInput.Typing) { return false; } if (!MeadworksInput.PadDown((PadButton)1)) { return false; } MeadworksInput.PadConsume((PadButton)1); return true; } private static void OnJoinStarted(ServerAddress? target) { Plugin.Settings?.BeginReconnectConnection(target); autoJoinDecided = true; countdown = null; countdownServer = null; BeaconJoinCard.Remove(); reconnect.OnJoinStarted(target); } private static void OnConnected(ServerAddress target) { reconnect.OnConnected(Now); } private static void OnMenu(MenuOutcome outcome) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Invalid comparison between Unknown and I4 //IL_0047: 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_0085: Unknown result type (might be due to invalid IL or missing references) //IL_008e: Unknown result type (might be due to invalid IL or missing references) //IL_0093: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_0096: Unknown result type (might be due to invalid IL or missing references) //IL_00a8: Expected I4, but got Unknown //IL_00fb: Unknown result type (might be due to invalid IL or missing references) //IL_0101: Invalid comparison between Unknown and I4 if (((MenuOutcome)(ref outcome)).Target == (ServerAddress)null || Plugin.Settings == null) { return; } if ((int)((MenuOutcome)(ref outcome)).Step == 1) { reconnect.Cancel(); return; } BeaconConfig settings = Plugin.Settings; settings.RefreshReconnectPolicy(); ReconnectPlan val = reconnect.OnDisconnected(((MenuOutcome)(ref outcome)).Target, ((MenuOutcome)(ref outcome)).Kind, ((MenuOutcome)(ref outcome)).WasConnected, settings.Get<bool>(Reconnect.Enabled), settings.ReconnectPolicy.LockedFor(((MenuOutcome)(ref outcome)).Target), settings.Get<int>(Reconnect.Attempts), Now); MeadworksLog log = Plugin.Log; ReconnectStep step = ((ReconnectPlan)(ref val)).Step; switch (step - 1) { case 0: reconnectAttempt = ((ReconnectPlan)(ref val)).Attempt; reconnectLimit = ((ReconnectPlan)(ref val)).Limit; if (log != null) { log.Info(BeaconText.Format("reconnecting", ((ReconnectPlan)(ref val)).Attempt, ((ReconnectPlan)(ref val)).Limit)); } break; case 1: if ((int)((ReconnectPlan)(ref val)).Decision == 4 && log != null) { log.Info(BeaconText.Format("reconnect_stopped", ((ReconnectPlan)(ref val)).Limit)); } break; case 2: if (log != null) { log.Info(BeaconText.Text("reconnect_locked")); } break; } } public static void Reset() { if (subscribed) { BeaconJoin.JoinStarted -= OnJoinStarted; BeaconJoin.ReturnedToMenu -= OnMenu; BeaconJoin.Connected -= OnConnected; subscribed = false; } reconnect.Cancel(); BeaconJoinCard.Remove(); countdown = null; countdownServer = null; countdownCharacter = -1; autoJoinDecided = false; } } public static class BeaconJoin { private static readonly JoinCoordinator coordinator = new JoinCoordinator(); private static string? sentStored; private static string? typed; private static bool injected; private static bool suppressCapture; private static ServerAddress? rejoinForPassword; private static CancellationTokenSource? readCancel; private static Task<SecretReadResult>? read; private static LinuxKeyringStore? readLinux; private static long readFallbackRevision; private static ZNet? deferredNet; private static ZRpc? deferredRpc; private static string? deferredSalt; private static ServerAddress? deferredTarget; private static string? resumedSecret; private static bool resuming; private static IDisposable? saveOperation; private static string? savingSecret; internal static JoinCoordinator Coordinator => coordinator; public static bool Active => BeaconModules.Active((BeaconModule)0); public static bool Joining => coordinator.AwaitingConnection; internal static event Action<ServerAddress?>? JoinStarted; internal static event Action<MenuOutcome>? ReturnedToMenu; internal static event Action<ServerAddress>? Connected; internal static void OnJoinServer(ServerJoinData data) { //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) if (Active && Plugin.Settings != null) { ServerAddress val = TargetOf(data); if (val != coordinator.Target) { ClearRead(); } BeaconConfig settings = Plugin.Settings; coordinator.OnJoinStarted(val, BeaconServers.List, (Func<ServerEntry, bool>)((ServerEntry e) => BeaconSecrets.Store is IAsyncSecretStore || BeaconSecrets.Store.Contains(e.SecretRef)), new JoinOptions(settings.Get<bool>(General.SkipPasswordPrompt), settings.Get<bool>(General.OfferToSavePasswords))); BeaconJoin.JoinStarted?.Invoke(val); } } internal static void BeforeHandshake(bool needPassword) { //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Invalid comparison between Unknown and I4 //IL_008a: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Invalid comparison between Unknown and I4 injected = false; suppressCapture = false; if (!needPassword || !Active) { return; } if (FejdStartup.ServerPassword != null) { suppressCapture = true; } else if ((int)coordinator.OnPasswordRequested() == 1) { ServerEntry entry = coordinator.Entry; ISecretStore store = BeaconSecrets.Store; string text = resumedSecret; bool flag = resumedSecret != null || (!(store is IAsyncSecretStore) && entry != null && store.TryRead(entry.SecretRef, ref text)); if (entry == null || !flag || string.IsNullOrEmpty(text)) { coordinator.OnStoredUnreadable(); Report(((int)store.LastFailure == 1 && entry != null) ? BeaconText.Format("secret_stored_elsewhere", entry.Name) : BeaconText.Text("secret_store_failed")); } else { BeaconSecrets.Redactor.Track(text); sentStored = text; suppressCapture = true; injected = true; FejdStartup.ServerPassword = text; } } } internal static void AfterHandshake() { if (injected) { FejdStartup.ServerPassword = null; } injected = false; suppressCapture = false; } internal static void BeforePasswordEntered(string password) { if (!suppressCapture && Active && !string.IsNullOrEmpty(password) && !(coordinator.Target == (ServerAddress)null)) { ForgetTyped(); typed = password; BeaconSecrets.Redactor.Track(password); coordinator.OnPlayerTyped(); } } internal static void OnMenu(GameStatus status) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Invalid comparison between Unknown and I4 //IL_004b: Unknown result type (might be due to invalid IL or missing references) ResetAsyncSecrets(); AfterHandshake(); ForgetSecrets(); MenuOutcome obj = coordinator.OnReturnedToMenu(status); if (!(((MenuOutcome)(ref obj)).Target == (ServerAddress)null)) { if ((int)((MenuOutcome)(ref obj)).Step == 1) { rejoinForPassword = ((MenuOutcome)(ref obj)).Target; } BeaconJoin.ReturnedToMenu?.Invoke(obj); } } public static void Tick(MeadworksLog log) { //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Invalid comparison between Unknown and I4 if (!Active) { ResumeWithoutSecret(); ResetAsyncSecrets(); return; } TickHandshake(); if (coordinator.AwaitingConnection && (Object)(object)ZNet.instance != (Object)null && !ZNet.instance.IsServer() && (int)ZNet.GetConnectionStatus() == 2) { Accept(log); } if (rejoinForPassword != (ServerAddress)null && BeaconMenu.Ready) { ServerAddress? address = rejoinForPassword; rejoinForPassword = null; BeaconMenu.Join(address, null); } } private static void Accept(MeadworksLog log) { //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Invalid comparison between Unknown and I4 //IL_00a4: Unknown result type (might be due to invalid IL or missing references) //IL_00a6: Invalid comparison between Unknown and I4 ServerAddress target = coordinator.Target; ServerEntry entry = coordinator.Entry; JoinAction val = coordinator.OnConnected(); if ((int)val == 4 && entry != null && typed != null) { if (BeaconSecrets.Store is IAsyncSecretStore) { SaveTypedAsync(entry.SecretRef, typed, entry.Name); } else if (BeaconSecrets.Store.Store(entry.SecretRef, typed)) { log.Info(BeaconSecrets.Redactor.Clean(BeaconText.Format("password_saved", entry.Name))); } else { Report(BeaconText.Text("secret_store_failed")); } } string text = (((int)val == 5) ? typed : null); ForgetSecrets(); if (!string.IsNullOrEmpty(text)) { BeaconSaveOffer.Offer(target, text); } BeaconJoin.Connected?.Invoke(target); } public static ServerAddress? TargetOf(ServerJoinData data) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //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_0007: 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_001b: Expected I4, but got Unknown //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) ServerJoinDataType type = data.m_type; switch (type - 1) { case 2: { ServerJoinDataDedicated dedicated = ((ServerJoinData)(ref data)).Dedicated; return JoinTargets.FromHost(((ServerJoinDataDedicated)(ref dedicated)).m_host, (int)((ServerJoinData)(ref data)).Dedicated.m_port); } case 0: return JoinTargets.FromSteam(((ServerJoinData)(ref data)).SteamUser.m_joinUserID.m_SteamID); case 1: return JoinTargets.FromPlayFab(((ServerJoinData)(ref data)).PlayFabUser.m_remotePlayerId); default: return null; } } public static bool TryJoinData(ServerAddress address, out ServerJoinData data) { //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_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0011: 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_0024: Expected I4, but got Unknown //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_003e: 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_007f: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) data = ServerJoinData.None; AddressKind kind = address.Kind; switch ((int)kind) { case 0: data = new ServerJoinData(new ServerJoinDataDedicated(address.Host, (ushort)address.Port)); return ((ServerJoinData)(ref data)).IsValid; case 1: { ulong num = default(ulong); if (!JoinTargets.TryGetSteamId(address, ref num)) { return false; } data = new ServerJoinData(new ServerJoinDataSteamUser(num)); return ((ServerJoinData)(ref data)).IsValid; } case 2: data = new ServerJoinData(new ServerJoinDataPlayFabUser(address.Host)); return ((ServerJoinData)(ref data)).IsValid; default: return false; } } private static void Report(string text) { MeadworksLog? log = Plugin.Log; if (log != null) { log.Warning(BeaconSecrets.Redactor.Clean(text)); } } private static void ForgetTyped() { BeaconSecrets.Redactor.Untrack(typed); typed = null; } private static void ForgetSecrets() { ForgetTyped(); BeaconSecrets.Redactor.Untrack(sentStored); sentStored = null; } public static void Reset() { ResetAsyncSecrets(); AfterHandshake(); ForgetSecrets(); coordinator.Reset(); rejoinForPassword = null; BeaconJoin.JoinStarted = null; BeaconJoin.ReturnedToMenu = null; BeaconJoin.Connected = null; } internal static bool DeferHandshake(ZNet net, ZRpc rpc, bool needPassword, string salt) { //IL_0082: Unknown result type (might be due to invalid IL or missing references) //IL_0088: Invalid comparison between Unknown and I4 if (!resuming && (!Active || FejdStartup.ServerPassword != null) && read != null) { ClearRead(); coordinator.OnStoredUnreadable(); } if (!resuming && needPassword && Active && FejdStartup.ServerPassword == null) { ISecretStore store = BeaconSecrets.Store; IAsyncSecretStore val = (IAsyncSecretStore)(object)((store is IAsyncSecretStore) ? store : null); if (val != null && coordinator.Entry != null) { if (read != null && (Object)(object)deferredNet == (Object)(object)net && deferredRpc == rpc) { return true; } if ((int)coordinator.OnPasswordRequested() != 1) { return false; } ClearRead(); readCancel = new CancellationTokenSource(); readLinux = (LinuxKeyringStore?)(object)((val is LinuxKeyringStore) ? val : null); LinuxKeyringStore? obj = readLinux; readFallbackRevision = ((obj != null) ? obj.FallbackRevision : 0); read = val.ReadAsync(coordinator.Entry.SecretRef, readCancel.Token); deferredNet = net; deferredRpc = rpc; deferredSalt = salt; deferredTarget = coordinator.Target; return true; } } return false; } private static void TickHandshake() { //IL_00a0: Unknown result type (might be due to invalid IL or missing references) //IL_00a6: Unknown result type (might be due to invalid IL or missing references) //IL_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_00af: Unknown result type (might be due to invalid IL or missing references) //IL_012f: Unknown result type (might be due to invalid IL or missing references) if (read == null) { return; } if (!coordinator.AwaitingConnection || deferredTarget != coordinator.Target || (Object)(object)deferredNet == (Object)null || (Object)(object)deferredNet != (Object)(object)ZNet.instance || deferredRpc == null || !deferredRpc.IsConnected()) { ClearRead(); } else { if (!read.IsCompleted) { return; } SecretReadResult val = (SecretReadResult)((!read.IsCanceled && !read.IsFaulted && (readLinux == null || readLinux.IsReadCurrent(readFallbackRevision))) ? read.Result : default(SecretReadResult)); ZNet val2 = deferredNet; ZRpc val3 = deferredRpc; string text = deferredSalt; ClearRead(); bool flag = FejdStartup.ServerPassword != null; if (flag) { coordinator.OnStoredUnreadable(); } if (!flag && ((SecretReadResult)(ref val)).Found && !string.IsNullOrEmpty(((SecretReadResult)(ref val)).Secret)) { resumedSecret = ((SecretReadResult)(ref val)).Secret; BeaconSecrets.Redactor.Track(resumedSecret); } else if (!flag) { coordinator.OnStoredUnreadable(); if ((int)BeaconSecrets.Store.LastFailure != 0) { Report(BeaconText.Text("secret_store_failed")); } } try { resuming = true; val2.RPC_ClientHandshake(val3, true, text); } catch (Exception ex) { MeadworksLog? log = Plugin.Log; if (log != null) { log.Warning("Beacon resumed handshake: " + ex.GetType().Name); } } finally { resuming = false; BeaconSecrets.Redactor.Untrack(resumedSecret); resumedSecret = null; } } } private static void ResumeWithoutSecret() { if (read == null) { return; } bool num = coordinator.AwaitingConnection && deferredTarget == coordinator.Target && (Object)(object)deferredNet != (Object)null && (Object)(object)deferredNet == (Object)(object)ZNet.instance && deferredRpc != null && deferredRpc.IsConnected(); ZNet val = deferredNet; ZRpc val2 = deferredRpc; string text = deferredSalt; ClearRead(); coordinator.OnStoredUnreadable(); if (!num) { return; } try { resuming = true; val.RPC_ClientHandshake(val2, true, text); } catch (Exception ex) { MeadworksLog? log = Plugin.Log; if (log != null) { log.Warning("Beacon native handshake fallback: " + ex.GetType().Name); } } finally { resuming = false; } } private static void SaveTypedAsync(string reference, string secret, string name) { ClearSave(); savingSecret = secret; BeaconSecrets.Redactor.Track(secret); saveOperation = BeaconSecretActions.Run((CancellationToken cancel) => BeaconSecretActions.Store(reference, secret, cancel), delegate(bool saved) { if (saved) { MeadworksLog? log = Plugin.Log; if (log != null) { log.Info(BeaconSecrets.Redactor.Clean(BeaconText.Format("password_saved", name))); } } else { Report(BeaconText.Text("secret_store_failed")); } ClearSave(); }, delegate { Report(BeaconText.Text("secret_store_failed")); ClearSave(); }); } private static void ClearSave() { saveOperation?.Dispose(); saveOperation = null; BeaconSecrets.Redactor.Untrack(savingSecret); savingSecret = null; } private static void ClearRead() { readCancel?.Cancel(); readCancel?.Dispose(); readCancel = null; readLinux = null; read = null; deferredNet = null; deferredRpc = null; deferredSalt = null; deferredTarget = null; } private static void ResetAsyncSecrets() { ClearRead(); ClearSave(); BeaconSecrets.Redactor.Untrack(resumedSecret); resumedSecret = null; resuming = false; } } internal static class BeaconJoinCard { private const float Width = 420f; private static KitWindow? card; private static TMP_Text? text; private static string shown = ""; public static void Show(IReadOnlyList<CardLine> lines) { //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00e2: Unknown result type (might be due to invalid IL or missing references) //IL_00f1: Unknown result type (might be due to invalid IL or missing references) //IL_0156: Unknown result type (might be due to invalid IL or missing references) //IL_0165: Unknown result type (might be due to invalid IL or missing references) if (!MeadworksHeadless.AllowsUi) { return; } string text = string.Join("\n", lines.Select((CardLine l) => BeaconText.Format(((CardLine)(ref l)).Key, ((CardLine)(ref l)).Arguments.ToArray()))); if (card == null || (Object)(object)card.Panel == (Object)null || (Object)(object)BeaconJoinCard.text == (Object)null) { Remove(); FejdStartup instance = FejdStartup.instance; object obj; if (instance == null) { obj = null; } else { GameObject mainMenu = instance.m_mainMenu; if (mainMenu == null) { obj = null; } else { Canvas componentInParent = mainMenu.GetComponentInParent<Canvas>(); obj = ((componentInParent != null) ? componentInParent.rootCanvas : null); } } Canvas val = (Canvas)obj; if ((Object)(object)val == (Object)null) { return; } card = KitWindow.Create("BeaconJoinCard", new Vector2(420f, 220f), ((Component)val).transform, false, false); if (card == null) { return; } BeaconJoinCard.text = KitAlmanacDialog.Label((Transform)(object)card.Panel, "Lines", text, new Vector2(20f, -20f), new Vector2(380f, 120f), 18f); BeaconJoinCard.text.alignment = (TextAlignmentOptions)514; Button val2 = KitButton.Create(card.Panel, "Cancel", BeaconPanel.Ui("cancel"), (Action)delegate { BeaconAutoJoin.Cancel(Plugin.Log); }, new Vector2(100f, -160f), new Vector2(220f, 38f)); card.Focusable((Selectable)(object)val2, 0, 0); BeaconPanel.Tip(((Component)val2).gameObject, "cancel", BeaconText.Text("card_cancel_hint")); card.Closed += Cancel; card.Open(); shown = ""; } if (!(text == shown) || !card.IsOpen) { BeaconJoinCard.text.text = text; shown = text; card.Open(); } } private static void Cancel() { BeaconAutoJoin.Cancel(Plugin.Log); } public static void Remove() { if (card != null) { card.Closed -= Cancel; card.Close(); card.Destroy(); } card = null; text = null; shown = ""; } } [HarmonyPatch] internal static class BeaconJoinPatches { [HarmonyPatch(typeof(FejdStartup), "JoinServer")] [HarmonyPrefix] private static void JoinServer(FejdStartup __instance) { Guard(delegate { //IL_0006: Unknown result type (might be due to invalid IL or missing references) BeaconJoin.OnJoinServer(__instance.m_joinServer); }); } [HarmonyPatch(typeof(ZNet), "RPC_ClientHandshake")] [HarmonyPrefix] private static bool Handshake(ZNet __instance, ZRpc rpc, bool needPassword, string serverPasswordSalt) { try { if (BeaconJoin.DeferHandshake(__instance, rpc, needPassword, serverPasswordSalt)) { return false; } BeaconJoin.BeforeHandshake(needPassword); } catch (Exception ex) { MeadworksLog? log = Plugin.Log; if (log != null) { log.Warning("Beacon join hook: " + ex.GetType().Name); } } return true; } [HarmonyPatch(typeof(ZNet), "RPC_ClientHandshake")] [HarmonyFinalizer] private static Exception? HandshakeDone(Exception? __exception) { Guard(BeaconJoin.AfterHandshake); return __exception; } [HarmonyPatch(typeof(ZNet), "OnPasswordEntered")] [HarmonyPrefix] private static void PasswordEntered(string pwd) { Guard(delegate { BeaconJoin.BeforePasswordEntered(pwd); }); } [HarmonyPatch(typeof(FejdStartup), "ShowConnectError")] [HarmonyPostfix] private static void ConnectError(ConnectionStatus statusOverride) { //IL_0006: 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) Guard(delegate { //IL_0001: 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_0009: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) BeaconJoin.OnMenu(ConnectionOutcomes.Parse(((object)(((int)statusOverride == 0) ? ZNet.GetConnectionStatus() : statusOverride)/*cast due to .constrained prefix*/).ToString())); }); } private static void Guard(Action action) { try { action(); } catch (Exception ex) { MeadworksLog? log = Plugin.Log; if (log != null) { log.Warning("Beacon join hook: " + ex.GetType().Name); } } } } internal static class BeaconMenu { public static bool Ready { get { FejdStartup instance = FejdStartup.instance; if ((Object)(object)instance == (Object)null || (Object)(object)Game.instance != (Object)null) { return false; } GameObject mainMenu = instance.m_mainMenu; if (mainMenu == null || !mainMenu.activeInHierarchy) { GameObject serverListPanel = instance.m_serverListPanel; if (serverListPanel == null || !serverListPanel.activeInHierarchy) { return false; } } if (CinematicsManager.IsStartedPlaying()) { return false; } if (((ServerJoinData)(ref instance.m_queuedJoinServer)).IsValid || MatchmakingManager.HasPendingInvite()) { return false; } if (instance.m_profiles != null) { return instance.m_profiles.Count > 0; } return false; } } public static (IReadOnlyList<string> Names, IReadOnlyList<string> Files, int Selected) Characters() { List<string> list = new List<string>(); List<string> list2 = new List<string>(); FejdStartup instance = FejdStartup.instance; if (instance?.m_profiles == null) { return (Names: list, Files: list2, Selected: -1); } foreach (PlayerProfile profile in instance.m_profiles) { list.Add(profile.GetName() ?? ""); list2.Add(profile.GetFilename() ?? ""); } return (Names: list, Files: list2, Selected: instance.m_profileIndex); } public static bool Join(ServerAddress address, int? character) { //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) if (!Ready || !BeaconJoin.TryJoinData(address, out var data)) { return false; } FejdStartup instance = FejdStartup.instance; int num = character ?? instance.m_profileIndex; if (num < 0 || num >= instance.m_profiles.Count) { return false; } if ((Object)(object)instance.m_connectionFailedPanel != (Object)null) { instance.m_connectionFailedPanel.SetActive(false); } instance.m_profileIndex = num; instance.m_queuedJoinServer = data; instance.OnCharacterStart(); return true; } } [BepInPlugin("com.meadworks.beacon", "Beacon", "0.1.14")] [BepInDependency("com.meadworks.crucible", "0.3.26")] [BepInDependency("com.meadworks.crucible.ui", "0.3.19")] [BepInDependency("com.jotunn.jotunn", "2.30.2")] [NetworkCompatibility(/*Could not decode attribute arguments.*/)] public sealed class Plugin : BaseUnityPlugin { public const string Guid = "com.meadworks.beacon"; public const string Name = "Beacon"; public const string Version = "0.1.14"; public const string Family = "Runestone"; private const string CrucibleUiGuid = "com.meadworks.crucible.ui"; private ProductRegistration? registration; private MeadworksLog? log; private Harmony? harmony; private bool started; internal static ProductRegistration? Source { get { if (!Chainloader.PluginInfos.TryGetValue("com.meadworks.beacon", out var value)) { return null; } return (value.Instance as Plugin)?.registration; } } public static bool Ready { get; private set; } public static BeaconConfig? Settings { get; private set; } public static MeadworksLog? Log { get; private set; } private void Awake() { //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Expected O, but got Unknown log = MeadworksLog.For(((BaseUnityPlugin)this).Logger, "Beacon"); Log = log; try { if (!Plugin.IsReady) { throw new InvalidOperationException("Required Crucible is not ready"); } registration = Plugin.Services.Register(new ProductMetadata("com.meadworks.beacon", "Beacon", new Version("0.1.14"), new Version(0, 1), new Version(0, 1), (MultiplayerPolicy)1, "Runestone")); Settings = BeaconConfig.Bind(((BaseUnityPlugin)this).Config, log); if (!MeadworksHeadless.AllowsUi) { log.Debug("Beacon loaded on a headless process: server policy bound; no UI, password storage or joining."); return; } BeaconText.Initialize(((BaseUnityPlugin)this).Info.Metadata, ((BaseUnityPlugin)this).Logger); Ready = true; } catch (Exception ex) { log.Error((object)("Beacon initialization rejected: " + ex.GetType().Name)); Shutdown(); } } private void Start() { //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Expected O, but got Unknown //IL_0098: Unknown result type (might be due to invalid IL or missing references) //IL_00a2: Expected O, but got Unknown if (!Ready || log == null) { return; } try { BeaconModules.Decide(log); BeaconSecrets.Initialize(Settings, log); BeaconServers.Load(log); if (BeaconModules.Active((BeaconModule)0)) { harmony = new Harmony("com.meadworks.beacon"); harmony.CreateClassProcessor(typeof(BeaconJoinPatches)).Patch(); harmony.CreateClassProcessor(typeof(BeaconFavoritePatches)).Patch(); BeaconAutoJoin.Initialize(); } if (harmony == null) { harmony = new Harmony("com.meadworks.beacon"); } harmony.CreateClassProcessor(typeof(BeaconServerListPatches)).Patch(); CommandManager.Instance.AddConsoleCommand((ConsoleCommand)(object)new BeaconCommand()); BeaconConversion.Register(); started = true; } catch (Exception ex) { log.Error((object)("Beacon modules rejected: " + ex.GetType().Name)); Shutdown(); } } private void Update() { if (Ready) { BeaconText.Tick(); if (started && log != null) { BeaconModules.Tick(log); BeaconSecrets.Tick(log); BeaconSecretActions.Tick(); BeaconFavorites.Tick(); BeaconMenuBack.Tick(); BeaconPanel.Tick(); BeaconJoin.Tick(log); BeaconAutoJoin.Tick(log); BeaconSaveOffer.Tick(); } } } private void OnDestroy() { Shutdown(); } private void Shutdown() { Ready = false; started = false; try { Harmony? obj = harmony; if (obj != null) { obj.UnpatchSelf(); } } catch (Exception ex) { MeadworksLog? obj2 = log; if (obj2 != null) { obj2.Warning("Beacon unpatch: " + ex.GetType().Name); } } harmony = null; Action[] array = new Action[12] { BeaconConversion.Reset, BeaconAutoJoin.Reset, BeaconSaveOffer.Reset, BeaconFavorites.Reset, BeaconPanel.Reset, BeaconImportSecrets.Reset, BeaconJoin.Reset, BeaconSecretActions.Reset, BeaconServers.Unload, BeaconSecrets.Shutdown, BeaconModules.Reset, BeaconText.Shutdown }; foreach (Action action in array) { try { action(); } catch (Exception ex2) { MeadworksLog? obj3 = log; if (obj3 != null) { obj3.Warning("Beacon teardown: " + ex2.GetType().Name); } } } Settings?.Shutdown(); Settings = null; ProductRegistration? obj4 = registration; if (obj4 != null) { obj4.Dispose(); } registration = null; } } public static class BeaconSecrets { private static Task<bool>? refresh; private static bool reportedFallback; private static BeaconConfig? linuxSettings; public static ISecretStore Store { get; private set; } = (ISecretStore)new DisabledSecretStore(); public static SecretRedactor Redactor { get; } = new SecretRedactor(); public static string SecretFilePath => Path.Combine(BeaconServers.DataDirectory, "secrets.dat"); public static void Initialize(BeaconConfig settings, MeadworksLog log) { //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Invalid comparison between Unknown and I4 //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Expected O, but got Unknown //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_0072: Invalid comparison between Unknown and I4 //IL_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_00ba: Expected O, but got Unknown //IL_00ac: Unknown result type (might be due to invalid IL or missing references) //IL_00b2: Expected O, but got Unknown //IL_00d3: Unknown result type (might be due to invalid IL or missing references) //IL_00d8: Unknown result type (might be due to invalid IL or missing references) SecretPlatform val = (SecretPlatform)((!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) ? (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? 1 : 2) : 0); if ((int)val == 1) { linuxSettings = settings; LinuxKeyringStore val2 = new LinuxKeyringStore(settings.Get<LinuxFallback>(Passwords.LinuxFallback)); Store = (ISecretStore)val2; refresh = val2.RefreshAsync(default(CancellationToken)); reportedFallback = false; } else { SecretBackend val3 = SecretStores.Choose(val, false, settings.Get<LinuxFallback>(Passwords.LinuxFallback)); ISecretStore store = (ISecretStore)(((int)val3 == 0) ? ((object)new DpapiSecretStore((Func<string>)ReadSecretFile, (Action<string>)WriteSecretFile)) : (((int)val3 != 3) ? ((object)MemorySecretStore.SessionOnly()) : ((object)new DisabledSecretStore()))); Store = store; log.Info("Beacon password storage: " + ((object)Store.Backend/*cast due to .constrained prefix*/).ToString() + "."); } } public static void Shutdown() { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Expected O, but got Unknown if (Store is IDisposable disposable) { disposable.Dispose(); } Store = (ISecretStore)new DisabledSecretStore(); refresh = null; linuxSettings = null; Redactor.Clear(); } public static void Tick(MeadworksLog log) { //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_0071: Invalid comparison between Unknown and I4 //IL_0095: Unknown result type (might be due to invalid IL or missing references) ISecretStore store = Store; LinuxKeyringStore val = (LinuxKeyringStore)(object)((store is LinuxKeyringStore) ? store : null); if (val != null && linuxSettings != null) { val.SetFallback(linuxSettings.Get<LinuxFallback>(Passwords.LinuxFallback)); } if (refresh != null && refresh.IsCompleted) { if (refresh.IsFaulted) { log.Warning("Beacon keyring initialization failed; no password was saved."); } refresh = null; } if (Store is LinuxKeyringStore && (int)Store.Backend != 1 && !reportedFallback) { reportedFallback = true; log.Warning(BeaconText.Text("secret_store_failed") + " " + BeaconText.Text(SecretStores.StorageLineKey(Store.Backend))); } } private static string? ReadSecretFile() { if (!File.Exists(SecretFilePath)) { return null; } return File.ReadAllText(SecretFilePath); } private static void WriteSecretFile(string text) { BeaconServers.WriteAtomically(SecretFilePath, text); } } internal static class BeaconFavorites { internal sealed class RemovalState { internal ServerJoinData Data; internal ServerAddress Address; internal ServerAddress Canonical; internal string Reference; internal RemovalState(ServerJoinData data, ServerAddress address, ServerEntry entry) { //IL_0007: 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) Data = data; Address = address; Reference = entry.SecretRef; Canonical = entry.Address; } } private static ServerListGui? owner; private static readonly Dictionary<string, ServerAddress> added = new Dictionary<string, ServerAddress>(); private static readonly HashSet<ServerAddress> suppressed = new HashSet<ServerAddress>(); private static readonly Queue<RemovalState> deferred = new Queue<RemovalState>(); private static readonly HashSet<string> reportedErrors = new HashSet<string>(); private static float nextSync; internal static RemovalState? Capture(ServerListGui gui) { //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_0054: 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) if (!BeaconJoin.Active || gui.m_favoriteServersList == null || (object)gui.m_serverLists[gui.m_currentServerList] != gui.m_favoriteServersList) { return null; } int selectedServer = gui.GetSelectedServer(); List<ServerListEntryData> currentServerListFiltered = gui.CurrentServerListFiltered; if (selectedServer < 0 || selectedServer >= currentServerListFiltered.Count) { return null; } ServerJoinData joinData = currentServerListFiltered[selectedServer].m_joinData; ServerAddress val = BeaconJoin.TargetOf(joinData); ServerEntry val2 = ((val == (ServerAddress)null) ? null : BeaconServers.List.Find(val)); if (val2 != null && !(val == (ServerAddress)null)) { return new RemovalState(joinData, val, val2); } return null; } internal static void Removed(ServerListGui gui, RemovalState? state) { //IL_0011: Unknown result type (might be due to invalid IL or missing references) if (state != null && BeaconJoin.Active && !gui.m_favoriteServersList.Contains(state.Data)) { suppressed.Add(state.Canonical); BeaconConfig? settings = Plugin.Settings; if (settings != null && settings.Get<bool>(General.RemoveEntryWithFavorite)) { deferred.Enqueue(state); } } } public static void Tick() { //IL_0092: Unknown result type (might be due to invalid IL or missing references) if (!BeaconJoin.Active) { Reset(); return; } BeaconFavoriteUndo.Tick(); if ((Object)(object)Game.instance != (Object)null || BeaconImportSecrets.Busy || Plugin.Settings == null || Plugin.Log == null) { return; } ServerListGui s_instance = ServerListGui.s_instance; if ((Object)(object)s_instance == (Object)null || s_instance.m_favoriteServersList == null) { return; } if (!Plugin.Settings.Get<bool>(General.RemoveEntryWithFavorite)) { deferred.Clear(); } while (deferred.Count > 0) { RemovalState removal = deferred.Dequeue(); if (s_instance.m_favoriteServersList.Contains(removal.Data)) { suppressed.Remove(removal.Canonical); continue; } ServerEntry current = ((IEnumerable<ServerEntry>)BeaconServers.List.Entries).FirstOrDefault((Func<ServerEntry, bool>)((ServerEntry e) => e.SecretRef == removal.Reference)); if (current != null && current.Matches(removal.Address) && !(current.Address != removal.Canonical)) { ServerRemoval undo = null; if (BeaconServers.Amend(delegate(ServerList candidate) { undo = candidate.RemoveForUndo(current.Name); }, Plugin.Log) && undo != null) { BeaconFavoriteUndo.Show(undo, removal.Address); } else { BeaconFavoriteUndo.Failure(BeaconPanel.Ui("write_failed")); } } } if ((Object)(object)owner != (Object)(object)s_instance) { owner = s_instance; added.Clear(); nextSync = 0f; } if (Time.unscaledTime < nextSync) { return; } nextSync = Time.unscaledTime + 0.5f; try { Sync(s_instance, Plugin.Settings.Get<bool>(General.AddToFavorites)); } catch (Exception ex) { if (reportedErrors.Add(ex.GetType().Name)) { Plugin.Log.Warning("Beacon favorites synchronization: " + ex.GetType().Name); } } } private static void Sync(ServerListGui gui, bool enabled) { //IL_0085: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0102: Unknown result type (might be due to invalid IL or missing references) //IL_010b: Unknown result type (might be due to invalid IL or missing references) //IL_0119: Unknown result type (might be due to invalid IL or missing references) //IL_0124: Unknown result type (might be due to invalid IL or missing references) LocalServerList favoriteServersList = gui.m_favoriteServersList; bool flag = false; KeyValuePair<string, ServerAddress>[] array = added.ToArray(); for (int i = 0; i < array.Length; i++) { KeyValuePair<string, ServerAddress> pair = array[i]; ServerEntry val = ((IEnumerable<ServerEntry>)BeaconServers.List.Entries).FirstOrDefault((Func<ServerEntry, bool>)((ServerEntry e) => e.SecretRef == pair.Key)); if (!enabled || val == null || !(val.Address == pair.Value)) { if (BeaconJoin.TryJoinData(pair.Value, out var data) && favoriteServersList.Contains(data)) { favoriteServersList.Remove(data); flag = true; } added.Remove(pair.Key); } } if (enabled) { foreach (ServerEntry entry in BeaconServers.List.Entries) { if (!suppressed.Contains(entry.Address) && BeaconJoin.TryJoinData(entry.Address, out var data2) && !favoriteServersList.Contains(data2)) { MultiBackendMatchmaking.SetServerName(data2, new ServerNameAtTimePoint(entry.Name, DateTime.UtcNow)); favoriteServersList.Add(data2); added[entry.SecretRef] = entry.Address; flag = true; } } } if (flag) { Changed(gui); } } private unsafe static void Changed(ServerListGui gui) { //IL_0019: 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) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Invalid comparison between Unknown and I4 gui.m_filteredListOutdated = true; gui.SetButtonsOutdated(); ServerListGui.UpdateServerListGuiStatic(); SaveStatusCode val = gui.m_favoriteServersList.Save(false); if ((int)val != 0 && (int)val != 1) { MeadworksLog? log = Plugin.Log; if (log != null) { log.Warning(BeaconText.Text("ui_favorite_save_failed") + " (" + ((object)(*(SaveStatusCode*)(&val))/*cast due to .constrained prefix*/).ToString() + ")"); } } } internal static bool Undo(ServerRemoval removal, ServerAddress favorite) { //IL_0086: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Unknown result type (might be due to invalid IL or missing references) if (!BeaconJoin.Active || BeaconImportSecrets.Busy || Plugin.Log == null) { return false; } try { if (!BeaconServers.Amend(delegate(ServerList candidate) { candidate.Restore(removal); }, Plugin.Log)) { return false; } suppressed.Remove(removal.Entry.Address); ServerListGui s_instance = ServerListGui.s_instance; if ((Object)(object)s_instance != (Object)null && s_instance.m_favoriteServersList != null && BeaconJoin.TryJoinData(favorite, out var data) && !s_instance.m_favoriteServersList.Contains(data)) { s_instance.m_favoriteServersList.Add(data); Changed(s_instance); } nextSync = 0f; return true; } catch (Exception ex) { Plugin.Log.Warning("Beacon favorite undo refused: " + ex.GetType().Name); return false; } } public static void Reset() { owner = null; added.Clear(); suppressed.Clear(); deferred.Clear(); reportedErrors.Clear(); nextSync = 0f; BeaconFavoriteUndo.Reset(); } } [HarmonyPatch(typeof(ServerListGui), "OnRemoveServerConfirm")] internal static class BeaconFavoritePatches { [HarmonyPrefix] private static void Before(ServerListGui __instance, out BeaconFavorites.RemovalState? __state) { __state = null; try { __state = BeaconFavorites.Capture(__instance); } catch (Exception ex) { MeadworksLog? log = Plugin.Log; if (log != null) { log.Warning("Beacon favorite removal capture: " + ex.GetType().Name); } } } [HarmonyPostfix] private static void After(ServerListGui __instance, BeaconFavorites.RemovalState? __state) { try { BeaconFavorites.Removed(__instance, __state); } catch (Exception ex) { MeadworksLog? log = Plugin.Log; if (log != null) { log.Warning("Beacon favorite removal: " + ex.GetType().Name); } } } } internal static class BeaconImportSecrets { private sealed class Pending : IDisposable { private string? secret; internal IDisposable? Operation; internal Pending(string secret) { this.secret = secret; BeaconSecrets.Redactor.Track(secret); } public void Dispose() { Operation?.Dispose(); Operation = null; BeaconSecrets.Redactor.Untrack(secret); secret = null; pending.Remove(this); } } private static readonly List<Pending> pending = new List<Pending>(); private static readonly List<string> reports = new List<string>(); internal static bool Busy => pending.Count > 0; internal static IReadOnlyList<string> Reports => reports.AsReadOnly(); internal static event Action<string>? Reported; internal static void Start(string reference, string name, string password) { if (pending.Count == 0) { reports.Clear(); } Pending item = new Pending(password); pending.Add(item); item.Operation = BeaconSecretActions.Run((CancellationToken cancel) => BeaconSecretActions.Store(reference, password, cancel), delegate(bool stored) { try { string text = BeaconSecrets.Redactor.Clean(BeaconText.Format(stored ? "password_saved" : "import_password_not_stored", name)); reports.Add(text); MeadworksLog? log = Plugin.Log; if (log != null) { log.Info(text); } BeaconImportSecrets.Reported?.Invoke(text); } finally { item.Dispose(); } }, delegate { try { string text = BeaconSecrets.Redactor.Clean(BeaconText.Format("import_password_not_stored", name)); reports.Add(text); MeadworksLog? log = Plugin.Log; if (log != null) { log.Warning(text); } BeaconImportSecrets.Reported?.Invoke(text); } finally { item.Dispose(); } }); } internal static void Reset() { Pending[] array = pending.ToArray(); for (int i = 0; i < array.Length; i++) { array[i].Dispose(); } reports.Clear(); BeaconImportSecrets.Reported = null; BeaconCommand.ResetPreview(); } } public static class BeaconServers { private static bool readOnly; public static ServerList List { get; private set; } = new ServerList((Func<string>)null); public static string DataDirectory => Path.Combine(Paths.ConfigPath, "Meadworks/beacon".Replace('/', Path.DirectorySeparatorChar)); public static string FilePath => Path.Combine(DataDirectory, "servers.json"); public static bool Writable => !readOnly; public static void Load(MeadworksLog log) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Expected O, but got Unknown readOnly = false; List = new ServerList((Func<string>)null); if (!File.Exists(FilePath)) { return; } try { List = ServerListCodec.Read(File.ReadAllText(FilePath, Encoding.UTF8)); log.Verbose("Beacon server list: " + List.Count + " servers."); } catch (Exception ex) when (ex is FormatException || ex is IOException || ex is UnauthorizedAccessException) { readOnly = true; log.Warning("Beacon could not read servers.json (" + ex.GetType().Name + "); it is left unchanged and nothing is saved this session."); } } public static bool Save(MeadworksLog log) { if (readOnly) { log.Warning("Beacon did not save servers.json: the file on disk could not be read at start."); return false; } WriteAtomically(FilePath, ServerListCodec.Write(List)); return true; } public static void Unload() { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Expected O, but got Unknown List = new ServerList((Func<string>)null); readOnly = false; } public static bool Amend(Action<ServerList> change, MeadworksLog log) { if (readOnly) { return false; } ServerList val = ServerListCodec.Read(ServerListCodec.Write(List)); change(val); try { WriteAtomically(FilePath, ServerListCodec.Write(val)); } catch (Exception ex) { log.Warning("Beacon server list was not changed: " + ex.GetType().Name); return false; } List = val; return true; } public static void WriteAtomically(string path, string text) { Directory.CreateDirectory(Path.GetDirectoryName(path)); string text2 = path + ".tmp"; File.WriteAllText(text2, text, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); if (File.Exists(path)) { File.Replace(text2, path, null); } else { File.Move(text2, path); } } } internal static class BeaconEditor { private static KitAlmanacDialog? dialog; private static KitTextField? name; private static KitTextField? address; private static KitTextField? password; private static KitAlmanacMultiline? aliases; private static IDisposable? operation; private static string? tracked; private static bool saving; private static TMP_Text? hint; private static string Ui(string key) { return BeaconPanel.Ui(key); } public static void Open(ServerEntry? entry) { //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_00ef: Unknown result type (might be due to invalid IL or missing references) //IL_00fe: Unknown result type (might be due to invalid IL or missing references) //IL_0127: Unknown result type (might be due to invalid IL or missing references) //IL_0136: Unknown result type (might be due to invalid IL or missing references) //IL_01f6: Unknown result type (might be due to invalid IL or missing references) //IL_0205: Unknown result type (might be due to invalid IL or missing references) //IL_029b: Unknown result type (might be due to invalid IL or missing references) //IL_02aa: Unknown result type (might be due to invalid IL or missing references) //IL_02f7: Unknown result type (might be due to invalid IL or missing references) //IL_0306: Unknown result type (might be due to invalid IL or missing references) Reset(); if (BeaconPanel.Window == null || !BeaconServers.Writable || BeaconImportSecrets.Busy) { return; } dialog = KitAlmanacDialog.Create(BeaconPanel.Window, Ui((entry == null) ? "add" : "edit"), BeaconPanel.Size); if (dialog == null) { return; } dialog.Dim(); ServerEntry? obj = entry; name = Field("name", 65f, ((obj != null) ? obj.Name : null) ?? ""); ServerEntry? obj2 = entry; address = Field("address", 135f, ((obj2 != null) ? ((object)obj2.Address).ToString() : null) ?? ""); KitAlmanacDialog.Label((Transform)(object)dialog.Panel, "AliasesCaption", Ui("aliases"), new Vector2(20f, -182f), new Vector2(740f, 24f), 16f); aliases = KitAlmanacMultiline.Create(dialog.Panel, "Aliases", new Vector2(20f, -205f), new Vector2(740f, 70f), (entry == null) ? "" : string.Join(", ", entry.Aliases), (Action<string>)null, false); aliases.Input.VirtualKeyboardTitle = Ui("aliases"); dialog.Window.Focusable((Selectable)(object)aliases.Input, 0, 2); BeaconPanel.Tip(((Component)aliases.Input).gameObject, "aliases"); password = Field("password", 315f, "", secret: true); hint = KitAlmanacDialog.Label((Transform)(object)dialog.Panel, "Hint", Ui("password_hint"), new Vector2(20f, -370f), new Vector2(740f, 62f), 14f); name.Validator = (string value) => !string.IsNullOrWhiteSpace(value); ServerAddress val2 = default(ServerAddress); address.Validator = (string value) => ServerAddress.TryParse(value, ref val2); Button save = KitButton.Create(dialog.Panel, "Save", Ui("save"), (Action)delegate { Save(entry); }, new Vector2(20f, -550f), new Vector2(350f, 38f)); Button val = KitButton.Create(dialog.Panel, "Cancel", Ui("cancel"), (Action)Reset, new Vector2(410f, -550f), new Vector2(350f, 38f)); BeaconPanel.Tip(((Component)save).gameObject, "save"); BeaconPanel.Tip(((Component)val).gameObject, "cancel"); dialog.Window.Focusable((Selectable)(object)save, 0, 4); dialog.Window.Focusable((Selectable)(object)val, 1, 4); dialog.Tick += delegate { bool flag = !saving && !BeaconImportSecrets.Busy; KitTextField? obj3 = name; if (obj3 != null) { obj3.SetUsable(flag); } KitTextField? obj4 = address; if (obj4 != null) { obj4.SetUsable(flag); } KitAlmanacMultiline? obj5 = aliases; if (obj5 != null) { obj5.SetUsable(flag); } KitTextField? obj6 = password; if (obj6 != null) { obj6.SetUsable(flag); } if ((Object)(object)save != (Object)null) { ((Selectable)save).interactable = flag; } }; dialog.Closed += Reset; dialog.Own((Action)delegate { KitTextField? obj3 = name; if (obj3 != null) { obj3.Destroy(); } KitTextField? obj4 = address; if (obj4 != null) { obj4.Destroy(); } KitAlmanacMultiline? obj5 = aliases; if (obj5 != null) { obj5.Dispose(); } KitTextField? obj6 = password; if (obj6 != null) { obj6.Destroy(); } }); dialog.TypingField = delegate { KitTextField? obj3 = name; if (obj3 == null || !obj3.Focused) { KitTextField? obj4 = address; if (obj4 == null || !obj4.Focused) { KitAlmanacMultiline? obj5 = aliases; if (obj5 == null || !obj5.Focused) { KitTextField? obj6 = password; if (obj6 == null) { return false; } return obj6.Focused; } } } return true; }; } private static KitTextField Field(string key, float y, string value, bool secret = false) { //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) KitAlmanacDialog.Label((Transform)(object)dialog.Panel, key + "Caption", Ui(key), new Vector2(20f, 0f - y + 23f), new Vector2(740f, 24f), 16f); KitTextField val = KitTextField.Create(dialog.Panel, key, (KitFieldMode)(secret ? 3 : 2), new Vector2(20f, 0f - y), new Vector2(740f, 36f), (string)null, Ui(key)); val.Text = value; BeaconPanel.Tip(((Component)val.Input).gameObject, key, (key == "password") ? Ui("password_hint") : null); dialog.Window.Focusable((Selectable)(object)val.Input, 0, (int)((y - 65f) / 70f)); return val; } private static void Save(ServerEntry? previous) { if (saving || BeaconImportSecrets.Busy || dialog == null || name == null || address == null || aliases == null || password == null) { return; } string nextName = ((previous != null && name.Text == KitFieldRules.Clip((KitFieldMode)2, previous.Name)) ? previous.Name : name.Text.Trim()); ServerAddress parsed; ServerAddress[] others; try { parsed = ((previous != null && address.Text == KitFieldRules.Clip((KitFieldMode)2, ((object)previous.Address).ToString())) ? previous.Address : ServerAddress.Parse(address.Text)); others = ((IEnumerable<string>)aliases.Text.Split(new char[4] { ',', ';', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)).Select((Func<string, ServerAddress>)ServerAddress.Parse).ToArray(); } catch { hint.text = Ui("invalid"); return; } if (!BeaconPanel.Amend(delegate(ServerList candidate) { if (previous == null) { candidate.Add(nextName, parsed, (IEnumerable<ServerAddress>)others); } else { candidate.Edit(previous.Name, nextName, parsed, (IEnumerable<ServerAddress>)others); } })) { hint.text = Ui("invalid"); return; } ServerEntry entry = BeaconServers.List.FindByName(nextName); string secret = password.Text; if (secret.Length == 0) { Reset(); return; } tracked = secret; BeaconSecrets.Redactor.Track(secret); saving = true; name.SetUsable(false); address.SetUsable(false); aliases.SetUsable(false); password.SetUsable(false); operation = BeaconSecretActions.Run((CancellationToken cancel) => BeaconSecretActions.Store(entry.SecretRef, secret, cancel), delegate(bool saved) { BeaconPanel.Message(saved ? BeaconText.Format("password_saved", entry.Name) : BeaconText.Text("secret_store_failed")); Reset(); }, delegate { BeaconPanel.Message(BeaconText.Text("secret_store_failed")); Reset(); }); } public static void Reset() { operation?.Dispose(); operation = null; BeaconSecrets.Redactor.Untrack(tracked); tracked = null; saving = false; KitAlmanacDialog val = dialog; dialog = null; if (val != null) { val.Closed -= Reset; val.Dispose(); } name = (address = (password = null)); aliases = null; hint = null; } } internal static class BeaconFavoriteUndo { private static KitWindow? window; private static ServerRemoval? removal; private static ServerAddress? favorite; private static TMP_Text? text; private static Button? undo; private static float expires; public static void Show(ServerRemoval removed, ServerAddress address) { Reset(); removal = removed; favorite = address; Create(BeaconText.Format("favorite_removed", removed.Entry.Name)); } public static void Failure(string message) { Reset(); Create(message); } private static void Create(string message) { //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Unknown result type (might be due to invalid IL or missing references) //IL_00a2: Unknown result type (might be due to invalid IL or missing references) //IL_00a3: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_00e7: Unknown result type (might be due to invalid IL or missing references) //IL_00f6: Unknown result type (might be due to invalid IL or missing references) //IL_01d4: Unknown result type (might be due to invalid IL or missing references) //IL_01e3: Unknown result type (might be due to invalid IL or missing references) //IL_0152: Unknown result type (might be due to invalid IL or missing references) //IL_0161: Unknown result type (might be due to invalid IL or missing references) FejdStartup instance = FejdStartup.instance; object obj; if (instance == null) { obj = null; } else { GameObject mainMenu = instance.m_mainMenu; if (mainMenu == null) { obj = null; } else { Canvas componentInParent = mainMenu.GetComponentInParent<Canvas>(); obj = ((componentInParent != null) ? componentInParent.rootCanvas : null); } } Canvas val = (Canvas)obj; if ((Object)(object)val == (Object)null) { Reset(); return; } window = KitWindow.Create("Beacon_FavoriteUndo", new Vector2(540f, 150f), ((Component)val).transform, false, false); if (window == null) { Reset(); return; } RectTransform panel = window.Panel; RectTransform panel2 = window.Panel; RectTransform panel3 = window.Panel; Vector2 val2 = default(Vector2); ((Vector2)(ref val2))..ctor(1f, 0f); panel3.pivot = val2; Vector2 anchorMin = (panel2.anchorMax = val2); panel.anchorMin = anchorMin; window.Panel.anchoredPosition = new Vector2(-20f, 20f); text = KitAlmanacDialog.Label((Transform)(object)window.Panel, "Message", message, new Vector2(20f, -12f), new Vector2(500f, 64f), 16f); if (removal != null) { undo = KitButton.Create(window.Panel, "Undo", BeaconPanel.Ui("undo"), (Action)Undo, new Vector2(20f, -96f), new Vector2(240f, 36f)); window.Focusable((Selectable)(object)undo, 0, 0); BeaconPanel.Tip(((Component)undo).gameObject, "undo"); } Button val4 = KitButton.Create(window.Panel, "Dismiss", BeaconPanel.Ui("close"), (Action)Reset, new Vector2(280f, -96f), new Vector2(240f, 36f)); BeaconPanel.Tip(((Component)val4).gameObject, "close"); window.Focusable((Selectable)(object)val4, 1, 0); window.Closed += Reset; window.Open(); expires = Time.unscaledTime + 15f; } private static void Undo() { if (removal != null && !(favorite == (ServerAddress)null)) { if (BeaconFavorites.Undo(removal, favorite)) { Reset(); } else if ((Object)(object)text != (Object)null) { text.text = BeaconPanel.Ui("undo_conflict"); } } } public static void Tick() { if (window != null) { if ((Object)(object)Game.instance != (Object)null || (Object)(object)FejdStartup.instance == (Object)null || Time.unscaledTime >= expires) { Reset(); } else if ((Object)(object)undo != (Object)null) { ((Selectable)undo).interactable = !BeaconImportSecrets.Busy; } } } public static void Reset() { KitWindow val = window; window = null; removal = null; favorite = null; text = null; undo = null; if (val != null) { val.Closed -= Reset; val.Close(); val.Destroy(); } } } internal static class BeaconMenuBack { private static readonly KeyboardShortcut EscapeKey = new KeyboardShortcut((KeyCode)27, Array.Empty<KeyCode>()); private static bool typedLastFrame; public static void Tick() { //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_00f2: Unknown result type (might be due to invalid IL or missing references) //IL_00f4: Unknown result type (might be due to invalid IL or missing references) //IL_00f8: Unknown result type (might be due to invalid IL or missing references) //IL_0106: Unknown result type (might be due to invalid IL or missing references) //IL_0107: Unknown result type (might be due to invalid IL or missing references) //IL_010c: Unknown result type (might be due to invalid IL or missing references) //IL_010e: Unknown result type (might be due to invalid IL or missing references) //IL_0111: Unknown result type (might be due to invalid IL or missing references) //IL_0127: Expected I4, but got Unknown if (!Plugin.Ready || (Object)(object)Game.instance != (Object)null || (Object)(object)FejdStartup.instance == (Object)null || !MeadworksHeadless.AllowsInput) { typedLastFrame = false; return; } bool flag = MeadworksInput.Typing || KitTextField.Typing; KitWindow? window = BeaconPanel.Window; bool flag2 = window != null && window.IsOpen; KitAlmanacDialog val = (flag2 ? KitAlmanacDialog.Active : null); MenuBackState val2 = new MenuBackState { Escape = (!flag && MeadworksInput.Pressed(EscapeKey, (InputContext)2)), Typing = flag, TypedLastFrame = typedLastFrame, DialogOpen = (val != null), DialogTyping = (val != null && val.TypingField?.Invoke() == true), SaveOfferOpen = BeaconSaveOffer.Showing, PanelOpen = flag2, WaitPending = BeaconAutoJoin.Waiting }; typedLastFrame = flag || val2.DialogTyping; MenuBackAction val3 = MenuBack.Decide(val2); switch (val3 - 1) { case 0: val.Dispose(); break; case 1: BeaconSaveOffer.Reset(); break; case 2: BeaconPanel.Reset(); break; case 3: BeaconAutoJoin.Cancel(Plugin.Log); break; } } } internal static class BeaconPanel { private static KitWindow? window; private static KitVirtualList? list; private static TMP_Text? status; private static TMP_Text? storage; private static string? selected; private static bool refreshNow = true; private static ServerList? seenList; private static int seenRevision = -1; private static SecretBackend seenBackend; private static string seenLanguage = ""; private static int seenSecrets = int.MinValue; private static long revision; private static IDisposable? refresh; private static float nextRefresh; private static string title = ""; private static GameObject? hiddenMenu; private static readonly List<Button> selectedActions = new List<Button>(); private static readonly List<Button> busyActions = new List<Button>(); internal static readonly Vector2 Size = new Vector2(780f, 610f); internal static KitWindow? Window => window; internal static ServerEntry? Selected => ((IEnumerable<ServerEntry>)BeaconServers.List.Entries).FirstOrDefault((Func<ServerEntry, bool>)((ServerEntry e) => e.SecretRef == selected)); internal static string Ui(string key) { return BeaconText.Text("ui_" + key); } public static void Tick() { //IL_0116: Unknown result type (might be due to invalid IL or missing references) //IL_01