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 Almanac v0.1.24
plugins/Almanac/Almanac.Core.dll
Decompiled 16 hours agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using System.Text.RegularExpressions; using ExtraPotions.Almanac.Attributes; using ExtraPotions.Almanac.Editors; using ExtraPotions.Almanac.Index; using ExtraPotions.Almanac.Values; 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.24.0")] [assembly: AssemblyInformationalVersion("0.1.24+fatal: not a git repository (or any of the parent directories): .git")] [assembly: AssemblyProduct("Almanac.Core")] [assembly: AssemblyTitle("Almanac.Core")] [assembly: AssemblyVersion("0.1.24.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.Almanac { public static class AlmanacCore { public const string Guid = "com.meadworks.almanac"; public const string Name = "Almanac"; public const string Version = "0.1.24"; public const string Family = "Runestone"; public const string DataFolder = "Meadworks/almanac"; } } namespace ExtraPotions.Almanac.Writing { public enum WriteVerdict { Write, NoChange, RefusedInvalid, RefusedLocked } public enum SaveAction { None, AutoSaveByFile, MarkDirty, SaveSuppressed, OwnedBySyncSystem, SaveEntry } public enum HandlerOutcome { Keep, RestorePrevious, MarkRejected } public sealed class WriteRequest { public bool Editable { get; set; } public string? LockReason { get; set; } public bool ValueValid { get; set; } public string? Error { get; set; } public bool SameAsCurrent { get; set; } public bool ConnectedAsClient { get; set; } public bool FileHasSyncedEntries { get; set; } public bool EntrySynced { get; set; } public bool SaveOnConfigSet { get; set; } public bool PersistOwnValue { get; set; } } public sealed class WriteDecision { public WriteVerdict Verdict { get; } public SaveAction Save { get; } public string? Reason { get; } internal WriteDecision(WriteVerdict verdict, SaveAction save, string? reason) { Verdict = verdict; Save = save; Reason = reason; } public override string ToString() { return Verdict.ToString() + " / " + Save.ToString() + ((Reason == null) ? "" : (" (" + Reason + ")")); } } public static class EntryWritePolicy { public static WriteDecision Decide(WriteRequest request) { if (!request.ValueValid) { return new WriteDecision(WriteVerdict.RefusedInvalid, SaveAction.None, request.Error ?? "Invalid value"); } if (!request.Editable) { return new WriteDecision(WriteVerdict.RefusedLocked, SaveAction.None, request.LockReason ?? "Locked"); } if (request.SameAsCurrent) { return new WriteDecision(WriteVerdict.NoChange, SaveAction.None, null); } SaveAction save = ((request.ConnectedAsClient && request.EntrySynced) ? SaveAction.OwnedBySyncSystem : (request.PersistOwnValue ? SaveAction.SaveEntry : (request.SaveOnConfigSet ? SaveAction.AutoSaveByFile : (MaySaveFile(request.ConnectedAsClient, request.FileHasSyncedEntries) ? SaveAction.MarkDirty : SaveAction.SaveSuppressed)))); return new WriteDecision(WriteVerdict.Write, save, null); } public static bool MaySaveFile(bool connectedAsClient, bool fileHasSyncedEntries) { return !(connectedAsClient && fileHasSyncedEntries); } public static bool MayReload(bool connectedAsClient, bool fileHasSyncedEntries) { return MaySaveFile(connectedAsClient, fileHasSyncedEntries); } public static bool MayCaptureForProfile(bool connectedAsClient, bool entrySynced) { return !(connectedAsClient && entrySynced); } public static HandlerOutcome AfterAssign(bool handlerThrew, bool restoreAlreadyTried) { if (!handlerThrew) { return HandlerOutcome.Keep; } if (!restoreAlreadyTried) { return HandlerOutcome.RestorePrevious; } return HandlerOutcome.MarkRejected; } } public static class LockReasonPolicy { public static string ForReadOnlyTag(bool entrySynced, bool known, bool editable, string? factsReason, string tagText) { if (!(entrySynced && known) || editable || string.IsNullOrEmpty(factsReason)) { return tagText; } return factsReason; } } public interface IFileOps { bool Exists(string path); string ReadAllText(string path); void WriteAllText(string path, string text); void Copy(string source, string destination); void Delete(string path); } public enum SafeWriteStep { Backup, Write, Verify, Rollback } public sealed class SafeWriteResult { public bool Success { get; } public bool RolledBack { get; } public bool RollbackFailed { get; } public string? BackupPath { get; } public IReadOnlyList<SafeWriteStep> Steps { get; } public string? Error { get; } internal SafeWriteResult(bool success, bool rolledBack, bool rollbackFailed, string? backupPath, IReadOnlyList<SafeWriteStep> steps, string? error) { Success = success; RolledBack = rolledBack; RollbackFailed = rollbackFailed; BackupPath = backupPath; Steps = steps; Error = error; } } public static class SafeFileWriter { public static SafeWriteResult Write(IFileOps ops, string path, string text, string backupPath) { List<SafeWriteStep> list = new List<SafeWriteStep>(); bool flag = ops.Exists(path); string backupPath2 = null; if (flag) { list.Add(SafeWriteStep.Backup); try { ops.Copy(path, backupPath); backupPath2 = backupPath; } catch (Exception ex) { return new SafeWriteResult(success: false, rolledBack: false, rollbackFailed: false, null, list, "Backup failed: " + ex.Message); } } string text2 = null; list.Add(SafeWriteStep.Write); try { ops.WriteAllText(path, text); } catch (Exception ex2) { text2 = "Write failed: " + ex2.Message; } if (text2 == null) { list.Add(SafeWriteStep.Verify); try { if (!string.Equals(ops.ReadAllText(path), text, StringComparison.Ordinal)) { text2 = "Verify failed: the file does not match what was written."; } } catch (Exception ex3) { text2 = "Verify failed: " + ex3.Message; } } if (text2 == null) { return new SafeWriteResult(success: true, rolledBack: false, rollbackFailed: false, backupPath2, list, null); } list.Add(SafeWriteStep.Rollback); try { if (flag) { ops.Copy(backupPath, path); } else if (ops.Exists(path)) { ops.Delete(path); } return new SafeWriteResult(success: false, rolledBack: true, rollbackFailed: false, backupPath2, list, text2); } catch (Exception ex4) { return new SafeWriteResult(success: false, rolledBack: false, rollbackFailed: true, backupPath2, list, text2 + " Rollback failed: " + ex4.Message); } } } public static class BackupNaming { public const string StampFormat = "yyyyMMdd-HHmmss"; public static string Name(string filePath, DateTime utc) { string text = ConfigPaths.FileName(filePath); int num = text.LastIndexOf('.'); return string.Concat((num > 0) ? text.Substring(0, num) : text, str3: (num > 0) ? text.Substring(num) : "", str1: ".", str2: utc.ToUniversalTime().ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture)); } public static IReadOnlyList<string> ToPrune(string fileName, IEnumerable<string> existing, int keep) { int num = fileName.LastIndexOf('.'); string str = ((num > 0) ? fileName.Substring(0, num) : fileName); string str2 = ((num > 0) ? fileName.Substring(num) : ""); Regex pattern = new Regex("^" + Regex.Escape(str) + "\\.(\\d{8}-\\d{6})" + Regex.Escape(str2) + "$", RegexOptions.CultureInvariant); return (from x in (from n in existing select (Name: n, Match: pattern.Match(n)) into x where x.Match.Success select x).OrderByDescending<(string, Match), string>(((string Name, Match Match) x) => x.Match.Groups[1].Value, StringComparer.Ordinal) select x.Name).ToList().Skip(Math.Max(0, keep)).Reverse() .ToList(); } } } namespace ExtraPotions.Almanac.World { public sealed class WorldEditWatch { private readonly long world; private readonly long generation; private readonly string? requested; public bool Pending { get; private set; } public bool Stale { get; private set; } public WorldEditWatch(long world, long generation, string? requested, bool applied) { this.world = world; this.generation = generation; this.requested = requested; Pending = !applied; } public void Observe(long currentWorld, long currentGeneration, string? nativeWorldText, bool readable) { if (Pending) { if (currentWorld != world || currentGeneration != generation) { Pending = false; Stale = true; } else if (readable && string.Equals(requested, nativeWorldText, StringComparison.Ordinal)) { Pending = false; } } } } } namespace ExtraPotions.Almanac.Values { public static class ValueCodec { private static readonly CultureInfo Inv = CultureInfo.InvariantCulture; public static ParseResult Parse(SettingShape shape, string? text) { ParseResult parseResult = ParseKind(shape, text ?? ""); if (!parseResult.Ok) { return parseResult; } if (shape.HasRange && (shape.Kind == SettingKind.Integer || shape.Kind == SettingKind.Floating)) { parseResult = Clamp(shape, parseResult); } if (shape.AcceptableList != null) { parseResult = CheckList(shape, parseResult); } return parseResult; } private static ParseResult ParseKind(SettingShape shape, string text) { switch (shape.Kind) { case SettingKind.Bool: { if (!bool.TryParse(text.Trim(), out var result)) { return ParseResult.Fail("Expected true or false."); } return ParseResult.Success(result ? "true" : "false", result); } case SettingKind.Integer: return Integer(shape, text); case SettingKind.Floating: return Floating(shape, text); case SettingKind.String: { string text2 = BepInExText.Unescape(text); return ParseResult.Success(BepInExText.Escape(text2), text2); } case SettingKind.Enum: return Enum(shape, text); case SettingKind.KeyCode: { if (!KeyNames.TryCanonical(text.Trim(), shape.KeyNames, out string canonical, out string error2)) { return ParseResult.Fail(error2); } return ParseResult.Success(canonical, canonical); } case SettingKind.KeyboardShortcut: { if (!ShortcutValue.TryParse(text, shape.KeyNames, out ShortcutValue value, out string error)) { return ParseResult.Fail(error); } return ParseResult.Success(value.ToString(), value); } case SettingKind.Color: { if (!ColorValue.TryParse(text, out var color)) { return ParseResult.Fail("Expected a color as RRGGBBAA hex."); } return ParseResult.Success(color.ToHex(), color); } case SettingKind.Vector2: return Vector(text, "xy"); case SettingKind.Vector3: return Vector(text, "xyz"); case SettingKind.Vector4: case SettingKind.Quaternion: return Vector(text, "xyzw"); default: return ParseResult.ConverterNeeded(); } } private static ParseResult Integer(SettingShape shape, string text) { if (!decimal.TryParse(text.Trim(), NumberStyles.AllowLeadingSign, Inv, out var result)) { return ParseResult.Fail("Expected a whole number."); } if (!IntegerBounds(shape.ShortTypeName, out var min, out var max)) { min = -9223372036854775808m; max = 18446744073709551615m; } if (result < min || result > max) { return ParseResult.Fail("Expected a whole number from " + min.ToString(Inv) + " to " + max.ToString(Inv) + "."); } return ParseResult.Success(result.ToString(Inv), result); } private static bool IntegerBounds(string type, out decimal min, out decimal max) { switch (type) { case "SByte": min = -128m; max = 127m; return true; case "Byte": min = default(decimal); max = 255m; return true; case "Int16": min = -32768m; max = 32767m; return true; case "UInt16": min = default(decimal); max = 65535m; return true; case "Int32": min = -2147483648m; max = 2147483647m; return true; case "UInt32": min = default(decimal); max = 4294967295m; return true; case "Int64": min = -9223372036854775808m; max = 9223372036854775807m; return true; case "UInt64": min = default(decimal); max = 18446744073709551615m; return true; default: min = default(decimal); max = default(decimal); return false; } } private static ParseResult Floating(SettingShape shape, string text) { string s = text.Trim(); if (shape.ShortTypeName == "Decimal") { if (!decimal.TryParse(s, NumberStyles.Float, Inv, out var result)) { return ParseResult.Fail("Expected a number such as 1.5."); } return ParseResult.Success(result.ToString(Inv), result); } if (!double.TryParse(s, NumberStyles.Float, Inv, out var result2) || double.IsNaN(result2) || double.IsInfinity(result2)) { return ParseResult.Fail("Expected a number such as 1.5."); } if (shape.ShortTypeName == "Single") { if (Math.Abs(result2) > 3.4028234663852886E+38) { return ParseResult.Fail("The number is too large."); } float num = (float)result2; return ParseResult.Success(num.ToString(Inv), (double)num); } return ParseResult.Success(result2.ToString("R", Inv), result2); } private static ParseResult Clamp(SettingShape shape, ParseResult parsed) { object value = parsed.Value; if (!(value is decimal num)) { if (value is double num2) { if (num2 >= shape.RangeMin && num2 <= shape.RangeMax) { return parsed; } double num3 = ((num2 < shape.RangeMin) ? shape.RangeMin : shape.RangeMax); return ParseResult.Success((shape.ShortTypeName == "Single") ? ((float)num3).ToString(Inv) : num3.ToString("R", Inv), num3, clamped: true); } return parsed; } decimal num4 = (decimal)shape.RangeMin; decimal num5 = (decimal)shape.RangeMax; if (num >= num4 && num <= num5) { return parsed; } decimal num6 = ((num < num4) ? num4 : num5); return ParseResult.Success((shape.Kind == SettingKind.Integer) ? decimal.Truncate(num6).ToString(Inv) : num6.ToString(Inv), num6, clamped: true); } private static ParseResult CheckList(SettingShape shape, ParseResult parsed) { foreach (string acceptable in shape.AcceptableList) { ParseResult parseResult = ParseKind(shape, acceptable); if (string.Equals(parseResult.Ok ? parseResult.Text : acceptable, parsed.Text, StringComparison.Ordinal)) { return parsed; } } return ParseResult.Fail("Allowed values: " + string.Join(", ", shape.AcceptableList) + "."); } private static ParseResult Enum(SettingShape shape, string text) { IReadOnlyList<string> enumNames = shape.EnumNames; if (!shape.IsFlags) { string text2 = enumNames.FirstOrDefault((string n) => string.Equals(n, text.Trim(), StringComparison.OrdinalIgnoreCase)); if (text2 == null) { return ParseResult.Fail("Allowed values: " + string.Join(", ", enumNames) + "."); } return ParseResult.Success(text2, text2); } HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); string[] array = text.Split(','); foreach (string part in array) { string text3 = enumNames.FirstOrDefault((string n) => string.Equals(n, part.Trim(), StringComparison.OrdinalIgnoreCase)); if (text3 == null) { return ParseResult.Fail("Unknown flag '" + part.Trim() + "'. Allowed: " + string.Join(", ", enumNames) + "."); } hashSet.Add(text3); } if (hashSet.Count > 1) { hashSet.Remove("None"); } string[] value = enumNames.Where(hashSet.Contains).ToArray(); return ParseResult.Success(string.Join(", ", value), value); } private static ParseResult Vector(string text, string axes) { string text2 = text.Trim(); if (text2.Length < 2 || text2[0] != '{' || text2[text2.Length - 1] != '}') { return ParseResult.Fail("Expected {\"x\":1.0,...}."); } text2 = text2.Substring(1, text2.Length - 2); Dictionary<char, float> values = new Dictionary<char, float>(); string[] array = text2.Split(','); foreach (string text3 in array) { int num = text3.IndexOf(':'); if (num < 0) { return ParseResult.Fail("Expected \"axis\":number pairs."); } string text4 = text3.Substring(0, num).Trim(); if (text4.Length != 3 || text4[0] != '"' || text4[2] != '"' || axes.IndexOf(text4[1]) < 0 || values.ContainsKey(text4[1])) { return ParseResult.Fail("Unexpected component " + text4 + "."); } if (!float.TryParse(text3.Substring(num + 1).Trim(), NumberStyles.Float, Inv, out var result) || float.IsNaN(result) || float.IsInfinity(result)) { return ParseResult.Fail("Component " + text4 + " is not a number."); } values[text4[1]] = result; } if (values.Count != axes.Length) { return ParseResult.Fail("Expected components " + string.Join(", ", axes.ToCharArray()) + "."); } float[] ordered = axes.Select((char a) => values[a]).ToArray(); return ParseResult.Success("{" + string.Join(",", axes.Select((char a, int num2) => "\"" + a + "\":" + JsonFloat(ordered[num2]))) + "}", ordered); } private static string JsonFloat(float value) { string text = value.ToString("R", Inv); if (text.IndexOfAny(new char[3] { '.', 'E', 'e' }) < 0) { return text + ".0"; } return text; } } public sealed class ParseResult { public bool Ok { get; private set; } public string Text { get; private set; } = ""; public object? Value { get; private set; } public string? Error { get; private set; } public bool WasClamped { get; private set; } public bool NeedsModConverter { get; private set; } private ParseResult() { } internal static ParseResult Success(string text, object? value, bool clamped = false) { return new ParseResult { Ok = true, Text = text, Value = value, WasClamped = clamped }; } internal static ParseResult Fail(string error) { return new ParseResult { Error = error }; } internal static ParseResult ConverterNeeded() { return new ParseResult { Error = "This type is converted by the mod's own converter.", NeedsModConverter = true }; } public override string ToString() { if (!Ok) { return "Error " + Error; } return "Ok " + Text; } } public readonly struct ColorValue : IEquatable<ColorValue> { public byte R { get; } public byte G { get; } public byte B { get; } public byte A { get; } public ColorValue(byte r, byte g, byte b, byte a) { R = r; G = g; B = b; A = a; } public string ToHex() { return R.ToString("X2", CultureInfo.InvariantCulture) + G.ToString("X2", CultureInfo.InvariantCulture) + B.ToString("X2", CultureInfo.InvariantCulture) + A.ToString("X2", CultureInfo.InvariantCulture); } public static bool TryParse(string? text, out ColorValue color) { color = default(ColorValue); string text2 = (text ?? "").Trim(); if (text2.StartsWith("#", StringComparison.Ordinal)) { text2 = text2.Substring(1); } if (text2.Length != 6 && text2.Length != 8) { return false; } byte[] array = new byte[4] { 0, 0, 0, 255 }; for (int i = 0; i < text2.Length / 2; i++) { if (!byte.TryParse(text2.Substring(i * 2, 2), NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out array[i])) { return false; } } color = new ColorValue(array[0], array[1], array[2], array[3]); return true; } public bool Equals(ColorValue other) { if (R == other.R && G == other.G && B == other.B) { return A == other.A; } return false; } public override bool Equals(object? obj) { if (obj is ColorValue other) { return Equals(other); } return false; } public override int GetHashCode() { return (R << 24) | (G << 16) | (B << 8) | A; } public override string ToString() { return ToHex(); } } public sealed class ShortcutValue : IEquatable<ShortcutValue> { public static readonly ShortcutValue Empty = new ShortcutValue("", new string[0]); public string MainKey { get; } public IReadOnlyList<string> Modifiers { get; } public bool IsEmpty => MainKey.Length == 0; private ShortcutValue(string mainKey, IReadOnlyList<string> modifiers) { MainKey = mainKey; Modifiers = modifiers; } public static ShortcutValue Parse(string? text, IReadOnlyList<string>? knownKeys = null) { if (!TryParse(text, knownKeys, out ShortcutValue value, out string error)) { throw new FormatException(error); } return value; } public static bool TryParse(string? text, IReadOnlyList<string>? knownKeys, out ShortcutValue value, out string error) { value = Empty; error = ""; string text2 = (text ?? "").Trim(); if (text2.Length == 0 || string.Equals(text2, "None", StringComparison.OrdinalIgnoreCase)) { return true; } List<string> list = new List<string>(); string[] array = text2.Split('+'); foreach (string obj in array) { if (!KeyNames.TryCanonical(obj.Trim(), knownKeys, out string canonical, out error)) { return false; } if (!string.Equals(canonical, "None", StringComparison.OrdinalIgnoreCase) && !list.Any((string k) => string.Equals(k, canonical, StringComparison.OrdinalIgnoreCase))) { list.Add(canonical); } } if (list.Count == 0) { return true; } value = new ShortcutValue(list[0], list.Skip(1).ToArray()); return true; } public override string ToString() { if (!IsEmpty) { return string.Join(" + ", new string[1] { MainKey }.Concat(Modifiers)); } return "None"; } public bool Equals(ShortcutValue? other) { if (other == null) { return false; } if (!string.Equals(MainKey, other.MainKey, StringComparison.OrdinalIgnoreCase) || Modifiers.Count != other.Modifiers.Count) { return false; } return Modifiers.All((string m) => other.Modifiers.Any((string o) => string.Equals(m, o, StringComparison.OrdinalIgnoreCase))); } public override bool Equals(object? obj) { return Equals(obj as ShortcutValue); } public override int GetHashCode() { return StringComparer.OrdinalIgnoreCase.GetHashCode(MainKey) ^ Modifiers.Count; } } public static class KeyNames { public static bool TryCanonical(string name, IReadOnlyList<string>? knownKeys, out string canonical, out string error) { canonical = ""; error = ""; if (name.Length == 0 || !name.All((char c) => char.IsLetterOrDigit(c) || c == '_')) { error = "Not a key name: '" + name + "'"; return false; } if (knownKeys != null && knownKeys.Count > 0) { string text = knownKeys.FirstOrDefault((string k) => string.Equals(k, name, StringComparison.OrdinalIgnoreCase)); if (text == null) { error = "Unknown key: '" + name + "'"; return false; } canonical = text; return true; } canonical = name; return true; } } public static class BepInExText { public static string Escape(string? text) { if (string.IsNullOrEmpty(text)) { return ""; } StringBuilder stringBuilder = new StringBuilder(text.Length + 2); foreach (char c in text) { switch (c) { case '\0': stringBuilder.Append("\\0"); break; case '\a': stringBuilder.Append("\\a"); break; case '\b': stringBuilder.Append("\\b"); break; case '\t': stringBuilder.Append("\\t"); break; case '\n': stringBuilder.Append("\\n"); break; case '\v': stringBuilder.Append("\\v"); break; case '\f': stringBuilder.Append("\\f"); break; case '\r': stringBuilder.Append("\\r"); break; case '\'': stringBuilder.Append("\\'"); break; case '\\': stringBuilder.Append("\\\\"); break; case '"': stringBuilder.Append("\\\""); break; default: stringBuilder.Append(c); break; } } return stringBuilder.ToString(); } public static string Unescape(string? text) { if (string.IsNullOrEmpty(text)) { return ""; } if (text.IndexOf('\\') < 0) { return text; } StringBuilder stringBuilder = new StringBuilder(text.Length); for (int i = 0; i < text.Length; i++) { char c = text[i]; if (c != '\\' || i == text.Length - 1) { stringBuilder.Append(c); continue; } char c2 = text[++i]; switch (c2) { case '0': stringBuilder.Append('\0'); break; case 'a': stringBuilder.Append('\a'); break; case 'b': stringBuilder.Append('\b'); break; case 't': stringBuilder.Append('\t'); break; case 'n': stringBuilder.Append('\n'); break; case 'v': stringBuilder.Append('\v'); break; case 'f': stringBuilder.Append('\f'); break; case 'r': stringBuilder.Append('\r'); break; case '\'': stringBuilder.Append('\''); break; case '\\': stringBuilder.Append('\\'); break; case '"': stringBuilder.Append('"'); break; default: stringBuilder.Append('\\').Append(c2); break; } } return stringBuilder.ToString(); } } public static class ListText { public static IReadOnlyList<string> Split(string? text, string separator) { if (string.IsNullOrEmpty(text)) { return new string[0]; } if (string.IsNullOrEmpty(separator)) { return new string[1] { text.Trim() }; } return (from s in text.Split(new string[1] { separator }, StringSplitOptions.None) select s.Trim()).ToArray(); } public static string Join(IEnumerable<string> items, string separator) { return string.Join(separator ?? "", items); } } } namespace ExtraPotions.Almanac.Localization { public static class TextKey { public const int MaximumLength = 60; private const int HashLength = 8; public static string For(string english) { StringBuilder stringBuilder = new StringBuilder(english.Length); bool flag = false; for (int i = 0; i < english.Length; i++) { char c = char.ToLowerInvariant(english[i]); if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) { if (flag && stringBuilder.Length > 0) { stringBuilder.Append('_'); } stringBuilder.Append(c); flag = false; } else { flag = true; } } string text = ((stringBuilder.Length == 0) ? "t" : stringBuilder.ToString()); if (text[0] >= '0' && text[0] <= '9') { text = "t_" + text; } if (text.Length <= 60) { return text; } return text.Substring(0, 51).TrimEnd('_') + "_" + Fnv(english).ToString("x8"); } private static uint Fnv(string value) { uint num = 2166136261u; foreach (char c in value) { num ^= c; num *= 16777619; } return num; } } } namespace ExtraPotions.Almanac.Index { public sealed class IndexedFile { private readonly List<ConfigEntryInfo> entries = new List<ConfigEntryInfo>(); private readonly HashSet<EntryKey> entryKeys = new HashSet<EntryKey>(); private readonly List<string> sections = new List<string>(); private readonly List<EntryKey> orphans = new List<EntryKey>(); private readonly HashSet<DiscoveryRoute> routes = new HashSet<DiscoveryRoute>(); private int ownerRank; public string Path { get; private set; } public PluginIdentity? Owner { get; private set; } public DataFileRegistration? Registration { get; private set; } public IReadOnlyCollection<DiscoveryRoute> Routes => routes; public IReadOnlyList<ConfigEntryInfo> Entries => entries; public IReadOnlyList<string> Sections => sections; public IReadOnlyList<EntryKey> OrphanedKeys => orphans; public FileKind Kind { get { if (routes.Contains(DiscoveryRoute.CoreConfig)) { return FileKind.CoreConfig; } if (IsLive) { return FileKind.BoundConfig; } if (Registration != null) { return FileKind.DataFile; } return FileKind.UnboundConfig; } } public bool EntriesEditable { get { if (Kind != FileKind.BoundConfig) { return Kind == FileKind.CoreConfig; } return true; } } public bool TextEditable => Kind == FileKind.DataFile; public bool AllEntriesAdvanced => Kind == FileKind.CoreConfig; public bool AllEntriesRequireRestart => Kind == FileKind.CoreConfig; public string GroupKey => Kind switch { FileKind.CoreConfig => "bepinex:core", FileKind.UnboundConfig => "disk:unbound", FileKind.DataFile => Registration.Product.Guid, _ => Owner?.Guid ?? ("unknown:" + ConfigPaths.FileName(Path)), }; public string DisplayName { get { if (Kind != FileKind.DataFile) { return ConfigPaths.FileName(Path); } return Registration.DisplayName; } } internal bool IsLive { get { if (!routes.Contains(DiscoveryRoute.PluginInstance) && !routes.Contains(DiscoveryRoute.ConstructorHook)) { return routes.Contains(DiscoveryRoute.FieldScan); } return true; } } internal IndexedFile(string path) { Path = path; } internal void AddRoute(DiscoveryRoute route, string path) { routes.Add(route); if (route == DiscoveryRoute.PluginInstance) { Path = path; } } internal void OfferOwner(PluginIdentity? owner, int rank) { if (owner != null && rank > ownerRank) { Owner = owner; ownerRank = rank; } } internal void SetRegistration(DataFileRegistration registration) { Registration = registration; } internal bool AddEntries(IEnumerable<ConfigEntryInfo>? incoming) { bool result = false; if (incoming == null) { return false; } foreach (ConfigEntryInfo item in incoming) { if (item != null && entryKeys.Add(item.Key)) { entries.Add(item); if (!sections.Contains(item.Section)) { sections.Add(item.Section); } result = true; } } return result; } internal void SetOrphans(IEnumerable<EntryKey> keys) { orphans.Clear(); foreach (EntryKey key in keys) { if (!orphans.Contains(key) && !entryKeys.Contains(key)) { orphans.Add(key); } } } public override string ToString() { return Kind.ToString() + " " + Path; } } public sealed class IndexGroup { public string Key { get; } public GroupKind Kind { get; } public string DisplayName { get; } public IReadOnlyList<IndexedFile> Files { get; } internal IndexGroup(string key, GroupKind kind, string displayName, IReadOnlyList<IndexedFile> files) { Key = key; Kind = kind; DisplayName = displayName; Files = files; } public override string ToString() { return Kind.ToString() + " " + DisplayName; } } public sealed class ConfigIndex { public const string CoreGroupKey = "bepinex:core"; public const string DiskGroupKey = "disk:unbound"; public const string DiskGroupName = "Files on disk"; private const int RankCalling = 1; private const int RankFieldScan = 2; private const int RankDeclared = 3; private const int RankInstance = 4; private readonly bool caseInsensitive; private readonly Dictionary<string, IndexedFile> byKey = new Dictionary<string, IndexedFile>(); private readonly List<IndexedFile> order = new List<IndexedFile>(); public IReadOnlyList<IndexedFile> Files => order; public IReadOnlyList<IndexGroup> Groups { get { List<IndexGroup> list = new List<IndexGroup>(); using (IEnumerator<IGrouping<string, IndexedFile>> enumerator = (from f in order group f by f.GroupKey).GetEnumerator()) { IGrouping<string, IndexedFile> current; List<IndexedFile> list2; GroupKind kind; string displayName; for (; enumerator.MoveNext(); list.Add(new IndexGroup(current.Key, kind, displayName, list2))) { current = enumerator.Current; list2 = (from f in current orderby (!f.Routes.Contains(DiscoveryRoute.PluginInstance)) ? 1 : 0, (f.Kind == FileKind.DataFile) ? 1 : 0 select f).ThenBy<IndexedFile, string>((IndexedFile f) => ConfigPaths.Normalize(f.Path), StringComparer.Ordinal).ToList(); IndexedFile indexedFile = list2[0]; switch (indexedFile.Kind) { case FileKind.CoreConfig: kind = GroupKind.Core; displayName = "BepInEx"; continue; case FileKind.UnboundConfig: kind = GroupKind.DiskFiles; displayName = "Files on disk"; continue; case FileKind.DataFile: if (list2.All((IndexedFile f) => f.Kind == FileKind.DataFile)) { kind = GroupKind.Plugin; displayName = indexedFile.Registration.Product.Name; continue; } break; } PluginIdentity? pluginIdentity = list2.Select((IndexedFile f) => f.Owner).FirstOrDefault((PluginIdentity o) => o != null); kind = ((pluginIdentity == null) ? GroupKind.UnknownOwner : GroupKind.Plugin); displayName = pluginIdentity?.Name ?? ConfigPaths.FileName(indexedFile.Path); } } return list.OrderBy((IndexGroup g) => (int)g.Kind).ThenBy<IndexGroup, string>((IndexGroup g) => g.DisplayName, StringComparer.OrdinalIgnoreCase).ThenBy<IndexGroup, string>((IndexGroup g) => g.Key, StringComparer.Ordinal) .ToList(); } } public ConfigIndex(bool caseInsensitivePaths = true) { caseInsensitive = caseInsensitivePaths; } public IndexedFile? Find(string path) { if (!byKey.TryGetValue(ConfigPaths.Key(path, caseInsensitive), out IndexedFile value)) { return null; } return value; } public void AddPluginConfig(PluginIdentity plugin, string path, IEnumerable<ConfigEntryInfo> entries) { IndexedFile indexedFile = Get(path); indexedFile.AddRoute(DiscoveryRoute.PluginInstance, path); indexedFile.OfferOwner(plugin ?? throw new ArgumentNullException("plugin"), 4); indexedFile.AddEntries(entries); } public void AddHookedFile(string path, PluginIdentity? declaredOwner, PluginIdentity? callingPlugin, IEnumerable<ConfigEntryInfo> entries) { IndexedFile indexedFile = Get(path); indexedFile.AddRoute(DiscoveryRoute.ConstructorHook, path); indexedFile.OfferOwner(declaredOwner, 3); indexedFile.OfferOwner(callingPlugin, 1); indexedFile.AddEntries(entries); } public void AddFieldScanFile(PluginIdentity holder, string path, IEnumerable<ConfigEntryInfo> entries) { IndexedFile indexedFile = Get(path); indexedFile.AddRoute(DiscoveryRoute.FieldScan, path); indexedFile.OfferOwner(holder, 2); indexedFile.AddEntries(entries); } public void AddCoreConfig(string path, IEnumerable<ConfigEntryInfo> entries) { IndexedFile indexedFile = Get(path); indexedFile.AddRoute(DiscoveryRoute.CoreConfig, path); indexedFile.AddEntries(entries); } public void AddDiskFile(string path) { if (ConfigPaths.HasExtension(path, ".cfg")) { Get(path).AddRoute(DiscoveryRoute.DiskScan, path); } } public void AddOrphanedKeys(string path, IEnumerable<EntryKey> keys) { (Find(path) ?? throw new InvalidOperationException("Unknown file: " + path)).SetOrphans(keys); } public void AddDataFile(DataFileRegistration registration) { if (registration == null) { throw new ArgumentNullException("registration"); } if (!ConfigPaths.HasExtension(registration.Path, ".yml", ".yaml")) { throw new ArgumentException("Only YAML data files can be registered: " + registration.Path, "registration"); } IndexedFile indexedFile = Find(registration.Path); if (indexedFile?.Registration != null) { if (indexedFile.Registration.Product.Guid != registration.Product.Guid) { throw new InvalidOperationException("Data file already registered by " + indexedFile.Registration.Product.Guid + ": " + registration.Path); } } else { IndexedFile obj = indexedFile ?? Get(registration.Path); obj.AddRoute(DiscoveryRoute.DataFileRegistry, registration.Path); obj.SetRegistration(registration); } } public bool UpdateEntries(string path, IEnumerable<ConfigEntryInfo> entries) { return (Find(path) ?? throw new InvalidOperationException("Unknown file: " + path)).AddEntries(entries); } private IndexedFile Get(string path) { if (string.IsNullOrEmpty(path)) { throw new ArgumentException("Path is required", "path"); } string key = ConfigPaths.Key(path, caseInsensitive); if (byKey.TryGetValue(key, out IndexedFile value)) { return value; } value = new IndexedFile(path); byKey.Add(key, value); order.Add(value); return value; } } public static class ConfigPaths { public static string Normalize(string path) { if (string.IsNullOrEmpty(path)) { return ""; } string text = path.Replace('\\', '/'); string text2 = (text.StartsWith("//", StringComparison.Ordinal) ? "//" : (text.StartsWith("/", StringComparison.Ordinal) ? "/" : "")); List<string> list = new List<string>(); string[] array = text.Split('/'); foreach (string text3 in array) { if (text3.Length != 0 && !(text3 == ".")) { if (text3 == ".." && list.Count > 0 && list[list.Count - 1] != ".." && !IsDrive(list[list.Count - 1])) { list.RemoveAt(list.Count - 1); } else { list.Add(text3); } } } return text2 + string.Join("/", list); } public static string Key(string path, bool caseInsensitive) { string text = Normalize(path); if (!caseInsensitive) { return text; } return text.ToUpperInvariant(); } public static bool Same(string a, string b, bool caseInsensitive) { return Key(a, caseInsensitive) == Key(b, caseInsensitive); } public static string FileName(string path) { string text = Normalize(path); int num = text.LastIndexOf('/'); if (num >= 0) { return text.Substring(num + 1); } return text; } public static bool HasExtension(string path, params string[] extensions) { string text = FileName(path); foreach (string text2 in extensions) { if (text.EndsWith(text2, StringComparison.OrdinalIgnoreCase) && text.Length > text2.Length) { return true; } } return false; } private static bool IsDrive(string segment) { if (segment.Length == 2) { return segment[1] == ':'; } return false; } } public sealed class PluginIdentity { public string Guid { get; } public string Name { get; } public string Version { get; } public PluginIdentity(string guid, string name, string version) { if (string.IsNullOrEmpty(guid)) { throw new ArgumentException("Plugin GUID is required", "guid"); } Guid = guid; Name = (string.IsNullOrEmpty(name) ? guid : name); Version = version ?? ""; } public override string ToString() { return Name + " (" + Guid + " " + Version + ")"; } } public enum DiscoveryRoute { PluginInstance, ConstructorHook, FieldScan, CoreConfig, DiskScan, DataFileRegistry } public enum FileKind { BoundConfig, CoreConfig, UnboundConfig, DataFile } public enum GroupKind { Plugin, UnknownOwner, Core, DiskFiles } public readonly struct EntryKey : IEquatable<EntryKey> { public string Section { get; } public string Key { get; } public EntryKey(string section, string key) { Section = section ?? ""; Key = key ?? ""; } public bool Equals(EntryKey other) { if (string.Equals(Section, other.Section, StringComparison.Ordinal)) { return string.Equals(Key, other.Key, StringComparison.Ordinal); } return false; } public override bool Equals(object? obj) { if (obj is EntryKey other) { return Equals(other); } return false; } public override int GetHashCode() { return (StringComparer.Ordinal.GetHashCode(Section ?? "") * 397) ^ StringComparer.Ordinal.GetHashCode(Key ?? ""); } public override string ToString() { return "[" + Section + "] " + Key; } } public sealed class ConfigEntryInfo { public EntryKey Key { get; } public string Section => Key.Section; public string Name => Key.Key; public string SettingTypeName { get; } public ConfigEntryInfo(string section, string key, string settingTypeName) { Key = new EntryKey(section, key); SettingTypeName = settingTypeName ?? ""; } public override string ToString() { return Key.ToString() + " : " + SettingTypeName; } } public sealed class DataFileRegistration { public PluginIdentity Product { get; } public string Path { get; } public string DisplayName { get; } public DataFileRegistration(PluginIdentity product, string path, string displayName) { Product = product ?? throw new ArgumentNullException("product"); if (string.IsNullOrEmpty(path)) { throw new ArgumentException("Path is required", "path"); } Path = path; DisplayName = (string.IsNullOrEmpty(displayName) ? System.IO.Path.GetFileName(path) : displayName); } } public static class LegacyPluginIds { private static readonly Dictionary<string, string> Renamed = new Dictionary<string, string>(StringComparer.Ordinal) { ["com.extrapotions.barter"] = "com.meadworks.barter", ["com.extrapotions.solstice"] = "com.meadworks.solstice" }; public static IReadOnlyDictionary<string, string> All => Renamed; public static string Current(string id) { string text; if (id == null || !Renamed.TryGetValue(id, out string value)) { text = id; if (text == null) { return ""; } } else { text = value; } return text; } public static bool Same(string a, string b) { return string.Equals(Current(a), Current(b), StringComparison.Ordinal); } public static string CurrentFile(string fileKey) { if (string.IsNullOrEmpty(fileKey) || fileKey.IndexOf('/') >= 0) { return fileKey ?? ""; } foreach (KeyValuePair<string, string> item in Renamed) { if (string.Equals(fileKey, item.Key + ".cfg", StringComparison.OrdinalIgnoreCase)) { return item.Value + ".cfg"; } } return fileKey; } } } namespace ExtraPotions.Almanac.Files { public sealed class CfgRow { public string Section { get; } public string Key { get; } public string Value { get; } public string? Note { get; } public CfgRow(string section, string key, string value, string? note = null) { Section = section; Key = key; Value = value; Note = note; } } public sealed class CfgParse { public IReadOnlyList<CfgRow> Values { get; } public IReadOnlyList<string> Errors { get; } internal CfgParse(IReadOnlyList<CfgRow> values, IReadOnlyList<string> errors) { Values = values; Errors = errors; } } public sealed class CfgChange { public string Section { get; } public string Key { get; } public string Before { get; } public string Value { get; } internal CfgChange(string section, string key, string before, string value) { Section = section; Key = key; Before = before; Value = value; } } public sealed class CfgPlan { public IReadOnlyList<CfgChange> Changes { get; } public IReadOnlyList<string> Errors { get; } public bool CanApply { get { if (Errors.Count == 0) { return Changes.Count > 0; } return false; } } internal CfgPlan(IReadOnlyList<CfgChange> changes, IReadOnlyList<string> errors) { Changes = changes; Errors = errors; } } public static class CfgText { public static string Render(IEnumerable<CfgRow> rows) { StringBuilder stringBuilder = new StringBuilder(); bool flag = true; foreach (IGrouping<string, CfgRow> item in rows.GroupBy<CfgRow, string>((CfgRow row) => row.Section, StringComparer.Ordinal)) { if (!flag) { stringBuilder.Append('\n'); } flag = false; stringBuilder.Append('[').Append(item.Key).Append("]\n"); foreach (CfgRow item2 in item) { if (!string.IsNullOrEmpty(item2.Note)) { string[] array = item2.Note.Split('\n'); foreach (string text in array) { stringBuilder.Append("# ").Append(text.TrimEnd('\r')).Append('\n'); } } stringBuilder.Append(item2.Key).Append(" = ").Append(item2.Value) .Append('\n'); } } return stringBuilder.ToString(); } public static CfgParse Parse(string text) { List<CfgRow> list = new List<CfgRow>(); List<string> list2 = new List<string>(); HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); string text2 = null; string[] array = (text ?? "").Replace("\r\n", "\n").Split('\n'); for (int i = 0; i < array.Length; i++) { string text3 = array[i].Trim(); string text4 = "Line " + (i + 1) + ": "; if (text3.Length == 0 || text3.StartsWith("#", StringComparison.Ordinal)) { continue; } if (text3.StartsWith("[", StringComparison.Ordinal) && text3.EndsWith("]", StringComparison.Ordinal)) { text2 = text3.Substring(1, text3.Length - 2).Trim(); continue; } int num = text3.IndexOf('='); if (num < 0) { list2.Add(text4 + "expected Key = Value."); continue; } string text5 = text3.Substring(0, num).Trim(); string value = text3.Substring(num + 1).Trim(); if (text5.Length == 0) { list2.Add(text4 + "missing key."); } else if (text2 == null) { list2.Add(text4 + "setting outside a [Section]."); } else if (!hashSet.Add(text2 + "\n" + text5)) { list2.Add(text4 + "duplicate setting " + text2 + " / " + text5 + "."); } else { list.Add(new CfgRow(text2, text5, value)); } } return new CfgParse(list.AsReadOnly(), list2.AsReadOnly()); } public static CfgPlan Plan(IEnumerable<CfgRow> original, CfgParse edited) { Dictionary<string, CfgRow> dictionary = new Dictionary<string, CfgRow>(StringComparer.Ordinal); foreach (CfgRow item in original) { dictionary[item.Section + "\n" + item.Key] = item; } List<string> list = new List<string>(edited.Errors); List<CfgChange> list2 = new List<CfgChange>(); foreach (CfgRow value2 in edited.Values) { if (!dictionary.TryGetValue(value2.Section + "\n" + value2.Key, out var value)) { list.Add("Unknown setting " + value2.Section + " / " + value2.Key + "; only listed settings can be edited."); } else if (!string.Equals(value.Value, value2.Value, StringComparison.Ordinal)) { list2.Add(new CfgChange(value2.Section, value2.Key, value.Value, value2.Value)); } } return new CfgPlan(list2.AsReadOnly(), list.AsReadOnly()); } } } namespace ExtraPotions.Almanac.Editors { public enum EditorKind { Toggle, Cycle, FlagsDropdown, Dropdown, Slider, Spinner, TextField, MultilineText, KeyCapture, ShortcutCapture, ColorPicker, VectorRow, ListEditor, RawText, ReadOnlyText, CustomDrawer, UguiDrawer } public enum CommitRule { None, OnClick, OnChoice, OnClose, OnRelease, OnEnter, SaveButton, OnKey, OnKeyRelease, ApplyButton } public enum CodecChoice { None, TomlConverter, ModDelegates } public sealed class EditorChoice { public EditorKind Kind { get; } public CommitRule Commit { get; } public int Fields { get; internal set; } public bool Integral { get; internal set; } public bool Percent { get; internal set; } public double? Step { get; internal set; } public string? Unit { get; internal set; } internal EditorChoice(EditorKind kind, CommitRule commit) { Kind = kind; Commit = commit; } public override string ToString() { return Kind.ToString() + " (" + Commit.ToString() + ")"; } } public static class EditorSelector { public const int MultilineThreshold = 120; public static EditorChoice Select(SettingShape shape, EntryAttributes attrs, string? currentText) { bool flag = shape.Kind == SettingKind.KeyCode || shape.Kind == SettingKind.KeyboardShortcut; if (attrs.HasCustomUguiDrawer) { return new EditorChoice(EditorKind.UguiDrawer, CommitRule.None); } if (attrs.HasCustomDrawer || (flag && attrs.HasCustomHotkeyDrawer)) { return new EditorChoice(EditorKind.CustomDrawer, CommitRule.None); } return Fallback(shape, attrs, currentText); } public static EditorChoice Fallback(SettingShape shape, EntryAttributes attrs, string? currentText) { if (shape.Kind == SettingKind.Bool) { return new EditorChoice(EditorKind.Toggle, CommitRule.OnClick); } if (shape.AcceptableList != null) { return new EditorChoice(EditorKind.Dropdown, CommitRule.OnChoice); } switch (shape.Kind) { case SettingKind.Enum: if (!shape.IsFlags) { return new EditorChoice(EditorKind.Cycle, CommitRule.OnChoice); } return new EditorChoice(EditorKind.FlagsDropdown, CommitRule.OnClose); case SettingKind.Integer: case SettingKind.Floating: return Numeric(shape, attrs); case SettingKind.String: if (!string.IsNullOrEmpty(attrs.ListSeparator)) { return new EditorChoice(EditorKind.ListEditor, CommitRule.ApplyButton); } if (attrs.Multiline || (currentText != null && (currentText.IndexOf('\n') >= 0 || currentText.Length > 120))) { return new EditorChoice(EditorKind.MultilineText, CommitRule.SaveButton); } return new EditorChoice(EditorKind.TextField, CommitRule.OnEnter); case SettingKind.KeyCode: return new EditorChoice(EditorKind.KeyCapture, CommitRule.OnKey); case SettingKind.KeyboardShortcut: return new EditorChoice(EditorKind.ShortcutCapture, CommitRule.OnKeyRelease); case SettingKind.Color: return new EditorChoice(EditorKind.ColorPicker, CommitRule.OnClose); case SettingKind.Vector2: return Vector(2); case SettingKind.Vector3: return Vector(3); case SettingKind.Vector4: case SettingKind.Quaternion: return Vector(4); case SettingKind.List: if (!shape.HasConverter) { return ReadOnly(); } return new EditorChoice(EditorKind.ListEditor, CommitRule.ApplyButton); default: if (Codec(shape, attrs) == CodecChoice.None) { return ReadOnly(); } return new EditorChoice(EditorKind.RawText, CommitRule.OnEnter); } } public static bool CanEditAsText(SettingShape shape, EntryAttributes attrs) { if (Codec(shape, attrs) != CodecChoice.None) { return Fallback(shape, attrs, null).Kind != EditorKind.ReadOnlyText; } return false; } public static CodecChoice Codec(SettingShape shape, EntryAttributes attrs) { if (attrs.HasCodecDelegates) { return CodecChoice.ModDelegates; } if (!shape.HasConverter) { return CodecChoice.None; } return CodecChoice.TomlConverter; } private static EditorChoice Numeric(SettingShape shape, EntryAttributes attrs) { bool flag = shape.Kind == SettingKind.Integer; EditorChoice editorChoice = ((!shape.HasRange) ? new EditorChoice(EditorKind.Spinner, CommitRule.OnEnter) { Step = (attrs.Step ?? (flag ? new double?(1.0) : ((double?)null))) } : new EditorChoice(EditorKind.Slider, CommitRule.OnRelease) { Step = (attrs.Step ?? (flag ? 1.0 : ((shape.RangeMax - shape.RangeMin) / 100.0))), Percent = (attrs.ShowRangeAsPercent == true) }); editorChoice.Integral = flag; editorChoice.Unit = attrs.Unit; return editorChoice; } private static EditorChoice Vector(int fields) { return new EditorChoice(EditorKind.VectorRow, CommitRule.OnEnter) { Fields = fields }; } private static EditorChoice ReadOnly() { return new EditorChoice(EditorKind.ReadOnlyText, CommitRule.None); } } public enum SettingKind { Bool, Enum, Integer, Floating, String, KeyCode, KeyboardShortcut, Color, Vector2, Vector3, Vector4, Quaternion, List, Other } public sealed class SettingShape { private static readonly string[] NoNames = new string[0]; public SettingKind Kind { get; } public string TypeName { get; } public bool HasConverter { get; } public bool IsFlags { get; private set; } public IReadOnlyList<string> EnumNames { get; private set; } = NoNames; public bool HasRange { get; private set; } public double RangeMin { get; private set; } public double RangeMax { get; private set; } public IReadOnlyList<string>? AcceptableList { get; private set; } public IReadOnlyList<string> KeyNames { get; private set; } = NoNames; public string ShortTypeName => TagObject.ShortTypeName(TypeName); private SettingShape(SettingKind kind, string typeName, bool hasConverter) { Kind = kind; TypeName = typeName ?? ""; HasConverter = hasConverter; } public static SettingShape Of(SettingKind kind, string typeName, bool hasConverter = true) { return new SettingShape(kind, typeName, hasConverter); } public static SettingShape Bool() { return Of(SettingKind.Bool, "System.Boolean"); } public static SettingShape String() { return Of(SettingKind.String, "System.String"); } public static SettingShape Integer(string typeName) { return Of(SettingKind.Integer, typeName); } public static SettingShape Floating(string typeName) { return Of(SettingKind.Floating, typeName); } public static SettingShape Enum(string typeName, IEnumerable<string> names, bool flags = false) { SettingShape settingShape = Of(SettingKind.Enum, typeName); settingShape.EnumNames = names.ToArray(); settingShape.IsFlags = flags; return settingShape; } public SettingShape WithRange(double min, double max) { if (min > max) { throw new ArgumentException("Range minimum is above maximum"); } SettingShape settingShape = Copy(); settingShape.HasRange = true; settingShape.RangeMin = min; settingShape.RangeMax = max; return settingShape; } public SettingShape WithList(IEnumerable<string> values) { SettingShape settingShape = Copy(); settingShape.AcceptableList = values.ToArray(); return settingShape; } public SettingShape WithKeyNames(IEnumerable<string> names) { SettingShape settingShape = Copy(); settingShape.KeyNames = names.ToArray(); return settingShape; } public static SettingKind KindOf(string typeName) { string text = typeName ?? ""; if (text.EndsWith("[]", StringComparison.Ordinal)) { return SettingKind.List; } int num = text.IndexOf('`'); if (num >= 0) { switch (TagObject.ShortTypeName(text.Substring(0, num))) { default: return SettingKind.Other; case "List": case "IList": case "HashSet": return SettingKind.List; } } switch (TagObject.ShortTypeName(text)) { case "Boolean": return SettingKind.Bool; case "Int64": case "SByte": case "Int16": case "Int32": case "UInt64": case "UInt16": case "UInt32": case "Byte": return SettingKind.Integer; case "Decimal": case "Single": case "Double": return SettingKind.Floating; case "String": return SettingKind.String; case "KeyCode": return SettingKind.KeyCode; case "KeyboardShortcut": return SettingKind.KeyboardShortcut; case "Color": return SettingKind.Color; case "Vector2": return SettingKind.Vector2; case "Vector3": return SettingKind.Vector3; case "Vector4": return SettingKind.Vector4; case "Quaternion": return SettingKind.Quaternion; default: return SettingKind.Other; } } private SettingShape Copy() { return new SettingShape(Kind, TypeName, HasConverter) { IsFlags = IsFlags, EnumNames = EnumNames, HasRange = HasRange, RangeMin = RangeMin, RangeMax = RangeMax, AcceptableList = AcceptableList, KeyNames = KeyNames }; } public override string ToString() { return Kind.ToString() + " " + TypeName; } } public sealed class TableShape { public char Delimiter { get; } public int Columns { get; } public IReadOnlyList<string[]> Rows { get; } internal TableShape(char delimiter, int columns, IReadOnlyList<string[]> rows) { Delimiter = delimiter; Columns = columns; Rows = rows; } } public static class TableText { public const int MaxColumns = 6; private static readonly char[] Candidates = new char[6] { '|', ';', ':', '=', ',', '\t' }; public static TableShape? Detect(IReadOnlyList<string> items, string? listSeparator = null) { List<string> list = items.Where((string item) => item.Length > 0).ToList(); if (list.Count == 0) { return null; } char[] candidates = Candidates; foreach (char delimiter in candidates) { if (listSeparator != null && listSeparator.IndexOf(delimiter) >= 0) { continue; } int count = Count(list[0], delimiter); if (count < 1 || count + 1 > 6 || list.Any((string item) => Count(item, delimiter) != count)) { continue; } List<string[]> rows = items.Select((string item) => (item.Length != 0) ? item.Split(delimiter) : new string[count + 1].Select((string _) => "").ToArray()).ToList(); return new TableShape(delimiter, count + 1, rows); } return null; } public static string Join(IReadOnlyList<string> cells, char delimiter) { if (!cells.All((string cell) => cell.Length == 0)) { return string.Join(delimiter.ToString(), cells); } return ""; } public static string CellError(string cell, char delimiter, string? listSeparator) { if (cell.IndexOf(delimiter) >= 0) { return "A cell cannot contain the column separator"; } if (!string.IsNullOrEmpty(listSeparator) && cell.Contains(listSeparator)) { return "A cell cannot contain the list separator"; } if (cell.IndexOfAny(new char[2] { '\r', '\n' }) >= 0) { return "A cell cannot contain a line break"; } return ""; } public static IReadOnlyList<string> Lines(string text, out string lineBreak) { lineBreak = (text.Contains("\r\n") ? "\r\n" : "\n"); return text.Split(new string[1] { lineBreak }, StringSplitOptions.None); } public static string JoinLines(IEnumerable<string> rows, string lineBreak) { return string.Join(lineBreak, rows); } private static int Count(string text, char c) { int num = 0; for (int i = 0; i < text.Length; i++) { if (text[i] == c) { num++; } } return num; } } public enum SyntaxKind { Plain, Cfg, Yaml, Json } public sealed class SyntaxIssue { internal sealed class Failure : Exception { public SyntaxIssue Issue { get; } public Failure(SyntaxIssue issue) { Issue = issue; } } public int Line { get; } public string Message { get; } public SyntaxIssue(int line, string message) { Line = line; Message = message; } public override string ToString() { return "Line " + Line + ": " + Message; } } public static class TextSyntax { private sealed class JsonCheck { private readonly string s; private int i; private JsonCheck(string text) { s = text; } public static void Run(string text) { JsonCheck jsonCheck = new JsonCheck(text); jsonCheck.Space(); jsonCheck.Value(0); jsonCheck.Space(); if (jsonCheck.i < text.Length) { jsonCheck.Fail("Unexpected text after the end"); } } private void Fail(string message) { int num = 0; for (int i = 0; i < Math.Min(this.i, s.Length); i++) { if (s[i] == '\n') { num++; } } Line(num, message); } private void Space() { while (i < s.Length && (s[i] == ' ' || s[i] == '\t' || s[i] == '\r' || s[i] == '\n')) { i++; } } private void Value(int depth) { if (depth > 256) { Fail("Nested too deeply"); } if (i >= s.Length) { Fail("Value expected"); } char c = s[i]; switch (c) { case '{': Container('}', depth, members: true); break; case '[': Container(']', depth, members: false); break; case '"': String(); break; default: if (!char.IsDigit(c)) { if (!Word("true") && !Word("false") && !Word("null")) { Fail("Unexpected character"); } break; } goto case '-'; case '-': Number(); break; } } private void Container(char close, int depth, bool members) { i++; Space(); if (i < s.Length && s[i] == close) { i++; return; } while (true) { if (members) { if (i >= s.Length || s[i] != '"') { Fail("Quoted key expected"); } String(); Space(); if (i >= s.Length || s[i] != ':') { Fail("Colon expected after the key"); } i++; Space(); } Value(depth + 1); Space(); if (i < s.Length && s[i] == ',') { i++; Space(); continue; } if (i < s.Length && s[i] == close) { break; } Fail("Comma or closing bracket expected"); } i++; } private void String() { i++; while (i < s.Length) { char c = s[i++]; switch (c) { case '"': return; case '\n': i--; Fail("Unclosed string"); break; } if (c != '\\') { continue; } if (i >= s.Length) { break; } char c2 = s[i++]; if (c2 == 'u') { int num = 0; while (num < 4) { if (i >= s.Length || !Uri.IsHexDigit(s[i])) { Fail("Invalid escape"); } num++; i++; } } else if ("\"\\/bfnrt".IndexOf(c2) < 0) { Fail("Invalid escape"); } } Fail("Unclosed string"); } private void Number() { int num = i; if (s[i] == '-') { i++; } if (i < s.Length && s[i] == '0') { i++; } else if (i < s.Length && char.IsDigit(s[i])) { while (i < s.Length && char.IsDigit(s[i])) { i++; } } else { Fail("Invalid number"); } if (i < s.Length && s[i] == '.') { i++; if (i >= s.Length || !char.IsDigit(s[i])) { Fail("Invalid number"); } while (i < s.Length && char.IsDigit(s[i])) { i++; } } if (i < s.Length && (s[i] == 'e' || s[i] == 'E')) { i++; if (i < s.Length && (s[i] == '+' || s[i] == '-')) { i++; } if (i >= s.Length || !char.IsDigit(s[i])) { Fail("Invalid number"); } while (i < s.Length && char.IsDigit(s[i])) { i++; } } if (i == num) { Fail("Invalid number"); } } private bool Word(string word) { if (string.CompareOrdinal(s, i, word, 0, word.Length) != 0) { return false; } i += word.Length; return true; } } public const string KeyColor = "#E8C27A"; public const string SectionColor = "#F0A050"; public const string StringColor = "#B8D88A"; public const string NumberColor = "#8EC4E8"; public const string CommentColor = "#8A8070"; public const string MarkColor = "#C8B89A"; public const int HighlightLimit = 60000; public static SyntaxKind Detect(string? fileName, string? text) { string text2 = (fileName ?? "").ToLowerInvariant(); if (text2.EndsWith(".json")) { return SyntaxKind.Json; } if (text2.EndsWith(".yml") || text2.EndsWith(".yaml")) { return SyntaxKind.Yaml; } if (text2.EndsWith(".cfg")) { return SyntaxKind.Cfg; } string text3 = (text ?? "").Trim(); if (text3.Length >= 2 && ((text3[0] == '{' && text3[text3.Length - 1] == '}') || (text3[0] == '[' && text3[text3.Length - 1] == ']'))) { return SyntaxKind.Json; } if (text3.IndexOf('\n') >= 0) { string[] array = text3.Split('\n'); for (int i = 0; i < array.Length; i++) { string text4 = array[i].TrimStart(); if (text4.StartsWith("- ") || text4 == "-" || text4 == "---" || YamlKeyEnd(text4) > 0) { return SyntaxKind.Yaml; } } } return SyntaxKind.Plain; } public static string Highlight(string? text, SyntaxKind kind) { if (text == null) { text = ""; } if (kind == SyntaxKind.Plain || text.Length > 60000) { return Escape(text); } StringBuilder stringBuilder = new StringBuilder(text.Length * 2); if (kind == SyntaxKind.Json) { Json(text, stringBuilder); return stringBuilder.ToString(); } int num = 0; while (num <= text.Length) { int num2 = text.IndexOf('\n', num); if (num2 < 0) { num2 = text.Length; } string line = text.Substring(num, num2 - num); if (kind == SyntaxKind.Cfg) { CfgLine(line, stringBuilder); } else { YamlLine(line, stringBuilder); } if (num2 < text.Length) { stringBuilder.Append('\n'); } num = num2 + 1; } return stringBuilder.ToString(); } public static SyntaxIssue? Find(string? text, SyntaxKind kind) { if (text == null) { text = ""; } try { switch (kind) { case SyntaxKind.Json: JsonCheck.Run(text); break; case SyntaxKind.Yaml: YamlCheck(text); break; } return null; } catch (SyntaxIssue.Failure failure) { return failure.Issue; } } public static string Check(string? text, SyntaxKind kind) { return Find(text, kind)?.ToString() ?? ""; } public static string Escape(string text) { if (text.IndexOf('<') >= 0) { return text.Replace("<", "<noparse><</noparse>"); } return text; } private static void Paint(StringBuilder b, string color, string text) { if (text.Length != 0) { b.Append("<color=").Append(color).Append('>') .Append(Escape(text)) .Append("</color>"); } } private static void Value(StringBuilder b, string value) { string text = value.Trim(); if (text.Length == 0) { b.Append(Escape(value)); return; } int num = value.IndexOf(text[0]); b.Append(value, 0, num); Paint(b, Scalar(text), text); b.Append(value, num + text.Length, value.Length - num - text.Length); } private static string Scalar(string core) { if (core.Length > 0 && (core[0] == '"' || core[0] == '\'')) { return "#B8D88A"; } switch (core.ToLowerInvariant()) { default: { if (!double.TryParse(core, NumberStyles.Float, CultureInfo.InvariantCulture, out var _)) { return "#B8D88A"; } goto case "true"; } case "true": case "false": case "null": case "~": return "#8EC4E8"; } } private static void CfgLine(string line, StringBuilder b) { string text = line.TrimStart(); if (text.StartsWith("#") || text.StartsWith(";")) { Paint(b, "#8A8070", line); return; } if (text.StartsWith("[") && text.TrimEnd().EndsWith("]")) { Paint(b, "#F0A050", line); return; } int num = line.IndexOf('='); if (num < 0) { b.Append(Escape(line)); return; } Paint(b, "#E8C27A", line.Substring(0, num)); Paint(b, "#C8B89A", "="); Value(b, line.Substring(num + 1)); } private static int YamlKeyEnd(string line) { int i = 0; if (line.Length > 0 && (line[0] == '"' || line[0] == '\'')) { int num = line.IndexOf(line[0], 1); if (num < 0) { return -1; } i = num + 1; if (i >= line.Length || line[i] != ':' || (i + 1 != line.Length && line[i + 1] != ' ')) { return -1; } return i; } for (; i < line.Length; i++) { switch (line[i]) { case '"': case '#': case '\'': case '[': case '{': return -1; case ':': if (i + 1 == line.Length || line[i + 1] == ' ') { if (i <= 0) { return -1; } return i; } break; } } return -1; } private static int CommentStart(string line) { char c = '\0'; for (int i = 0; i < line.Length; i++) { char c2 = line[i]; if (c != 0) { if (c2 == c) { c = '\0'; } continue; } switch (c2) { case '"': case '\'': c = c2; break; case '#': if (i == 0 || line[i - 1] == ' ' || line[i - 1] == '\t') { return i; } break; } } return -1; } private static void YamlLine(string line, StringBuilder b) { int num = CommentStart(line); string text = ((num >= 0) ? line.Substring(0, num) : line); string text2 = ((num >= 0) ? line.Substring(num) : ""); int i; for (i = 0; i < text.Length && text[i] == ' '; i++) { } b.Append(text, 0, i); string text3 = text.Substring(i); if (text3.TrimEnd() == "---" || text3.TrimEnd() == "...") { Paint(b, "#C8B89A", text3); Paint(b, "#8A8070", text2); return; } while (text3.StartsWith("- ") || text3 == "-") { Paint(b, "#C8B89A", "-"); int j; for (j = 1; j < text3.Length && text3[j] == ' '; j++) { } b.Append(text3, 1, j - 1); text3 = text3.Substring(j); } int num2 = YamlKeyEnd(text3); if (num2 > 0) { Paint(b, "#E8C27A", text3.Substring(0, num2)); Paint(b, "#C8B89A", ":"); text3 = text3.Substring(num2 + 1); } if (text3.TrimStart().StartsWith("&") || text3.TrimStart().StartsWith("*")) { Paint(b, "#F0A050", text3); } else { Value(b, text3); } Paint(b, "#8A8070", text2); } private static void Json(string text, StringBuilder b) { int num = 0; while (num < text.Length) { char c = text[num]; switch (c) { case '"': { int j; for (j = num + 1; j < text.Length && text[j] != '"' && text[j] != '\n'; j += ((text[j] != '\\') ? 1 : 2)) { } j = Math.Min(j + 1, text.Length); int k; for (k = j; k < text.Length && (text[k] == ' ' || text[k] == '\t'); k++) { } Paint(b, (k < text.Length && text[k] == ':') ? "#E8C27A" : "#B8D88A", text.Substring(num, j - num)); num = j; break; } default: if (!char.IsDigit(c) && !char.IsLetter(c)) { if (c == '{' || c == '}' || c == '[' || c == ']' || c == ':' || c == ',') { Paint(b, "#C8B89A", c.ToString()); num++; } else { b.Append((c == '<') ? "<noparse><</noparse>" : c.ToString()); num++; } break; } goto case '-'; case '-': { int i; for (i = num; i < text.Length && (char.IsLetterOrDigit(text[i]) || text[i] == '.' || text[i] == '-' || text[i] == '+'); i++) { } Paint(b, "#8EC4E8", text.Substring(num, i - num)); num = i; break; } } } } private static string YamlCheck(string text) { string[] array = text.Split('\n'); int num = 0; int index = 0; for (int i = 0; i < array.Length; i++) { string text2 = array[i].TrimEnd('\r'); int j; for (j = 0; j < text2.Length && (text2[j] == ' ' || text2[j] == '\t'); j++) { if (text2[j] == '\t') { return Line(i, "Tab used for indentation; use spaces"); } } char c = '\0'; for (int k = j; k < text2.Length; k++) { char c2 = text2[k]; switch (c) { case '\'': if (c2 == '\'') { if (k + 1 < text2.Length && text2[k + 1] == '\'') { k++; } else { c = '\0'; } } continue; case '"': switch (c2) { case '\\': k++; break; case '"': c = '\0'; break; } continue; default: if (c2 == '#' && (k == j || text2[k - 1] == ' ')) { break; } if ((c2 == '"' || c2 == '\'') && (k == j || " :-[{,".IndexOf(text2[k - 1]) >= 0)) { c = c2; continue; } switch (c2) { case '[': case '{': if (num == 0) { index = i; } num++; break; case ']': case '}': if (--num < 0) { return Line(i, "Closing bracket without an opening one"); } break; } continue; } break; } if (c != 0) { return Line(i, "Unclosed quote"); } } if (num <= 0) { return ""; } return Line(index, "Unclosed bracket"); } private static string Line(int index, string message) { throw new SyntaxIssue.Failure(new SyntaxIssue(index + 1, message)); } } } namespace ExtraPotions.Almanac.Display { public enum ChangeMark { Default, Modified, ChangedThisSession } public static class ChangeMarks { public static ChangeMark Of(string current, string resetTarget, string? sessionBefore) { if (sessionBefore != null && !string.Equals(sessionBefore, current, StringComparison.Ordinal)) { return ChangeMark.ChangedThisSession; } if (!string.Equals(current, resetTarget, StringComparison.Ordinal)) { return ChangeMark.Modified; } return ChangeMark.Default; } } public enum PauseAction { None, Pause, Unpause } public sealed class PauseLatch { public bool Owned { get; private set; } public PauseAction Tick(bool wanted, bool paused, bool externalHold) { if (wanted) { if (paused) { return PauseAction.None; } Owned = true; return PauseAction.Pause; } return Release(paused, externalHold); } public PauseAction Release(bool paused, bool externalHold) { if (!Owned) { return PauseAction.None; } Owned = false; if (!paused || externalHold) { return PauseAction.None; } return PauseAction.Unpause; } } public static class WindowFit { public const float Width = 1100f; public const float Height = 760f; public const float Margin = 24f; public static (float Width, float Height) Size(float canvasWidth, float canvasHeight, float halfWidthLimit = float.PositiveInfinity) { float val = Math.Min(1100f, Math.Min(canvasWidth - 48f, 2f * (halfWidthLimit - 24f))); float val2 = Math.Min(760f, canvasHeight - 48f); return (Width: Math.Max(320f, val), Height: Math.Max(300f, val2)); } } public static class WindowScale { public const string Default = "100%"; public static readonly string[] Presets = new string[5] { "80%", "90%", "100%", "115%", "130%" }; public static float Factor(string? preset) { int num = Array.IndexOf(Presets, preset?.Trim()); if (num >= 0) { return (float)int.Parse(Presets[num].TrimEnd('%')) / 100f; } return 1f; } public static float Bounded(float factor, float width, float height, float canvasWidth, float canvasHeight) { if (factor <= 1f) { return factor; } float val = Math.Min((canvasWidth - 48f) / width, (canvasHeight - 48f) / height); return Math.Max(1f, Math.Min(factor, val)); } } public static class RowButtons { public const float Text = 56f; public const float Reset = 68f; public const float Undo = 68f; public static float Width(float english, float caption) { return Math.Max(english, (float)Math.Ceiling(caption + 18f)); } public static float Cluster(float text, float reset, float undo) { return 6f + text + 4f + reset + 4f + undo; } public static (float X, float Value) Columns(float rowWidth, float cluster) { float num = Math.Max(160f, Math.Min(Math.Max(240f, rowWidth * 0.46f), rowWidth - cluster - 160f)); return (X: num, Value: Math.Max(100f, rowWidth - num - cluster)); } } public static class PluginBlurb { private static readonly Regex Description = new Regex("\"description\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)\"", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); public static string FromManifest(string? json) { if (string.IsNullOrEmpty(json)) { return ""; } Match match = Description.Match(json); if (!match.Success) { return ""; } try { return Regex.Unescape(match.Groups[1].Value).Trim(); } catch (ArgumentException) { return match.Groups[1].Value.Trim(); } } public static string Text(string? manifest, string? assembly, string name, string? version) { if (!string.IsNullOrWhiteSpace(manifest)) { return manifest.Trim(); } if (!string.IsNullOrWhiteSpace(assembly)) { return assembly.Trim(); } if (!string.IsNullOrWhiteSpace(version)) { return name + " " + version; } return name; } } public enum ChangeTiming { Live, Restart, WorldReload, NewItemsOnly } public static class SettingNotes { private static readonly string[] SyncSuffixes = new string[2] { " [Synced with Server]", " [Not Synced with Server]" }; private static readonly string[] RestartPhrases = new string[5] { "requires restart", "restart required", "after restart", "relog", "restart the game" }; private static readonly string[] WorldPhrases = new string[3] { "next time you enter a world", "re-enter the world", "re-entering the world" }; private static readonly string[] NewItemPhrases = new string[2] { "applies only to items", "applies to items created after" }; public static string WithoutSyncSuffix(string? description) { string text = description ?? ""; string[] syncSuffixes = SyncSuffixes; foreach (string text2 in syncSuffixes) { if (text.EndsWith(text2, StringComparison.Ordinal)) { return text.Substring(0, text.Length - text2.Length); } } return text; } public static ChangeTiming Classify(string? description, IEnumerable<string>? extraRestartPhrases = null) { string text = description ?? ""; if (Contains(text, RestartPhrases)) { return ChangeTiming.Restart; } if (extraRestartPhrases != null) { int num = 0; foreach (string extraRestartPhrase in extraRestartPhrases) { if (++num > 256) { break; } string text2 = extraRestartPhrase?.Trim() ?? ""; if (text2.Length > 0 && text.IndexOf(text2, StringComparison.OrdinalIgnoreCase) >= 0) { return ChangeTiming.Restart; } } } if (Contains(text, WorldPhrases)) { return ChangeTiming.WorldReload; } if (Contains(text, NewItemPhrases)) { return ChangeTiming.NewItemsOnly; } return ChangeTiming.Live; } private static bool Contains(string text, string[] phrases) { foreach (string value in phrases) { if (text.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0) { return true; } } return false; } } } namespace ExtraPotions.Almanac.Coexistence { public sealed class ChordBinding { public string Id { get; } public string Label { get; } public string Chord { get; } public ChordBinding(string id, string label, string chord) { Id = id ?? ""; Label = label ?? ""; Chord = chord ?? ""; } } public static class ChordClashPolicy { public static IReadOnlyDictionary<string, string[]> Find(IEnumerable<ChordBinding> bindings) { Dictionary<string, string[]> dictionary = new Dictionary<string, string[]>(StringComparer.Ordinal); foreach (IGrouping<string, ChordBinding> item in bindings.Where((ChordBinding b) => b.Chord.Length > 0 && b.Id.Length > 0).GroupBy<ChordBinding, string>((ChordBinding b) => b.Chord, StringComparer.OrdinalIgnoreCase)) { ChordBinding[] array = (from g in item.GroupBy<ChordBinding, string>((ChordBinding b) => b.Id, StringComparer.Ordinal) select g.First()).ToArray(); if (array.Length < 2) { continue; } ChordBinding[] array2 = array; foreach (ChordBinding member in array2) { dictionary[member.Id] = (from other in array where other != member select other.Label).Distinct<string>(StringComparer.Ordinal).OrderBy<string, string>((string label) => label, StringComparer.Ordinal).ToArray(); } } return dictionary; } } public enum CoexistenceMode { Auto, AlwaysOwnKey } public sealed class KnownManager { public string Guid { get; } public string Name { get; } public string ToggleSection { get; } public string ToggleKey { get; } public KnownManager(string guid, string name, string toggleSection, string toggleKey) { Guid = guid; Name = name; ToggleSection = toggleSection; ToggleKey = toggleKey; } } public static class KnownManagers { public static readonly IReadOnlyList<KnownManager> All = new KnownManager[3] { new KnownManager("com.bepis.bepinex.configurationmanager", "Configuration Manager", "General", "Show config manager"), new KnownManager("_shudnal.ConfigurationManager", "Valheim Configuration Manager", "General", "Show config manager"), new KnownManager("vapok.bepinex.configdrawers", "ConfigDrawers", "General", "Toggle Keybind") }; } public sealed class ManagerFacts { public string Guid { get; } public string Name { get; } public string? ToggleChord { get; } public ManagerFacts(string guid, string name, string? toggleChord) { Guid = guid; Name = name; ToggleChord = toggleChord; } } public sealed class HotkeyDecision { public string Chord { get; } public bool UsedFallback { get; } public string? NoticeManager { get; } public string? CollisionWith { get; } public string? NoticeText { get { if (NoticeManager != null) { return "F1 opens " + NoticeManager + ". Almanac opens with Left Alt + F1 (change it in Almanac)."; } return null; } } internal HotkeyDecision(string chord, bool usedFallback, string? noticeManager, string? collisionWith) { Chord = chord; UsedFallback = usedFallback; NoticeManager = noticeManager; CollisionWith = collisionWith; } } public static class HotkeyPolicy { public const string DefaultChord = "F1"; public const string FallbackChord = "F1 + LeftAlt"; public static HotkeyDecision Decide(string? configuredChord, CoexistenceMode mode, IEnumerable<ManagerFacts> installed) { List<ManagerFacts> managers = (installed ?? Enumerable.Empty<ManagerFacts>()).ToList(); ShortcutValue shortcutValue = ShortcutValue.Parse("F1"); if (!ShortcutValue.TryParse(configuredChord, null, out ShortcutValue value, out string _)) { value = shortcutValue; } if (value.IsEmpty) { return new HotkeyDecision("None", usedFallback: false, null, null); } if (!value.Equals(shortcutValue) || mode == CoexistenceMode.AlwaysOwnKey) { return new HotkeyDecision(value.ToString(), usedFallback: false, null, Owner(managers, value)?.Name); } ManagerFacts managerFacts = Owner(managers, shortcutValue); if (managerFacts == null) { return new HotkeyDecision(shortcutValue.ToString(), usedFallback: false, null, null); } ShortcutValue shortcutValue2 = ShortcutValue.Parse("F1 + LeftAlt"); return new HotkeyDecision(shortcutValue2.ToString(), usedFallback: true, managerFacts.Name, Owner(managers, shortcutValue2)?.Name); } private static ManagerFacts? Owner(IEnumerable<ManagerFacts> managers, ShortcutValue chord) { return managers.FirstOrDefault((ManagerFacts m) => Toggle(m).Equals(chord)); } private static ShortcutValue Toggle(ManagerFacts manager) { if (!ShortcutValue.TryParse(manager.ToggleChord ?? "F1", null, out ShortcutValue value, out string _)) { return ShortcutValue.Parse("F1"); } return value; } } public static class KeyConflictPolicy { public static string Describe(string path, bool hasModifiers, IEnumerable<KeyValuePair<string, string>> bindings, bool available) { if (string.IsNullOrEmpty(path)) { return ""; } if (!available) { return "Game key bindings are unavailable; native conflicts were not checked."; } if (!path.StartsWith("<Keyboard>/", StringComparison.OrdinalIgnoreCase)) { return ""; } string[] array = (from b in bindings where string.Equals(path, b.Value, StringComparison.OrdinalIgnoreCase) && !b.Key.StartsWith("Joy", StringComparison.Ordinal) select Name(b.Key)).Distinct().OrderBy<string, string>((string n) => n, StringComparer.Ordinal).ToArray(); if (array.Length == 0) { return ""; } return (hasModifiers ? "Main key also used by " : "Also bound to ") + string.Join(", ", array) + (hasModifiers ? ". Possible overlap depends on modifiers and gameplay context." : ". Gameplay context may prevent overlap."); } private static string Name(string action) { if (action.Length == 7 && action.StartsWith("Hotbar", StringComparison.Ordinal) && action[6] >= '1' && action[6] <= '8') { return "Hotbar slot " + action[6]; } return action; } } } namespace ExtraPotions.Almanac.Attributes { public sealed class TagObject { public string TypeName { get; } public string ShortName { get; } public IReadOnlyDictionary<string, object?> Members { get; } public TagObject(string typeName, IReadOnlyDictionary<string, object?> members) { TypeName = typeName ?? ""; Members = members ?? new Dictionary<string, object>(); ShortName = ShortTypeName(TypeName); } public bool TryGet(string member, out object? value) { return Members.TryGetValue(member, out value); } public static string ShortTypeName(string typeName) { string text = typeName ?? ""; int num = text.IndexOf('`'); if (num >= 0) { text = text.Substring(0, num); } int num2 = Math.Max(text.LastIndexOf('.'), text.LastIndexOf('+')); if (num2 < 0) { return text; } return text.Substring(num2 + 1); } } public sealed class EntryAttributes { public int? Order { get; internal set; } public bool? Browsable { get; internal set; } public string? Category { get; internal set; } public bool? IsAdvanced { get; internal set; } public bool? ReadOnly { get; internal set; } public string? DispName { get; internal set; } public string? Description { get; internal set; } public bool HasDefaultOverride { get; internal set; } public object? DefaultValue { get; internal set; } public bool? HideDefaultButton { get; internal set; } public bool? HideSettingName { get; internal set; } public bool? ShowRangeAsPercent { get; internal set; } public bool HasObjToStr { get; internal set; } public bool HasStrToObj { get; internal set; } public object? ObjToStr { get; internal set; } public object? StrToObj { get; internal set; } public bool HasCustomDrawer { get; internal set; } public bool HasCustomHotkeyDrawer { get; internal set; } public bool HasCustomUguiDrawer { get; internal set; } public bool? IsAdminOnly { get; internal set; } public bool? IsUnlocked { get; internal set; } public bool HasEntryColor { get; internal set; } public bool HasDescriptionColor { get; internal set; } public bool IsJotunnShaped => IsAdminOnly.HasValue; public bool RequiresRestart { get; internal set; } public string? ListSeparator { get; internal set; } public bool Multiline { get; internal set; } public string? Unit { get; internal set; } public double? Step { get; internal set; } public bool CharterReadOnly { get; internal set; } public bool GenericReadOnly { get; internal set; } public bool HasCodecDelegates { get { if (HasObjToStr) { return HasStrToObj; } return false; } } } public static class AttributeReader { public const string ManagerTag = "ConfigurationManagerAttributes"; public const string HintsTag = "AlmanacHints"; public const string CharterTag = "ReadOnlyTag"; public static readonly string[] UguiDrawerMembers = new string[3] { "CustomUguiDrawer", "CustomDrawerUGUI", "CustomDrawerUgui" }; public static EntryAttributes Read(IEnumerable<TagObject>? tags) { EntryAttributes entryAttributes = new EntryAttributes(); if (tags == null) { return entryAttributes; } foreach (TagObject tag in tags) { if (tag == null) { continue; } switch (tag.ShortName) { case "ConfigurationManagerAttributes": ReadManager(tag, entryAttributes); break; case "AlmanacHints": ReadHints(tag, entryAttributes); break; case "ReadOnlyTag": if (Bool(tag, "ReadOnly") == true) { entryAttributes.CharterReadOnly = true; } break; default: if (Bool(tag, "ReadOnly") == true) { entryAttributes.GenericReadOnly = true; } break; } } return entryAttributes; } private static void ReadManager(TagObject tag, EntryAttributes a) { EntryAttributes entryAttributes = a; if (!entryAttributes.Order.HasValue) { int? num = (entryAttributes.Order = Int(tag, "Order")); } entryAttributes = a; if (!entryAttributes.Browsable.HasValue) { bool? flag = (entryAttributes.Browsable = Bool(tag, "Browsable")); } entryAttributes = a; if (entryAttributes.Category == null) { string text = (entryAttributes.Category = Text(tag, "Category")); } entryAttributes = a; if (!entryAttributes.IsAdvanced.HasValue) { bool? flag = (entryAttributes.IsAdvanced = Bool(tag, "IsAdvanced")); } entryAttributes = a; if (!entryAttributes.ReadOnly.HasValue) { bool? flag = (entryAttributes.ReadOnly = Bool(tag, "ReadOnly")); } entryAttributes = a; if (entryAttributes.DispName == null) { string text = (entryAttributes.DispName = Text(tag, "DispName")); } entryAttributes = a; if (entryAttributes.Description == null) { string text = (entryAttributes.Description = Text(tag, "Description")); } if (!a.HasDefaultOverride && tag.TryGet("DefaultValue", out object value) && value != null) { a.HasDefaultOverride = true; a.DefaultValue = value; } entryAttributes = a; if (!entryAttributes.HideDefaultButton.HasValue) { bool? flag = (entryAttributes.HideDefaultButton = Bool(tag, "HideDefaultButton")); } entryAttributes = a; if (!entryAttributes.HideSettingName.HasValue) { bool? flag = (entryAttributes.HideSettingName = Bool(tag, "HideSettingName")); } entryAttributes = a; if (!entryAttributes.ShowRangeAsPercent.HasValue) { bool? flag = (entryAttributes.ShowRangeAsPercent = Bool(tag, "ShowRangeAsPercent")); } if (!a.HasObjToStr && Present(tag, "ObjToStr", out object value2)) { a.HasObjToStr = true; a.ObjToStr = value2; } if (!a.HasStrToObj && Present(tag, "StrToObj", out object value3)) { a.HasStrToObj = true; a.StrToObj = value3; } a.HasCustomDrawer |= Present(tag, "CustomDrawer", out object value4); a.HasCustomHotkeyDrawer |= Present(tag, "CustomHotkeyDrawer", out value4); string[] uguiDrawerMembers = UguiDrawerMembers; foreach (string member in uguiDrawerMembers) { a.HasCustomUguiDrawer |= tag.TryGet(member, out object value5) && value5 is Delegate; } entryAttributes = a; if (!entryAttributes.IsAdminOnly.HasValue) { bool? flag = (entryAttributes.IsAdminOnly = Bool(tag, "IsAdminOnly")); } entryAttributes = a; if (!entryAttributes.IsUnlocked.HasValue) { bool? flag = (entryAttributes.IsUnlocked = Bool(tag, "IsUnlocked")); } a.HasEntryColor |= Present(tag, "EntryColor", out value4); a.HasDescriptionColor |= Present(tag, "DescriptionColor", out value4); } private static void ReadHints(TagObject tag, EntryAttributes a) { if (Bool(tag, "RequiresRestart") == true) { a.RequiresRestart = true; } if (Bool(tag, "Multiline") == true) { a.Multiline = true; } EntryAttributes entryAttributes = a; if (entryAttributes.ListSeparator == null) { string text = (entryAttributes.ListSeparator = Text(tag, "ListSeparator")); } entryAttributes = a; if (entryAttributes.Unit == null) { string text = (entryAttributes.Unit = Text(tag, "Unit")); } entryAttributes = a; if (!entryAttributes.Step.HasValue) { double? num = (entryAttributes.Step = Number(tag, "Step")); } } private static bool Present(TagObject tag, string name, out object? value) { if (tag.TryGet(name, out value)) { return value != null; } return false; } private static bool? Bool(TagObject tag, string name) { if (!tag.TryGet(name, out object value) || !(value is bool)) { return null; } return (bool)value; } private static string? Text(TagObject tag, string name) { if (!tag.TryGet(name, out object value) || !(value is string result)) { return null; } return result; } private static int? Int(TagObject tag, string name) { if (!tag.TryGet(name, out object value) || value == null) { return null; } if (!(value is int)) { long num5; if (!(value is short num)) { if (!(value is sbyte b)) { if (!(value is byte b2)) { if (!(value is ushort num2)) { if (!(value is uint num3)) { if (!(value is long num4)) { return null; } num5 = num4; } else { num5 = num3; } } else { num5 = num2; } } else { num5 = b2; } } else { num5 = b; } } else { num5 = num; } if (num5 >= int.MinValue && num5 <= int.MaxValue) { return (int)num5; } return null; } return (int)value; } private static double? Number(TagObject tag, string name) { if (!tag.TryGet(name, out object value) || value == null) { return null; } if (!(value is double)) { if (!(value is float num)) { if (!(value is decimal num2)) { if (!(value is int num3)) { if (!(value is long num4)) { if (value is short num5) { return num5; } return null; } return num4; } return num3; } return (double)num2; } return num; } return (double)value; } } public enum Visibility { Visible, HiddenNotBrowsable, HiddenByServerPolicy, HiddenAdvanced } public static class Presentation { public static Visibility Decide(EntryAttributes a, bool showAdvanced, bool matchedBySearch, bool hiddenByServerPolicy) { if (a.Browsable == false) { return Visibility.HiddenNotBrowsable; } if (hiddenByServerPolicy) { return Visibility.HiddenByServerPolicy; } if (a.IsAdvanced == true && !showAdvanced && !matchedBySearch) { return Visibility.HiddenAdvanced; } return Visibility.Visible; } public static string DisplayName(EntryAttributes a, string key) { if (!string.IsNullOrEmpty(a.DispName)) { return a.DispName; } return key; } public static string Heading(EntryAttributes a, string section) { if (!string.IsNullOrEmpty(a.Category)) { return a.Category; } return section; } public static bool ShowsSettingName(EntryAttributes a, bool drawingCustom) { if (drawingCustom) { return a.HideSettingName != true; } return true; } public static bool ShowsResetButton(EntryAttributes a) { return a.HideDefaultButton != true; } public static object? ResetTarget(EntryAttributes a, object? entryDefault) { if (!a.HasDefaultOverride) { return entryDefault; } return a.DefaultValue; } public static IEnumerable<T> Sort<T>(IEnumerable<T> rows, Func<T, int?> order, Func<T, string> displayName) { return rows.OrderByDescending((T r) => order(r).GetValueOrDefault()).ThenBy<T, string>(displayName, StringComparer.OrdinalIgnoreCase).ThenBy<T, string>(displayName, StringComparer.Ordinal); } } }