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 Thing v0.1.14
plugins/Thing/Thing.Core.dll
Decompiled 13 hours 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.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using ExtraPotions.Thing.Admin; using ExtraPotions.Thing.Communication; using ExtraPotions.Thing.Config; using ExtraPotions.Thing.ConfigEditing.Attributes; using ExtraPotions.Thing.ConfigEditing.Editors; using ExtraPotions.Thing.Moderation; using ExtraPotions.Thing.RemoteConfig; using ExtraPotions.Thing.UI; using ExtraPotions.Thing.WorldAdmin; 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+source.C5CB98230C177B26BBB9D41F4869FF0DA5DE81F8C31B5A5A34E1D7BBF025C5D7")] [assembly: AssemblyProduct("Thing.Core")] [assembly: AssemblyTitle("Thing.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.Thing { public static class ThingCore { public const string PluginGuid = "com.meadworks.thing"; public const string ProductName = "Thing"; public const string ProductVersion = "0.1.14"; public const string Family = "Runestone"; public const string TokenNamespace = "thing"; public const string TokenPrefix = "meadworks_thing_"; public const string DataFolder = "Meadworks/thing"; public const string SupportedGameVersion = "1.0.17"; } } namespace ExtraPotions.Thing.WorldAdmin { public enum DropReceiptOutcome { Continue, Refused, Recovery } public static class DropReceipt { public static DropReceiptOutcome Decide(string status, int removed, int approved, int previousRemoved) { if (removed < 0 || approved < 0 || previousRemoved < 0 || removed > approved || (status != "ok" && status != "refused")) { return DropReceiptOutcome.Recovery; } if (status == "ok") { return DropReceiptOutcome.Continue; } if (previousRemoved <= 0 && removed <= 0) { return DropReceiptOutcome.Refused; } return DropReceiptOutcome.Recovery; } } public static class PickupRadius { public const float Min = 1f; public const float Max = 10f; public static bool TryParse(string? text, out float? radius) { radius = null; string text2 = (text ?? "").Trim(); if (text2.Equals("reset", StringComparison.OrdinalIgnoreCase)) { return true; } if (!float.TryParse(text2, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var result) || !(result >= 1f) || !(result <= 10f)) { return false; } radius = result; return true; } public static bool Reapply(float? wanted, float live) { if (wanted.HasValue) { return Math.Abs(wanted.Value - live) > 0.001f; } return false; } } public static class PoiData { public const string CapabilityId = "meadworks.thing.poi"; public const string OperationId = "meadworks.thing.poi_locations"; public static readonly Version Version = new Version(1, 0); public const int MaximumLocations = 512; public const int MaximumBytes = 16384; private static readonly HashSet<string> names = new HashSet<string>(StringComparer.Ordinal) { "Eikthyrnir", "GDKing", "Bonemass", "Dragonqueen", "GoblinKing", "Mistlands_DvergrBossEntrance1", "FaderLocation", "DN_Bossroom", "Vendor_BlackForest", "Hildir_camp", "BogWitch_Camp" }; public static bool Known(string name) { if (name != null) { return names.Contains(name); } return false; } public static bool Trader(string name) { if (!(name == "Vendor_BlackForest") && !(name == "Hildir_camp")) { return name == "BogWitch_Camp"; } return true; } private static bool Coordinate(float value) { if (!float.IsNaN(value) && !float.IsInfinity(value)) { return Math.Abs(value) <= 20000f; } return false; } private static bool Valid(Tuple<string, float, float, float, bool> row) { if (row != null && Known(row.Item1) && Coordinate(row.Item2) && Coordinate(row.Item3)) { return Coordinate(row.Item4); } return false; } public static IReadOnlyList<Tuple<string, float, float, float, bool>> Select(IEnumerable<Tuple<string, float, float, float, bool>> source) { Tuple<string, float, float, float, bool>[] source2 = source.Where(Valid).ToArray(); HashSet<string> confirmed = new HashSet<string>(from r in source2 where Trader(r.Item1) && r.Item5 select r.Item1, StringComparer.Ordinal); Tuple<string, float, float, float, bool>[] array = source2.Where((Tuple<string, float, float, float, bool> r) => !Trader(r.Item1) || r.Item5 || !confirmed.Contains(r.Item1)).Distinct().OrderBy<Tuple<string, float, float, float, bool>, string>((Tuple<string, float, float, float, bool> r) => r.Item1, StringComparer.Ordinal) .ThenBy((Tuple<string, float, float, float, bool> r) => r.Item2) .ThenBy((Tuple<string, float, float, float, bool> r) => r.Item4) .ToArray(); if (array.Length > 512 || 5 + array.Sum((Tuple<string, float, float, float, bool> r) => 14 + Encoding.UTF8.GetByteCount(r.Item1)) > 16384) { throw new InvalidOperationException("PoiLimit"); } return Array.AsReadOnly(array); } public static byte[] Encode(IReadOnlyList<Tuple<string, float, float, float, bool>> rows) { if (rows == null || rows.Count > 512 || rows.Any((Tuple<string, float, float, float, bool> r) => !Valid(r))) { throw new ArgumentException("Invalid POI rows."); } using MemoryStream memoryStream = new MemoryStream(); using BinaryWriter binaryWriter = new BinaryWriter(memoryStream, Encoding.UTF8, leaveOpen: true); binaryWriter.Write((byte)1); binaryWriter.Write(rows.Count); foreach (Tuple<string, float, float, float, bool> row in rows) { binaryWriter.Write(row.Item1); binaryWriter.Write(row.Item2); binaryWriter.Write(row.Item3); binaryWriter.Write(row.Item4); binaryWriter.Write(row.Item5); } if (memoryStream.Length > 16384) { throw new ArgumentException("POI reply too large."); } return memoryStream.ToArray(); } public static IReadOnlyList<Tuple<string, float, float, float, bool>> Decode(byte[] bytes) { if (bytes == null || bytes.Length < 5 || bytes.Length > 16384) { throw new FormatException("Invalid POI payload length."); } using MemoryStream memoryStream = new MemoryStream(bytes, writable: false); using BinaryReader binaryReader = new BinaryReader(memoryStream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true)); if (binaryReader.ReadByte() != 1) { throw new FormatException("Unsupported POI payload."); } int num = binaryReader.ReadInt32(); if (num < 0 || num > 512) { throw new FormatException("Invalid POI count."); } List<Tuple<string, float, float, float, bool>> list = new List<Tuple<string, float, float, float, bool>>(num); for (int i = 0; i < num; i++) { string item = binaryReader.ReadString(); float item2 = binaryReader.ReadSingle(); float item3 = binaryReader.ReadSingle(); float item4 = binaryReader.ReadSingle(); byte b = binaryReader.ReadByte(); Tuple<string, float, float, float, bool> tuple = Tuple.Create(item, item2, item3, item4, b == 1); if (b > 1 || !Valid(tuple)) { throw new FormatException("Invalid POI row."); } list.Add(tuple); } if (memoryStream.Position != memoryStream.Length) { throw new FormatException("Trailing POI payload."); } return list.AsReadOnly(); } } public static class TerrainCells { public static IEnumerable<int> InRadius(int pitch, float centerX, float centerY, float radius, float spacing) { if (pitch <= 0 || pitch > 1024 || !Finite(centerX) || !Finite(centerY) || !Finite(radius) || radius < 0f || !Finite(spacing) || spacing <= 0f) { throw new ArgumentOutOfRangeException("pitch", "Invalid terrain grid."); } float squaredRadius = radius * radius; for (int h = 0; h < pitch; h++) { for (int w = 0; w < pitch; w++) { float num = ((float)w - centerX) * spacing; float num2 = ((float)h - centerY) * spacing; if (num * num + num2 * num2 <= squaredRadius) { yield return h * pitch + w; } } } } public static bool Shape(int pitch, params int[] lengths) { if (pitch <= 0 || pitch > 1024) { return false; } for (int i = 0; i < lengths.Length; i++) { if (lengths[i] != pitch * pitch) { return false; } } return lengths.Length != 0; } private static bool Finite(float value) { if (!float.IsNaN(value)) { return !float.IsInfinity(value); } return false; } } public sealed class WardConfirmation { public const double MaxAgeSeconds = 75.0; private long generation = -1L; private string character = ""; private double started; private double expires; public void Confirm(long session, string nativeCharacter, double requestStarted) { if (session < 0 || string.IsNullOrEmpty(nativeCharacter) || double.IsNaN(requestStarted) || double.IsInfinity(requestStarted) || requestStarted < 0.0) { Clear(); return; } generation = session; character = nativeCharacter; started = requestStarted; expires = requestStarted + 75.0; } public bool Allows(long session, string nativeCharacter, double now) { if (character.Length > 0 && generation == session && character == nativeCharacter && !double.IsNaN(now) && !double.IsInfinity(now) && now >= started) { return now < expires; } return false; } public void Clear() { generation = -1L; character = ""; expires = 0.0; } } public sealed class WardStatusSchedule { public const double RefreshSeconds = 60.0; private double next = double.PositiveInfinity; private bool requested; public void Confirmed(double requestStarted) { next = ((double.IsNaN(requestStarted) || double.IsInfinity(requestStarted)) ? double.PositiveInfinity : (requestStarted + 60.0)); } public void Request() { requested = true; } public bool Due(bool wanted, double now) { if (wanted) { if (!requested) { return now >= next; } return true; } return false; } public void Sent() { requested = false; next = double.PositiveInfinity; } public void Clear() { requested = false; next = double.PositiveInfinity; } } public readonly struct PieceFootprint { public float X { get; } public float Z { get; } public float Extent { get; } public long Creator { get; } public PieceFootprint(float x, float z, float extent, long creator) { X = x; Z = z; Extent = Math.Max(0f, extent); Creator = creator; } public bool Overlaps(float centerX, float centerZ, float radius) { float num = X - centerX; float num2 = Z - centerZ; float num3 = radius + Extent; return num * num + num2 * num2 <= num3 * num3; } } public sealed class TerrainArea { public bool Complete { get; } public IReadOnlyList<PieceFootprint> Pieces { get; } public TerrainArea(bool complete, IReadOnlyList<PieceFootprint> pieces) { Complete = complete; Pieces = pieces ?? Array.Empty<PieceFootprint>(); } } public enum TerrainOutcome { Proceed, RefusedNotAdmin, RefusedIncomplete, RefusedOtherPieces } public static class TerrainReset { public static int Radius(float requested, int serverMaximum) { int num = Math.Max(1, serverMaximum); if (float.IsNaN(requested) || float.IsInfinity(requested)) { return 1; } return (int)Math.Min(num, Math.Max(1.0, Math.Round(requested, MidpointRounding.AwayFromZero))); } public static TerrainOutcome Decide(Verdict verdict, long requester, float centerX, float centerZ, int radius, TerrainArea area) { if (!AdminCheck.Permits(verdict)) { return TerrainOutcome.RefusedNotAdmin; } if (area == null || !area.Complete) { return TerrainOutcome.RefusedIncomplete; } if (verdict == Verdict.Allowed) { return TerrainOutcome.Proceed; } if (!area.Pieces.Any((PieceFootprint p) => p.Overlaps(centerX, centerZ, radius) && (p.Creator == 0L || p.Creator != requester))) { return TerrainOutcome.Proceed; } return TerrainOutcome.RefusedOtherPieces; } } public enum DropKind { LooseItem, PlacedItem, BuildingPiece, Container, Tombstone, SwimmingFish } public static class DropRules { public static bool Removes(DropKind kind, float distance, float radius) { if (kind == DropKind.LooseItem && !float.IsNaN(distance) && distance >= 0f) { return distance <= radius; } return false; } } public static class TimeOfDay { public static double? AtCurrentDay(double seconds, double dayLength, double fraction) { if (double.IsNaN(seconds) || double.IsInfinity(seconds) || seconds < 0.0 || double.IsNaN(dayLength) || double.IsInfinity(dayLength) || dayLength <= 0.0 || double.IsNaN(fraction) || double.IsInfinity(fraction) || fraction < 0.0 || fraction >= 1.0) { return null; } double num = Math.Floor(seconds / dayLength) * dayLength + fraction * dayLength; if (!double.IsInfinity(num) && !(num > 315537897599.0)) { return num; } return null; } public static double? Parse(string? text) { string text2 = (text ?? "").Trim(); switch (text2.ToLowerInvariant()) { case "midnight": return 0.0; case "sunrise": return 0.25; case "noon": return 0.5; case "sunset": return 0.75; default: { string[] array = text2.Split(':'); if (array.Length != 2 || array[0].Length == 0 || array[0].Length > 2 || array[1].Length != 2) { return null; } if (!int.TryParse(array[0], NumberStyles.None, CultureInfo.InvariantCulture, out var result) || !int.TryParse(array[1], NumberStyles.None, CultureInfo.InvariantCulture, out var result2)) { return null; } if (result > 23 || result2 > 59) { return null; } return (double)(result * 60 + result2) / 1440.0; } } } public static string Format(double fraction) { int num = (int)Math.Round((double.IsNaN(fraction) ? 0.0 : (fraction - Math.Floor(fraction))) * 1440.0) % 1440; return (num / 60).ToString("00", CultureInfo.InvariantCulture) + ":" + (num % 60).ToString("00", CultureInfo.InvariantCulture); } } public sealed class GlobalTimeState { public string SetBy { get; private set; } = ""; public double Fraction { get; private set; } public bool Frozen { get; private set; } public void Apply(string adminId, double fraction, bool freeze) { SetBy = (adminId ?? "").Trim(); Fraction = fraction; Frozen = freeze; } public bool ShouldRelease(Func<string, bool> online, Func<string, bool> isAdmin, bool releaseOnLeave) { if (!Frozen || !releaseOnLeave || SetBy.Length == 0) { return false; } if (online(SetBy)) { return !isAdmin(SetBy); } return true; } public bool Release() { if (!Frozen) { return false; } Frozen = false; return true; } } public sealed class WardOverrideGrants { private readonly HashSet<string> granted = new HashSet<string>(StringComparer.Ordinal); public bool Grant(string id) { return granted.Add((id ?? "").Trim()); } public bool Revoke(string id) { return granted.Remove((id ?? "").Trim()); } public bool Has(string id) { return granted.Contains((id ?? "").Trim()); } public IReadOnlyList<string> Prune(Func<string, bool> stillOnlineAdmin) { List<string> list = granted.Where((string id) => !stillOnlineAdmin(id)).OrderBy<string, string>((string id) => id, StringComparer.Ordinal).ToList(); foreach (string item in list) { granted.Remove(item); } return list; } public void Clear() { granted.Clear(); } } } namespace ExtraPotions.Thing.UI { public static class GrantReply { public static bool IsCurrent(string requestedId, string selectedId) { if (!string.IsNullOrEmpty(requestedId)) { return string.Equals(requestedId, selectedId, StringComparison.Ordinal); } return false; } public static bool TryRead(string requestedId, string selectedId, IReadOnlyList<string> lines, out AdminRight set) { set = AdminRight.None; if (!IsCurrent(requestedId, selectedId) || lines == null) { return false; } foreach (string line in lines) { bool fullAdmin; if (line != null && line.StartsWith("GRANTS\t", StringComparison.Ordinal)) { return AdminPermissions.TryWire(line, out fullAdmin, out set); } } return false; } } public sealed class ThingUiRow { public string Kind { get; } public string Id { get; } public string Caption { get; } public string Detail { get; } public ThingUiRow(string kind, string id, string caption, string detail) { Kind = kind; Id = id; Caption = caption; Detail = detail; } } public sealed class ThingUiPage { public const int PageSize = 12; public const int MaxTotal = 10000; private const string Marker = "THING_UI_PAGE"; public string Page { get; } public int Offset { get; } public int Total { get; } public string Version { get; } public IReadOnlyList<ThingUiRow> Rows { get; } private ThingUiPage(string page, int offset, int total, IReadOnlyList<ThingUiRow> rows, string version = "") { Page = page; Offset = offset; Total = total; Rows = rows; Version = version; } public static bool Known(string page) { switch (page) { default: return page == "config_entries"; case "players": case "moderation": case "audit": case "permitted": case "admins": case "grants": case "announcements": case "delve": case "delve_actions": case "delve_due": case "delve_nearest": case "delve_nature": case "config_files": return true; } } public static bool AdminReadPage(string page) { if (Known(page) && page != "announcements" && page != "delve" && page != "delve_actions" && page != "delve_due" && page != "delve_nearest" && page != "delve_nature" && page != "config_files") { return page != "config_entries"; } return false; } public static bool PlatformId(string id) { if (!string.IsNullOrWhiteSpace(id) && Encoding.UTF8.GetByteCount(id) <= 256) { return id.All((char c) => char.IsLetterOrDigit(c) || c == '_' || c == '-' || c == ':' || c == '.'); } return false; } public static IReadOnlyList<string> Encode(string page, int offset, IEnumerable<ThingUiRow> source, string version = "") { if (version.Length > 0 && !MessageEditing.Version(version)) { throw new ArgumentException("Invalid page version."); } if (!Known(page) || offset < 0 || offset >= 10000) { throw new ArgumentException("Invalid Thing page."); } List<ThingUiRow> list = source.Take(10000).ToList(); List<string> list2 = new List<string>(); list2.Add("THING_UI_PAGE\t" + page + "\t" + offset.ToString(CultureInfo.InvariantCulture) + "\t" + list.Count.ToString(CultureInfo.InvariantCulture) + ((version.Length == 0) ? "" : ("\t" + version))); List<string> list3 = list2; foreach (ThingUiRow item in list.Skip(offset).Take(12)) { if (!PlatformId(item.Id)) { throw new ArgumentException("Invalid row identity."); } list3.Add(string.Join("\t", AuditEntry.Escape(Clip(item.Kind, 16)), AuditEntry.Escape(item.Id), AuditEntry.Escape(Clip(item.Caption, 128)), AuditEntry.Escape(Clip(item.Detail, 224)))); } return list3; } private static string Clip(string text, int bytes) { if (text == null) { text = ""; } int num = 0; int num2 = 0; string text2 = text; for (int i = 0; i < text2.Length; i++) { char c = text2[i]; int num3 = (char.IsSurrogate(c) ? 3 : Encoding.UTF8.GetByteCount(c.ToString())); if (num + num3 > bytes) { break; } num += num3; num2++; } if (num2 > 0 && char.IsHighSurrogate(text[num2 - 1])) { num2--; } return text.Substring(0, num2); } public static bool TryDecode(IReadOnlyList<string> lines, out ThingUiPage page) { page = null; if (lines == null || lines.Count < 1 || lines.Count > 13) { return false; } string[] array = lines[0].Split('\t'); if ((array.Length != 4 && array.Length != 5) || (array.Length == 5 && !MessageEditing.Version(array[4])) || array[0] != "THING_UI_PAGE" || !Known(array[1]) || !int.TryParse(array[2], NumberStyles.None, CultureInfo.InvariantCulture, out var result) || result < 0 || result >= 10000 || !int.TryParse(array[3], NumberStyles.None, CultureInfo.InvariantCulture, out var result2) || result2 < 0 || result2 > 10000 || lines.Count - 1 != Math.Min(12, Math.Max(0, result2 - result))) { return false; } List<ThingUiRow> list = new List<ThingUiRow>(); foreach (string item in lines.Skip(1)) { string[] array2 = item.Split('\t'); string[] array3 = new string[4]; if (array2.Length != 4) { return false; } for (int i = 0; i < array3.Length; i++) { if (!AuditEntry.TryUnescape(array2[i], out array3[i])) { return false; } } if (!PlatformId(array3[1])) { return false; } list.Add(new ThingUiRow(array3[0], array3[1], array3[2], array3[3])); } page = new ThingUiPage(array[1], result, result2, list, (array.Length == 5) ? array[4] : ""); return true; } } } namespace ExtraPotions.Thing.RemoteConfig { public sealed class ConfigListDocument { public ConfigEntryDocument Element = new ConfigEntryDocument(); public IReadOnlyList<string> Values = Array.Empty<string>(); public const int MaxItems = 128; public const int MaxValueBytes = 1024; public const int MaxValuesBytes = 4096; public static IReadOnlyList<string> PackValues(IReadOnlyList<string> values) { return ConfigWire.Pack(ValueBytes(values)); } private static byte[] ValueBytes(IReadOnlyList<string> values) { using MemoryStream memoryStream = new MemoryStream(); using (BinaryWriter writer = new BinaryWriter(memoryStream, ConfigWire.Utf8, leaveOpen: true)) { WriteValues(writer, values); } if (memoryStream.Length > 4096) { throw new FormatException(); } return memoryStream.ToArray(); } private static void WriteValues(BinaryWriter writer, IReadOnlyList<string> values) { if (values.Count > 128) { throw new FormatException(); } writer.Write(values.Count); foreach (string value in values) { if (value == null || ConfigWire.Utf8.GetByteCount(value) > 1024) { throw new FormatException(); } writer.Write(value); } } private static IReadOnlyList<string> ReadValues(BinaryReader reader) { int num = reader.ReadInt32(); if (num < 0 || num > 128) { throw new FormatException(); } string[] array = new string[num]; for (int i = 0; i < num; i++) { array[i] = reader.ReadString(); if (ConfigWire.Utf8.GetByteCount(array[i]) > 1024) { throw new FormatException(); } } if (ValueBytes(array).Length > 4096) { throw new FormatException(); } return array; } public static IReadOnlyList<string> UnpackValues(IReadOnlyList<string> chunks) { using MemoryStream memoryStream = new MemoryStream(ConfigWire.UnpackBytes(chunks, 4096)); using BinaryReader reader = new BinaryReader(memoryStream, ConfigWire.Utf8); IReadOnlyList<string> result = ReadValues(reader); if (memoryStream.Position != memoryStream.Length) { throw new FormatException(); } return result; } public IReadOnlyList<string> Encode() { using MemoryStream memoryStream = new MemoryStream(); using (BinaryWriter binaryWriter = new BinaryWriter(memoryStream, ConfigWire.Utf8, leaveOpen: true)) { binaryWriter.Write((byte)1); IReadOnlyList<string> readOnlyList = Element.Encode(); binaryWriter.Write(readOnlyList.Count); foreach (string item in readOnlyList) { binaryWriter.Write(item); } WriteValues(binaryWriter, Values); } if (memoryStream.Length > 7168) { throw new FormatException(); } return new string[1] { "THING_CONFIG_LIST" }.Concat(ConfigWire.Pack(memoryStream.ToArray())).ToArray(); } public static bool TryDecode(IReadOnlyList<string> lines, out ConfigListDocument document) { document = null; if (lines.Count < 2 || lines.Count > 16 || lines[0] != "THING_CONFIG_LIST") { return false; } try { using MemoryStream memoryStream = new MemoryStream(ConfigWire.UnpackBytes(lines.Skip(1).ToArray(), 7168)); using BinaryReader binaryReader = new BinaryReader(memoryStream, ConfigWire.Utf8); if (binaryReader.ReadByte() != 1) { return false; } int num = binaryReader.ReadInt32(); if (num < 2 || num > 16) { return false; } string[] array = new string[num]; for (int i = 0; i < num; i++) { array[i] = binaryReader.ReadString(); } if (!ConfigEntryDocument.TryDecode(array, out ConfigEntryDocument document2)) { return false; } IReadOnlyList<string> values = ReadValues(binaryReader); if (memoryStream.Position != memoryStream.Length) { return false; } document = new ConfigListDocument { Element = document2, Values = values }; return true; } catch { return false; } } } public sealed class ConfigEditRequest { public string Verb { get; private set; } = ""; public string File { get; private set; } = ""; public string Entry { get; private set; } = ""; public string Version { get; private set; } = ""; public string Value { get; private set; } = ""; public int Offset { get; private set; } public string Token { get; private set; } = ""; public int Total { get; private set; } public byte[] Bytes { get; private set; } = Array.Empty<byte>(); public IReadOnlyList<string> ListValues { get; private set; } = Array.Empty<string>(); public bool Writes { get { if (!(Verb == "list-set") && !(Verb == "set") && !(Verb == "yaml-begin") && !(Verb == "yaml-put")) { return Verb == "yaml-commit"; } return true; } } public ConfigEditRequest WithValue(string value) { return new ConfigEditRequest { Verb = "set", File = File, Entry = Entry, Version = Version, Value = value }; } public static bool TryParse(IReadOnlyList<string> args, out ConfigEditRequest request) { request = null; if (args == null || args.Count == 0) { return false; } string text = args[0].ToLowerInvariant(); ConfigEditRequest configEditRequest = new ConfigEditRequest(); if (text == "set") { if (args.Count < 5 || !MessageEditing.Version(args[1]) || !MessageEditing.Version(args[2]) || !MessageEditing.Version(args[3])) { return false; } try { configEditRequest.Value = ConfigWire.Unpack(args.Skip(4).ToArray(), 4096); } catch { return false; } configEditRequest.Verb = "set"; configEditRequest.File = args[1]; configEditRequest.Entry = args[2]; configEditRequest.Version = args[3]; request = configEditRequest; return true; } if (text != "config" || args.Count < 2) { return false; } configEditRequest.Verb = args[1].ToLowerInvariant(); if (configEditRequest.Verb == "list-get" || configEditRequest.Verb == "list-set") { if (args.Count < 4 || !MessageEditing.Version(args[2]) || !MessageEditing.Version(args[3])) { return false; } configEditRequest.File = args[2]; configEditRequest.Entry = args[3]; if (configEditRequest.Verb == "list-get") { if (args.Count != 4) { return false; } } else { if (args.Count < 6 || !MessageEditing.Version(args[4])) { return false; } configEditRequest.Version = args[4]; try { configEditRequest.ListValues = ConfigListDocument.UnpackValues(args.Skip(5).ToArray()); } catch { return false; } } request = configEditRequest; return true; } if (configEditRequest.Verb.StartsWith("yaml-", StringComparison.Ordinal)) { if (args.Count < 3 || !MessageEditing.Version(args[2])) { return false; } configEditRequest.Token = args[2]; switch (configEditRequest.Verb) { case "yaml-open": if (args.Count != 3) { return false; } configEditRequest.File = args[2]; break; case "yaml-cancel": case "yaml-commit": if (args.Count != 3) { return false; } break; case "yaml-read": { if (args.Count != 4 || !int.TryParse(args[3], NumberStyles.None, CultureInfo.InvariantCulture, out var result2) || result2 < 0 || result2 > 1048576) { return false; } configEditRequest.Offset = result2; break; } case "yaml-begin": { if (args.Count != 5 || !int.TryParse(args[3], NumberStyles.None, CultureInfo.InvariantCulture, out var result3) || result3 < 0 || result3 > 1048576 || !MessageEditing.Version(args[4])) { return false; } configEditRequest.Total = result3; configEditRequest.Version = args[4]; break; } case "yaml-put": { if (args.Count < 5 || !int.TryParse(args[3], NumberStyles.None, CultureInfo.InvariantCulture, out var result) || result < 0 || result > 1048576) { return false; } try { configEditRequest.Bytes = YamlTransfer.Unpack(args.Skip(4).ToArray()); } catch { return false; } if (configEditRequest.Bytes.Length == 0) { return false; } configEditRequest.Offset = result; break; } default: return false; } request = configEditRequest; return true; } if (configEditRequest.Verb == "list") { if (args.Count != 2 && args.Count != 3) { return false; } } else { if (!(configEditRequest.Verb == "entries")) { if (configEditRequest.Verb == "get") { if (args.Count != 4 || !MessageEditing.Version(args[2]) || !MessageEditing.Version(args[3])) { return false; } configEditRequest.File = args[2]; configEditRequest.Entry = args[3]; request = configEditRequest; return true; } return false; } if ((args.Count != 3 && args.Count != 4) || !MessageEditing.Version(args[2])) { return false; } configEditRequest.File = args[2]; } int num = ((configEditRequest.Verb == "list") ? 2 : 3); if (args.Count > num && (!int.TryParse(args[num], NumberStyles.None, CultureInfo.InvariantCulture, out var result4) || result4 < 0 || result4 >= 10000)) { return false; } configEditRequest.Offset = ((args.Count > num) ? int.Parse(args[num], CultureInfo.InvariantCulture) : 0); request = configEditRequest; return true; } } public sealed class ConfigEntryDocument { public string File = ""; public string Entry = ""; public string Version = ""; public string Section = ""; public string Key = ""; public string Description = ""; public string Current = ""; public string Default = ""; public string Reason = ""; public string Effective = ""; public bool Editable; public SettingShape Shape = SettingShape.String(); public EntryAttributes Attributes = AttributeReader.Read(Array.Empty<TagObject>()); public IReadOnlyList<string> Encode() { if (!MessageEditing.Version(File) || !MessageEditing.Version(Entry) || !MessageEditing.Version(Version)) { throw new FormatException("Invalid config document identity."); } using MemoryStream memoryStream = new MemoryStream(); using (BinaryWriter binaryWriter = new BinaryWriter(memoryStream, ConfigWire.Utf8, leaveOpen: true)) { binaryWriter.Write((byte)1); string[] array = new string[10] { File, Entry, Version, Section, Key, Description, Current, Default, Reason, Effective }; foreach (string value in array) { binaryWriter.Write(value); } binaryWriter.Write(Editable); binaryWriter.Write((int)Shape.Kind); binaryWriter.Write(Shape.TypeName); binaryWriter.Write(Shape.HasConverter); binaryWriter.Write(Shape.IsFlags); binaryWriter.Write(Shape.HasRange); binaryWriter.Write(Shape.RangeMin); binaryWriter.Write(Shape.RangeMax); WriteList(binaryWriter, Shape.EnumNames); WriteList(binaryWriter, Shape.AcceptableList); WriteList(binaryWriter, Shape.KeyNames); binaryWriter.Write(Attributes.RequiresRestart); binaryWriter.Write(Attributes.Multiline); binaryWriter.Write(Attributes.ListSeparator ?? ""); binaryWriter.Write(Attributes.Unit ?? ""); binaryWriter.Write(Attributes.Step.HasValue); binaryWriter.Write(Attributes.Step.GetValueOrDefault()); binaryWriter.Write(Attributes.ShowRangeAsPercent == true); } if (memoryStream.Length > 7168) { throw new FormatException("Config metadata exceeds one bounded frame."); } return new string[1] { "THING_CONFIG_ENTRY" }.Concat(ConfigWire.Pack(memoryStream.ToArray())).ToArray(); } private static void WriteList(BinaryWriter writer, IReadOnlyList<string>? list) { if (list != null && list.Count > 512) { throw new FormatException("Too many metadata choices."); } writer.Write(list?.Count ?? (-1)); if (list == null) { return; } foreach (string item in list) { writer.Write(item); } } private static string[]? ReadList(BinaryReader reader) { int num = reader.ReadInt32(); if (num == -1) { return null; } if (num < 0 || num > 512) { throw new FormatException(); } string[] array = new string[num]; for (int i = 0; i < num; i++) { array[i] = reader.ReadString(); } return array; } public static bool TryDecode(IReadOnlyList<string> lines, out ConfigEntryDocument document) { document = null; if (lines == null || lines.Count < 2 || lines.Count > 16 || lines[0] != "THING_CONFIG_ENTRY") { return false; } try { using MemoryStream memoryStream = new MemoryStream(ConfigWire.UnpackBytes(lines.Skip(1).ToArray(), 7168)); using BinaryReader binaryReader = new BinaryReader(memoryStream, ConfigWire.Utf8); if (binaryReader.ReadByte() != 1) { return false; } ConfigEntryDocument configEntryDocument = new ConfigEntryDocument { File = binaryReader.ReadString(), Entry = binaryReader.ReadString(), Version = binaryReader.ReadString(), Section = binaryReader.ReadString(), Key = binaryReader.ReadString(), Description = binaryReader.ReadString(), Current = binaryReader.ReadString(), Default = binaryReader.ReadString(), Reason = binaryReader.ReadString(), Effective = binaryReader.ReadString(), Editable = binaryReader.ReadBoolean() }; if (!MessageEditing.Version(configEntryDocument.File) || !MessageEditing.Version(configEntryDocument.Entry) || !MessageEditing.Version(configEntryDocument.Version) || configEntryDocument.Current.Length > 4096 || configEntryDocument.Default.Length > 4096) { return false; } SettingKind settingKind = (SettingKind)binaryReader.ReadInt32(); if (!Enum.IsDefined(typeof(SettingKind), settingKind)) { return false; } string typeName = binaryReader.ReadString(); bool hasConverter = binaryReader.ReadBoolean(); bool flags = binaryReader.ReadBoolean(); bool num = binaryReader.ReadBoolean(); double num2 = binaryReader.ReadDouble(); double num3 = binaryReader.ReadDouble(); string[] array = ReadList(binaryReader); string[] array2 = ReadList(binaryReader); string[] array3 = ReadList(binaryReader); configEntryDocument.Shape = ((settingKind == SettingKind.Enum) ? SettingShape.Enum(typeName, array ?? Array.Empty<string>(), flags) : SettingShape.Of(settingKind, typeName, hasConverter)); if (num) { if (double.IsNaN(num2) || double.IsInfinity(num2) || double.IsNaN(num3) || double.IsInfinity(num3)) { return false; } configEntryDocument.Shape = configEntryDocument.Shape.WithRange(num2, num3); } if (array2 != null) { configEntryDocument.Shape = configEntryDocument.Shape.WithList(array2); } if (array3 != null) { configEntryDocument.Shape = configEntryDocument.Shape.WithKeyNames(array3); } configEntryDocument.Attributes.RequiresRestart = binaryReader.ReadBoolean(); configEntryDocument.Attributes.Multiline = binaryReader.ReadBoolean(); string text = binaryReader.ReadString(); configEntryDocument.Attributes.ListSeparator = ((text.Length == 0) ? null : text); string text2 = binaryReader.ReadString(); configEntryDocument.Attributes.Unit = ((text2.Length == 0) ? null : text2); bool num4 = binaryReader.ReadBoolean(); double num5 = binaryReader.ReadDouble(); if (num4) { if (double.IsNaN(num5) || double.IsInfinity(num5) || num5 <= 0.0) { return false; } configEntryDocument.Attributes.Step = num5; } configEntryDocument.Attributes.ShowRangeAsPercent = binaryReader.ReadBoolean(); if (memoryStream.Position != memoryStream.Length) { return false; } document = configEntryDocument; return true; } catch { return false; } } } public static class ConfigWire { public const string Marker = "THING_CONFIG_ENTRY"; public const int MaxDocumentBytes = 7168; internal static readonly UTF8Encoding Utf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); public static IReadOnlyList<string> PackValue(string value) { return Pack(Utf8.GetBytes(value ?? "")); } public static IReadOnlyList<string> WriteEcho(string version, string value) { if (!MessageEditing.Version(version) || Utf8.GetByteCount(value) > 4096) { throw new FormatException(); } return new string[1] { "THING_CONFIG_WRITE\t" + version }.Concat(PackValue(value)).ToArray(); } public static bool TryWriteEcho(IReadOnlyList<string> lines, out string version, out string value) { version = (value = ""); if (lines == null || lines.Count < 2 || lines.Count > 16) { return false; } string[] array = lines[0].Split('\t'); if (array.Length != 2 || array[0] != "THING_CONFIG_WRITE" || !MessageEditing.Version(array[1])) { return false; } try { value = Unpack(lines.Skip(1).ToArray(), 4096); version = array[1]; return true; } catch { return false; } } internal static IReadOnlyList<string> Pack(byte[] bytes) { string text = Convert.ToBase64String(bytes); List<string> list = new List<string>(); for (int i = 0; i < text.Length; i += 700) { list.Add(text.Substring(i, Math.Min(700, text.Length - i))); } if (list.Count == 0) { list.Add(""); } return list; } internal static string Unpack(IReadOnlyList<string> parts, int max) { return Utf8.GetString(UnpackBytes(parts, max)); } internal static byte[] UnpackBytes(IReadOnlyList<string> parts, int max) { if (parts.Count < 1 || parts.Count > 15 || parts.Any((string p) => p == null || p.Length > 700)) { throw new FormatException(); } byte[] array = Convert.FromBase64String(string.Concat(parts)); if (array.Length > max) { throw new FormatException(); } return array; } } public enum FileVerdict { Allowed, RefusedPath, RefusedLink, RefusedAdminList, RefusedExtension, RefusedScope, RefusedTooLarge } public readonly struct FileRequest { public string Path { get; } public long SizeBytes { get; } public bool IsLink { get; } public FileRequest(string path, long sizeBytes, bool isLink) { Path = path ?? ""; SizeBytes = sizeBytes; IsLink = isLink; } } public sealed class FilePolicy { public RemoteConfigScope Scope { get; } public bool CaseSensitive { get; } public ISet<string> AllowList { get; } public ISet<string> RegisteredYaml { get; } public FilePolicy(RemoteConfigScope scope, string allowList, IEnumerable<string> registeredYaml, bool caseSensitive) { Scope = scope; CaseSensitive = caseSensitive; StringComparer comparer = (caseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase); AllowList = new HashSet<string>(from n in ThingSettings.NormalizeList(allowList ?? "").Split(',') where n.Length > 0 select n, comparer); RegisteredYaml = new HashSet<string>(registeredYaml ?? Enumerable.Empty<string>(), comparer); } } public static class RemoteFiles { public const long MaxBytes = 1048576L; public const string MeadworksPrefix = "com.meadworks."; public const string ThingConfig = "com.meadworks.thing.cfg"; public const string ThingFolder = "Meadworks/thing/"; public static FileVerdict Check(FileRequest request, FilePolicy policy) { if (policy == null) { throw new ArgumentNullException("policy"); } string path = request.Path; if (!SafePath(path)) { return FileVerdict.RefusedPath; } if (request.IsLink) { return FileVerdict.RefusedLink; } if (string.Equals(path, "adminlist.txt", StringComparison.OrdinalIgnoreCase)) { return FileVerdict.RefusedAdminList; } bool flag = path.IndexOf('/') < 0; bool flag2 = path.EndsWith(".cfg", StringComparison.OrdinalIgnoreCase); bool flag3 = path.EndsWith(".yml", StringComparison.OrdinalIgnoreCase) || path.EndsWith(".yaml", StringComparison.OrdinalIgnoreCase); if (flag3 ? (!policy.RegisteredYaml.Contains(path)) : (!flag2)) { return FileVerdict.RefusedExtension; } StringComparison comparisonType = (policy.CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase); bool flag4 = flag3 || (flag && path.StartsWith("com.meadworks.", comparisonType)); if (policy.Scope switch { RemoteConfigScope.AllowList => (flag4 || (flag && policy.AllowList.Contains(path))) ? 1 : 0, RemoteConfigScope.AllConfigFiles => (flag4 || flag) ? 1 : 0, _ => flag4 ? 1 : 0, } == 0) { return FileVerdict.RefusedScope; } if (request.SizeBytes < 0 || request.SizeBytes > 1048576) { return FileVerdict.RefusedTooLarge; } return FileVerdict.Allowed; } public static bool SafePath(string? path) { if (string.IsNullOrEmpty(path) || path[0] == '/') { return false; } if (path.Any((char c) => c == '\\' || c == ':' || char.IsControl(c))) { return false; } string[] array = path.Split('/'); foreach (string text in array) { if (text.Length == 0 || text == "." || text == "..") { return false; } if (text.Trim() != text || text.EndsWith(".", StringComparison.Ordinal)) { return false; } } return true; } public static bool RawTextEditable(string path, IEnumerable<string> registeredYaml, bool caseSensitive) { return new FilePolicy(RemoteConfigScope.MeadworksOnly, "", registeredYaml, caseSensitive).RegisteredYaml.Contains(path ?? ""); } public static bool Protected(string path) { if (!string.Equals(path, "com.meadworks.thing.cfg", StringComparison.OrdinalIgnoreCase)) { return (path ?? "").StartsWith("Meadworks/thing/", StringComparison.OrdinalIgnoreCase); } return true; } } public static class FileVersion { public static string Hash(byte[] bytes) { using SHA256 sHA = SHA256.Create(); byte[] array = sHA.ComputeHash(bytes ?? Array.Empty<byte>()); StringBuilder stringBuilder = new StringBuilder(64); byte[] array2 = array; foreach (byte b in array2) { stringBuilder.Append(b.ToString("x2", CultureInfo.InvariantCulture)); } return stringBuilder.ToString(); } public static bool Matches(string expectedHash, byte[] current) { return string.Equals(expectedHash, Hash(current), StringComparison.OrdinalIgnoreCase); } } public static class Backups { private const string StampFormat = "yyyyMMdd-HHmmss"; private const string Suffix = ".bak"; public static string Name(string path, DateTime utc) { return path + "." + utc.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture) + ".bak"; } public static IReadOnlyList<string> ToDelete(string path, IEnumerable<string> existing, int keep) { string text = path + "."; List<(DateTime, string)> list = new List<(DateTime, string)>(); foreach (string item in existing ?? Enumerable.Empty<string>()) { if (item.StartsWith(text, StringComparison.Ordinal) && item.EndsWith(".bak", StringComparison.Ordinal) && DateTime.TryParseExact(item.Substring(text.Length, item.Length - text.Length - ".bak".Length), "yyyyMMdd-HHmmss", CultureInfo.InvariantCulture, DateTimeStyles.None, out var result)) { list.Add((result, item)); } } List<string> list2 = (from m in list.OrderBy<(DateTime, string), DateTime>(((DateTime Stamp, string Name) m) => m.Stamp).ThenBy<(DateTime, string), string>(((DateTime Stamp, string Name) m) => m.Name, StringComparer.Ordinal) select m.Name).ToList(); return list2.Take(Math.Max(0, list2.Count - Math.Max(0, keep))).ToList(); } public static bool FirstWriteThisSession(string path, ISet<string> writtenThisSession) { return !writtenThisSession.Contains(path); } } public static class YamlTransfer { public const int MaxBytes = 1048576; public const int ChunkBytes = 6144; public static IReadOnlyList<string> Pack(byte[] bytes) { return ConfigWire.Pack(bytes); } public static byte[] Unpack(IReadOnlyList<string> parts) { return ConfigWire.UnpackBytes(parts, 6144); } public static string Text(byte[] bytes) { return ConfigWire.Utf8.GetString(bytes); } public static byte[] Bytes(string text) { return ConfigWire.Utf8.GetBytes(text); } public static IReadOnlyList<string> Header(string token, string version, int total) { return new string[1] { "THING_YAML\t" + token + "\t" + version + "\t" + total.ToString(CultureInfo.InvariantCulture) }; } public static bool TryHeader(IReadOnlyList<string> lines, out string token, out string version, out int total) { token = (version = ""); total = 0; if (lines.Count != 1) { return false; } string[] array = lines[0].Split('\t'); if (array.Length != 4 || array[0] != "THING_YAML" || !MessageEditing.Version(array[1]) || !MessageEditing.Version(array[2]) || !int.TryParse(array[3], NumberStyles.None, CultureInfo.InvariantCulture, out total) || total < 0 || total > 1048576) { return false; } token = array[1]; version = array[2]; return true; } public static IReadOnlyList<string> Chunk(string token, int offset, byte[] bytes) { return new string[1] { "THING_YAML_CHUNK\t" + token + "\t" + offset.ToString(CultureInfo.InvariantCulture) }.Concat(Pack(bytes)).ToArray(); } public static bool TryChunk(IReadOnlyList<string> lines, string token, int offset, out byte[] bytes) { bytes = Array.Empty<byte>(); if (lines.Count < 2 || lines.Count > 16 || lines[0] != "THING_YAML_CHUNK\t" + token + "\t" + offset.ToString(CultureInfo.InvariantCulture)) { return false; } try { bytes = Unpack(lines.Skip(1).ToArray()); return true; } catch { return false; } } } public sealed class YamlUpload { private byte[]? bytes; private readonly string hash; public int Received { get; private set; } public int Length { get; } public YamlUpload(int length, string hash) { if (length < 0 || length > 1048576 || !MessageEditing.Version(hash)) { throw new ArgumentException(); } bytes = new byte[length]; Length = length; this.hash = hash; } public bool Append(int offset, byte[] chunk) { if (bytes == null || chunk == null || chunk.Length == 0 || chunk.Length > 6144 || offset != Received || chunk.Length > Length - Received) { return false; } Buffer.BlockCopy(chunk, 0, bytes, Received, chunk.Length); Received += chunk.Length; return true; } public bool Take(out string text) { text = ""; byte[] array = bytes; bytes = null; if (array == null || Received != Length || !FileVersion.Matches(hash, array)) { return false; } try { text = YamlTransfer.Text(array); return true; } catch { return false; } } } } namespace ExtraPotions.Thing.Moderation { public static class GameListIdentity { public static bool Valid(string text) { if (string.IsNullOrEmpty(text) || text.Length > 160) { return false; } if (text.All((char c) => c >= '0' && c <= '9')) { return true; } int num = text.IndexOf('_'); if (num > 0 && num < text.Length - 1 && text.Substring(0, num).All((char c) => (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'))) { return text.Substring(num + 1).All((char c) => char.IsLetterOrDigit(c) || c == '-' || c == '_' || c == ':' || c == '.'); } return false; } } public readonly struct Actor { public string Id { get; } public string Name { get; } public Actor(string id, string name) { Id = (id ?? "").Trim(); Name = name ?? ""; } } public enum LedgerChange { Unchanged, Added, Updated, Removed } public enum RecordKind { Ban, Mute, Warn } public sealed class LedgerRecord { public RecordKind Kind { get; } public Actor Target { get; } public Actor Admin { get; } public string Reason { get; } public DateTime AtUtc { get; } public DateTime? ExpiresUtc { get; } public LedgerRecord(RecordKind kind, Actor target, Actor admin, string reason, DateTime atUtc, DateTime? expiresUtc) { Kind = kind; Target = target; Admin = admin; Reason = reason ?? ""; AtUtc = atUtc; ExpiresUtc = expiresUtc; } public bool ActiveAt(DateTime nowUtc) { if (ExpiresUtc.HasValue) { return nowUtc < ExpiresUtc.Value; } return true; } } public sealed class ModerationLedger { private const string Header = "# Thing moderation ledger (managed by Thing on the server; one entry per line, tab-separated)"; private const string TimeFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"; private readonly Dictionary<string, LedgerRecord> bans = new Dictionary<string, LedgerRecord>(StringComparer.Ordinal); private readonly Dictionary<string, LedgerRecord> mutes = new Dictionary<string, LedgerRecord>(StringComparer.Ordinal); private readonly List<LedgerRecord> warnings = new List<LedgerRecord>(); public IReadOnlyDictionary<string, LedgerRecord> Bans => bans; public IReadOnlyDictionary<string, LedgerRecord> Mutes => mutes; public IReadOnlyList<LedgerRecord> Warnings => warnings; public LedgerChange Ban(Actor target, Actor admin, string reason, DateTime nowUtc, TimeSpan? duration) { return Put(bans, RecordKind.Ban, target, admin, reason, nowUtc, duration); } public LedgerChange Unban(string id) { if (!bans.Remove((id ?? "").Trim())) { return LedgerChange.Unchanged; } return LedgerChange.Removed; } public bool IsBanned(string id, DateTime nowUtc) { if (bans.TryGetValue((id ?? "").Trim(), out LedgerRecord value)) { return value.ActiveAt(nowUtc); } return false; } public IReadOnlyList<LedgerRecord> TakeExpired(DateTime nowUtc) { return Take(bans, nowUtc); } public LedgerChange Mute(Actor target, Actor admin, string reason, DateTime nowUtc, TimeSpan? duration) { return Put(mutes, RecordKind.Mute, target, admin, reason, nowUtc, duration); } public LedgerChange Unmute(string id) { if (!mutes.Remove((id ?? "").Trim())) { return LedgerChange.Unchanged; } return LedgerChange.Removed; } public bool IsMuted(string id, DateTime nowUtc) { if (mutes.TryGetValue((id ?? "").Trim(), out LedgerRecord value)) { return value.ActiveAt(nowUtc); } return false; } public IReadOnlyList<LedgerRecord> TakeExpiredMutes(DateTime nowUtc) { return Take(mutes, nowUtc); } public void Warn(Actor target, Actor admin, string reason, DateTime nowUtc) { Require(target); warnings.Add(new LedgerRecord(RecordKind.Warn, target, admin, Reasons.Normalize(reason), Second(nowUtc), null)); } public IReadOnlyList<LedgerRecord> WarningsFor(string id) { return warnings.Where((LedgerRecord w) => w.Target.Id == (id ?? "").Trim()).ToList(); } public string Save() { StringBuilder stringBuilder = new StringBuilder("# Thing moderation ledger (managed by Thing on the server; one entry per line, tab-separated)").Append('\n'); foreach (LedgerRecord item in bans.Values.OrderBy<LedgerRecord, string>((LedgerRecord r) => r.Target.Id, StringComparer.Ordinal)) { stringBuilder.Append(Line(item)).Append('\n'); } foreach (LedgerRecord item2 in mutes.Values.OrderBy<LedgerRecord, string>((LedgerRecord r) => r.Target.Id, StringComparer.Ordinal)) { stringBuilder.Append(Line(item2)).Append('\n'); } foreach (LedgerRecord warning in warnings) { stringBuilder.Append(Line(warning)).Append('\n'); } return stringBuilder.ToString(); } public static ModerationLedger Load(string? text, out int skipped) { ModerationLedger moderationLedger = new ModerationLedger(); skipped = 0; string[] array = (text ?? "").Split('\n'); for (int i = 0; i < array.Length; i++) { string text2 = array[i].TrimEnd('\r'); if (text2.Trim().Length == 0 || text2.StartsWith("#", StringComparison.Ordinal)) { continue; } if (!TryParse(text2, out LedgerRecord record) || record.Target.Id.Length == 0) { skipped++; continue; } switch (record.Kind) { case RecordKind.Ban: moderationLedger.bans[record.Target.Id] = record; break; case RecordKind.Mute: moderationLedger.mutes[record.Target.Id] = record; break; default: moderationLedger.warnings.Add(record); break; } } return moderationLedger; } private static LedgerChange Put(Dictionary<string, LedgerRecord> map, RecordKind kind, Actor target, Actor admin, string reason, DateTime nowUtc, TimeSpan? duration) { Require(target); if (duration.HasValue && duration.Value <= TimeSpan.Zero) { throw new ArgumentException("Expected a positive duration.", "duration"); } DateTime dateTime = Second(nowUtc); DateTime? expiresUtc = (duration.HasValue ? new DateTime?(dateTime + duration.Value) : ((DateTime?)null)); string text = Reasons.Normalize(reason); if (map.TryGetValue(target.Id, out LedgerRecord value)) { if (value.Reason == text && !value.ExpiresUtc.HasValue && !expiresUtc.HasValue) { return LedgerChange.Unchanged; } map[target.Id] = new LedgerRecord(kind, target, admin, text, dateTime, expiresUtc); return LedgerChange.Updated; } map[target.Id] = new LedgerRecord(kind, target, admin, text, dateTime, expiresUtc); return LedgerChange.Added; } private static IReadOnlyList<LedgerRecord> Take(Dictionary<string, LedgerRecord> map, DateTime nowUtc) { List<LedgerRecord> list = map.Values.Where((LedgerRecord r) => !r.ActiveAt(nowUtc)).OrderBy<LedgerRecord, string>((LedgerRecord r) => r.Target.Id, StringComparer.Ordinal).ToList(); foreach (LedgerRecord item in list) { map.Remove(item.Target.Id); } return list; } private static void Require(Actor target) { if (target.Id.Length == 0) { throw new ArgumentException("A moderation target needs a platform ID.", "target"); } } private static DateTime Second(DateTime utc) { return DateTime.SpecifyKind(new DateTime(utc.Ticks - utc.Ticks % 10000000), DateTimeKind.Utc); } private static string Time(DateTime utc) { return utc.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'", CultureInfo.InvariantCulture); } private static string Line(LedgerRecord r) { return string.Join("\t", r.Kind.ToString().ToLowerInvariant(), AuditEntry.Escape(r.Target.Id), AuditEntry.Escape(r.Target.Name), AuditEntry.Escape(r.Admin.Id), AuditEntry.Escape(r.Admin.Name), Time(r.AtUtc), r.ExpiresUtc.HasValue ? Time(r.ExpiresUtc.Value) : "-", AuditEntry.Escape(r.Reason)); } private static bool TryParse(string line, out LedgerRecord record) { record = null; string[] array = line.Split('\t'); if (array.Length != 8) { return false; } RecordKind recordKind; switch (array[0]) { case "ban": recordKind = RecordKind.Ban; break; case "mute": recordKind = RecordKind.Mute; break; case "warn": recordKind = RecordKind.Warn; break; default: return false; } if (!TryTime(array[5], out var utc)) { return false; } DateTime? expiresUtc = null; if (array[6] != "-") { if (recordKind == RecordKind.Warn || !TryTime(array[6], out var utc2)) { return false; } expiresUtc = utc2; } if (!AuditEntry.TryUnescape(array[1], out string value) || !AuditEntry.TryUnescape(array[2], out string value2) || !AuditEntry.TryUnescape(array[3], out string value3) || !AuditEntry.TryUnescape(array[4], out string value4) || !AuditEntry.TryUnescape(array[7], out string value5)) { return false; } record = new LedgerRecord(recordKind, new Actor(value, value2), new Actor(value3, value4), value5, utc, expiresUtc); return true; } private static bool TryTime(string text, out DateTime utc) { bool result = DateTime.TryParseExact(text, "yyyy-MM-dd'T'HH:mm:ss'Z'", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out utc); utc = DateTime.SpecifyKind(utc, DateTimeKind.Utc); return result; } } public enum DurationUnit { Minutes, Hours } public static class Durations { public static TimeSpan? Parse(string? text, DurationUnit defaultUnit, int maximum) { string text2 = (text ?? "").Trim().ToLowerInvariant(); if (text2.Length == 0) { return null; } char c = text2[text2.Length - 1]; string text3 = (char.IsDigit(c) ? text2 : text2.Substring(0, text2.Length - 1)); if (text3.Length == 0 || !text3.All(char.IsDigit) || !long.TryParse(text3, NumberStyles.None, CultureInfo.InvariantCulture, out var result) || result <= 0) { return null; } double val; switch ((char)((!char.IsDigit(c)) ? c : ((defaultUnit == DurationUnit.Hours) ? 104 : 109))) { case 'm': val = result; break; case 'h': val = (double)result * 60.0; break; case 'd': val = (double)result * 1440.0; break; default: return null; } double val2 = ((defaultUnit == DurationUnit.Hours) ? ((double)maximum * 60.0) : ((double)maximum)); return TimeSpan.FromMinutes(Math.Min(val, val2)); } public static string Describe(TimeSpan span) { long num = (long)Math.Max(0.0, Math.Round(span.TotalMinutes)); List<string> list = new List<string>(); if (num / 1440 > 0) { list.Add((num / 1440).ToString(CultureInfo.InvariantCulture) + "d"); } if (num / 60 % 24 > 0) { list.Add((num / 60 % 24).ToString(CultureInfo.InvariantCulture) + "h"); } if (num % 60 > 0 || list.Count == 0) { list.Add((num % 60).ToString(CultureInfo.InvariantCulture) + "m"); } return string.Join(" ", list); } } public static class Reasons { public const int MaxLength = 200; public static string Normalize(string? text) { StringBuilder stringBuilder = new StringBuilder(); bool flag = false; string text2 = (text ?? "").Trim(); foreach (char c in text2) { if (char.IsWhiteSpace(c) || char.IsControl(c)) { flag = true; continue; } if (flag && stringBuilder.Length > 0) { stringBuilder.Append(' '); } flag = false; stringBuilder.Append(c); } if (stringBuilder.Length <= 200) { return stringBuilder.ToString(); } return stringBuilder.ToString(0, 200); } public static bool Acceptable(string? text, bool required) { if (required) { return Normalize(text).Length > 0; } return true; } } public static class GameListFile { public static IReadOnlyList<string> Parse(string? text) { HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); List<string> list = new List<string>(); string[] array = (text ?? "").Split('\n'); for (int i = 0; i < array.Length; i++) { string text2 = array[i].Trim(); if (text2.Length != 0 && !text2.StartsWith("//", StringComparison.Ordinal) && hashSet.Add(text2)) { list.Add(text2); } } return list; } public static bool Contains(IEnumerable<string> list, string? id) { string wanted = (id ?? "").Trim(); if (wanted.Length == 0) { return false; } return list.Any((string entry) => Same(entry, wanted)); } private static bool Same(string a, string b) { if (string.Equals(a, b, StringComparison.Ordinal)) { return true; } int num = a.IndexOf('_'); int num2 = b.IndexOf('_'); if (num >= 0 && num2 >= 0) { return false; } string text = ((num >= 0) ? a.Substring(num + 1) : a); string b2 = ((num2 >= 0) ? b.Substring(num2 + 1) : b); if (text.Length > 0 && text.All(char.IsDigit)) { return string.Equals(text, b2, StringComparison.Ordinal); } return false; } } public readonly struct PlayerFreezeUpdate { public string PlayerId { get; } public long Peer { get; } public long Revision { get; } public bool Frozen { get; } public PlayerFreezeUpdate(string playerId, long peer, long revision, bool frozen) { PlayerId = playerId; Peer = peer; Revision = revision; Frozen = frozen; } } public sealed class PlayerFreezeSet { private sealed class State { internal PlayerFreezeUpdate Update; internal bool Acknowledged; internal double LastSent = double.NegativeInfinity; } private readonly Dictionary<string, State> players = new Dictionary<string, State>(StringComparer.Ordinal); private long revision; public void Retain(IEnumerable<KeyValuePair<string, long>> online) { Dictionary<string, long> dictionary = new Dictionary<string, long>(StringComparer.Ordinal); foreach (KeyValuePair<string, long> item in online) { if (!string.IsNullOrEmpty(item.Key) && item.Value != 0L) { dictionary[item.Key] = item.Value; } } string[] array = players.Keys.ToArray(); foreach (string key in array) { if (!dictionary.TryGetValue(key, out var value) || value != players[key].Update.Peer) { players.Remove(key); } } foreach (KeyValuePair<string, long> item2 in dictionary) { if (!players.ContainsKey(item2.Key)) { players.Add(item2.Key, new State { Update = new PlayerFreezeUpdate(item2.Key, item2.Value, ++revision, frozen: false) }); } } } public bool TrySet(string playerId, long peer, bool frozen, out PlayerFreezeUpdate update) { update = default(PlayerFreezeUpdate); if (!players.TryGetValue(playerId, out State value) || peer == 0L || value.Update.Peer != peer) { return false; } update = new PlayerFreezeUpdate(playerId, peer, ++revision, frozen); value.Update = update; value.Acknowledged = false; value.LastSent = double.NegativeInfinity; return true; } public bool IsCurrent(PlayerFreezeUpdate update) { if (players.TryGetValue(update.PlayerId, out State value) && value.Update.Peer == update.Peer) { return value.Update.Revision == update.Revision; } return false; } public bool Acknowledge(long peer, long receivedRevision, bool frozen) { foreach (State value in players.Values) { PlayerFreezeUpdate update = value.Update; if (update.Peer == peer && update.Revision == receivedRevision && update.Frozen == frozen && !value.Acknowledged) { value.Acknowledged = true; return true; } } return false; } public IReadOnlyList<PlayerFreezeUpdate> Due(double now) { List<PlayerFreezeUpdate> list = new List<PlayerFreezeUpdate>(); foreach (State value in players.Values) { if (!(now - value.LastSent < 1.0)) { value.LastSent = now; list.Add(value.Update); } } return list; } public void ClearAll() { players.Clear(); } public void Disconnect(string playerId) { players.Remove(playerId); } } public sealed class PlayerFreezeLease { public const double LeaseSeconds = 4.0; private long revision; private bool desired; private double expires; public bool IsFrozen(double now) { if (desired) { return now < expires; } return false; } public bool Receive(long receivedRevision, bool frozen, double now) { if (receivedRevision <= 0 || receivedRevision < revision || (receivedRevision == revision && frozen != desired)) { return false; } revision = receivedRevision; desired = frozen; expires = now + 4.0; return true; } public void Clear() { revision = 0L; desired = false; expires = 0.0; } } } namespace ExtraPotions.Thing.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 ThingTextKeys { public const string OffNoCrucible = "off_no_crucible"; public const string OffNoThing = "off_no_thing"; public const string ImportDone = "import_done"; public const string HintNotAdmin = "hint_not_admin"; public const string Kicked = "kicked"; public const string Banned = "banned"; public const string BannedFor = "banned_for"; public const string Warned = "warned"; public const string MutedFor = "muted_for"; public const string MutedUntilLifted = "muted_until_lifted"; public const string Unmuted = "unmuted"; public const string YouAreMuted = "you_are_muted"; public const string ShutdownWarning = "shutdown_warning"; public const string ShutdownSaving = "shutdown_saving"; public const string ShutdownCancelled = "shutdown_canceled"; public const string TimeReleased = "time_released"; public const string TempBanLifted = "temp_ban_lifted"; public const string NoReason = "no_reason"; public const string CommandHelp = "cmd_help"; public const string CommandDone = "cmd_done"; public const string CommandUnknownPlayer = "cmd_unknown_player"; public const string CommandUsage = "cmd_usage"; public const string CommandReasonRequired = "cmd_reason_required"; public const string CommandStatus = "cmd_status"; public const string CommandUnknownVerb = "cmd_unknown_verb"; public const string CommandFailed = "cmd_failed"; public const string CommandRemoteSent = "cmd_remote_sent"; public const string CommandRemoteFailed = "cmd_remote_failed"; public static IReadOnlyList<TextKey> All { get; } = Array.AsReadOnly(new TextKey[347] { new TextKey("time_unavailable", 0), new TextKey("time_set", 1), new TextKey("time_frozen", 1), new TextKey("time_resumed", 0), new TextKey("step_aside_worldtools", 1), new TextKey("off_no_crucible", 0), new TextKey("off_no_thing", 0), new TextKey("refused_not_admin", 0), new TextKey("refused_no_other_players", 0), new TextKey("refused_needs_delve", 0), new TextKey("refused_disabled_by_server", 0), new TextKey("refused_product_off", 0), new TextKey("refused_needs_relay", 0), new TextKey("import_done", 2), new TextKey("hint_not_admin", 0), new TextKey("kicked", 1), new TextKey("player_frozen", 0), new TextKey("player_unfrozen", 0), new TextKey("banned", 1), new TextKey("banned_for", 2), new TextKey("warned", 1), new TextKey("muted_for", 1), new TextKey("muted_until_lifted", 0), new TextKey("unmuted", 0), new TextKey("you_are_muted", 0), new TextKey("shutdown_warning", 1), new TextKey("shutdown_saving", 0), new TextKey("shutdown_canceled", 0), new TextKey("time_released", 0), new TextKey("temp_ban_lifted", 1), new TextKey("no_reason", 0), new TextKey("cmd_help", 0), new TextKey("cmd_done", 1), new TextKey("cmd_unknown_player", 1), new TextKey("cmd_usage", 1), new TextKey("cmd_reason_required", 0), new TextKey("cmd_status", 4), new TextKey("cmd_unknown_verb", 1), new TextKey("cmd_failed", 1), new TextKey("cmd_remote_sent", 0), new TextKey("cmd_remote_failed", 1), new TextKey("terrain_usage", 0), new TextKey("terrain_incomplete", 0), new TextKey("terrain_ownership", 0), new TextKey("terrain_other_pieces", 0), new TextKey("terrain_canceled", 0), new TextKey("terrain_expired", 0), new TextKey("terrain_preview", 1), new TextKey("terrain_done", 1), new TextKey("terrain_unknown", 0), new TextKey("drops_usage", 0), new TextKey("drops_unavailable", 0), new TextKey("drops_canceled", 0), new TextKey("drops_expired", 0), new TextKey("drops_preview", 2), new TextKey("drops_done", 1), new TextKey("drops_unknown", 0), new TextKey("seed_value", 1), new TextKey("seed_unavailable", 0), new TextKey("ward_usage", 0), new TextKey("ward_unavailable", 0), new TextKey("ward_on", 0), new TextKey("ward_off", 0), new TextKey("ui_close", 0), new TextKey("ui_refresh", 0), new TextKey("ui_previous", 0), new TextKey("ui_next", 0), new TextKey("ui_ledger", 0), new TextKey("ui_audit", 0), new TextKey("ui_kick", 0), new TextKey("ui_ban", 0), new TextKey("ui_tempban", 0), new TextKey("ui_warn", 0), new TextKey("ui_unban", 0), new TextKey("ui_select_ban", 0), new TextKey("ui_select_player", 0), new TextKey("ui_duration", 0), new TextKey("ui_reason", 0), new TextKey("ui_broadcast", 0), new TextKey("ui_text", 0), new TextKey("ui_shutdown", 0), new TextKey("ui_minutes", 0), new TextKey("ui_cancel_shutdown", 0), new TextKey("ui_save_world", 0), new TextKey("ui_world_step_aside", 0), new TextKey("ui_seed", 0), new TextKey("ui_time", 0), new TextKey("ui_clock", 0), new TextKey("ui_freeze", 0), new TextKey("config_unavailable", 0), new TextKey("config_bound", 0), new TextKey("config_yaml", 0), new TextKey("ui_config_yaml_hint", 0), new TextKey("config_yaml_saved", 0), new TextKey("config_unbound", 0), new TextKey("config_readonly", 0), new TextKey("config_invalid", 0), new TextKey("config_stale", 0), new TextKey("config_saved", 0), new TextKey("config_unknown", 0), new TextKey("config_failed", 0), new TextKey("ui_config_back", 0), new TextKey("ui_config_hint", 0), new TextKey("ui_settings_delegation", 0), new TextKey("ui_config_open", 0), new TextKey("ui_config_select", 0), new TextKey("ui_config_write_warning", 0), new TextKey("ui_config_restart", 0), new TextKey("ui_config_choose_value", 0), new TextKey("teleport_usage", 0), new TextKey("teleport_unavailable", 0), new TextKey("teleport_unknown", 0), new TextKey("teleport_expired", 0), new TextKey("teleport_declined", 0), new TextKey("teleport_busy", 0), new TextKey("teleport_waiting", 0), new TextKey("teleport_accepted", 0), new TextKey("teleport_prompt_title", 0), new TextKey("teleport_prompt_goto", 1), new TextKey("teleport_prompt_bring", 1), new TextKey("teleport_accept", 0), new TextKey("teleport_decline", 0), new TextKey("teleport_unsafe", 0), new TextKey("teleport_arrived", 0), new TextKey("teleport_recovering", 0), new TextKey("ui_goto", 0), new TextKey("ui_bring", 0), new TextKey("delve_unavailable", 0), new TextKey("delve_not_checked", 0), new TextKey("delve_counts", 5), new TextKey("delve_schedule", 4), new TextKey("delve_none_active", 0), new TextKey("delve_requested_skip", 1), new TextKey("delve_requested_due", 1), new TextKey("ui_delve_actions", 0), new TextKey("ui_delve_kinds", 0), new TextKey("ui_delve_request", 0), new TextKey("ui_delve_select", 0), new TextKey("ui_delve_request_warning", 0), new TextKey("ui_delve_dryrun", 0), new TextKey("ui_delve_dryrun_hint", 0), new TextKey("ui_delve_scope", 0), new TextKey("delve_kind_fullreset", 0), new TextKey("delve_kind_chests", 0), new TextKey("delve_kind_mudpiles", 0), new TextKey("delve_kind_frostcavecrystals", 0), new TextKey("delve_kind_blackcores", 0), new TextKey("delve_kind_morkhalla", 0), new TextKey("delve_kind_hole", 0), new TextKey("delve_kind_ancientarmor", 0), new TextKey("delve_kind_obsidian", 0), new TextKey("delve_kind_flametal", 0), new TextKey("delve_kind_ice", 0), new TextKey("delve_kind_queen", 0), new TextKey("delve_kind_campspawners", 0), new TextKey("delve_kind_dungeonspawners", 0), new TextKey("ui_top_left", 0), new TextKey("ui_mute", 0), new TextKey("ui_unmute", 0), new TextKey("ui_freeze_player", 0), new TextKey("ui_unfreeze_player", 0), new TextKey("ui_mute_duration", 0), new TextKey("mute_persistence_unknown", 0), new TextKey("ui_on", 0), new TextKey("ui_off", 0), new TextKey("ui_resume_time", 0), new TextKey("ui_ward_on", 0), new TextKey("ui_ward_off", 0), new TextKey("ui_terrain", 0), new TextKey("ui_drops", 0), new TextKey("ui_world_console_result", 0), new TextKey("ui_invalid", 0), new TextKey("ui_request_failed", 0), new TextKey("ui_pending", 0), new TextKey("ui_apply", 0), new TextKey("ui_cancel", 0), new TextKey("ui_confirm", 0), new TextKey("ui_hold", 0), new TextKey("ui_hold_progress", 1), new TextKey("ui_read_invalid", 0), new TextKey("ui_read_failed", 0), new TextKey("ui_page", 3), new TextKey("ui_audit_bounded", 0), new TextKey("ui_radius", 0), new TextKey("ui_tab_people", 0), new TextKey("ui_tab_moderation", 0), new TextKey("ui_tab_messages", 0), new TextKey("ui_tab_world", 0), new TextKey("ui_tab_regeneration", 0), new TextKey("ui_tab_server_config", 0), new TextKey("ui_players", 0), new TextKey("ui_permitted", 0), new TextKey("ui_admins", 0), new TextKey("ui_permit", 0), new TextKey("ui_unpermit", 0), new TextKey("ui_admin_add", 0), new TextKey("ui_admin_remove", 0), new TextKey("ui_platform_id", 0), new TextKey("ui_admin_readonly", 0), new TextKey("ui_select_identity", 0), new TextKey("message_saved", 0), new TextKey("message_stale", 0), new TextKey("message_invalid", 0), new TextKey("message_unknown", 0), new TextKey("message_failed", 0), new TextKey("ui_motd", 0), new TextKey("ui_motd_hint", 0), new TextKey("ui_announcements", 0), new TextKey("ui_announcement_hint", 0), new TextKey("ui_add", 0), new TextKey("ui_edit", 0), new TextKey("ui_remove", 0), new TextKey("ui_clear", 0), new TextKey("ui_announcement_every", 1), new TextKey("ui_add_announcement", 0), new TextKey("ui_edit_announcement", 0), new TextKey("ui_select_announcement", 0), new TextKey("ui_repeat_minutes", 0), new TextKey("ui_remove_announcement", 0), new TextKey("ui_clear_announcements_warning", 0), new TextKey("ui_delve_due", 0), new TextKey("ui_delve_nearest", 0), new TextKey("delve_position_unknown", 0), new TextKey("delve_site_requested", 0), new TextKey("delve_site_unknown", 0), new TextKey("delve_site_due", 0), new TextKey("ui_delve_site_scope", 0), new TextKey("delve_nearest_none", 0), new TextKey("delve_nearest_queued", 2), new TextKey("delve_nearest_refused", 0), new TextKey("ui_delve_reset_nearest", 0), new TextKey("ui_delve_reset_nearest_warning", 0), new TextKey("ui_delve_nature", 0), new TextKey("ui_delve_nature_scope", 0), new TextKey("pickup_usage", 0), new TextKey("pickup_set", 1), new TextKey("pickup_reset", 1), new TextKey("ui_pickup_radius", 0), new TextKey("ui_pickup_radius_hint", 0), new TextKey("ui_regenerate_nature", 0), new TextKey("ui_regenerate_nature_warning", 0), new TextKey("ui_reset_resources", 0), new TextKey("ui_reset_resources_warning", 0), new TextKey("ui_settings_delegation_hint", 0), new TextKey("ui_freeze_hint", 0), new TextKey("ui_top_left_hint", 0), new TextKey("ui_clock_hint", 0), new TextKey("ui_reason_hint", 0), new TextKey("ui_minutes_hint", 0), new TextKey("ui_platform_id_hint", 0), new TextKey("ui_duration_hint", 0), new TextKey("ui_terrain_radius_hint", 0), new TextKey("ui_message_hint", 0), new TextKey("ui_apply_hint", 0), new TextKey("ui_cancel_hint", 0), new TextKey("ui_help_close", 0), new TextKey("ui_help_refresh", 0), new TextKey("ui_help_previous", 0), new TextKey("ui_help_next", 0), new TextKey("ui_help_identity_lists", 0), new TextKey("ui_help_delve_views", 0), new TextKey("ui_help_moderation_views", 0), new TextKey("ui_help_select", 0), new TextKey("ui_help_tab_people", 0), new TextKey("ui_help_tab_moderation", 0), new TextKey("ui_help_tab_messages", 0), new TextKey("ui_help_tab_world", 0), new TextKey("ui_help_tab_regeneration", 0), new TextKey("ui_help_kick", 0), new TextKey("ui_help_ban", 0), new TextKey("ui_help_tempban", 0), new TextKey("ui_help_warn", 0), new TextKey("ui_help_mute", 0), new TextKey("ui_help_unmute", 0), new TextKey("ui_help_freeze_player", 0), new TextKey("ui_help_unfreeze_player", 0), new TextKey("ui_help_goto", 0), new TextKey("ui_help_bring", 0), new TextKey("ui_help_unban", 0), new TextKey("ui_help_permit", 0), new TextKey("ui_help_unpermit", 0), new TextKey("ui_help_admin_add", 0), new TextKey("ui_help_admin_remove", 0), new TextKey("ui_help_broadcast", 0), new TextKey("ui_help_shutdown", 0), new TextKey("ui_help_cancel_shutdown", 0), new TextKey("ui_help_save_world", 0), new TextKey("ui_help_motd", 0), new TextKey("ui_help_announcements", 0), new TextKey("ui_help_seed", 0), new TextKey("ui_help_time", 0), new TextKey("ui_help_resume_time", 0), new TextKey("ui_help_ward_on", 0), new TextKey("ui_help_ward_off", 0), new TextKey("ui_help_terrain", 0), new TextKey("ui_help_drops", 0), new TextKey("ui_help_add", 0), new TextKey("ui_help_edit", 0), new TextKey("ui_help_remove", 0), new TextKey("ui_grants", 0), new TextKey("ui_help_grants", 0), new TextKey("ui_grant_world", 0), new TextKey("ui_help_grant_world", 0), new TextKey("ui_grant_spawn", 0), new TextKey("ui_help_grant_spawn", 0), new TextKey("ui_grant_teleport", 0), new TextKey("ui_help_grant_teleport", 0), new TextKey("ui_grant_freeze", 0), new TextKey("ui_help_grant_freeze", 0), new TextKey("ui_grant_inventory", 0), new TextKey("ui_help_grant_inventory", 0), new TextKey("ui_grant_ward", 0), new TextKey("ui_help_grant_ward", 0), new TextKey("ui_inventory", 0), new TextKey("ui_help_inventory", 0), new TextKey("ui_inventory_empty", 0), new TextKey("ui_inventory_take", 0), new TextKey("ui_inventory_delete", 0), new TextKey("ui_inventory_take_body", 2), new TextKey("ui_inventory_delete_body", 2), new TextKey("ui_inventory_delete_again", 2), new TextKey("ui_inventory_stored", 0), new TextKey("ui_inventory_dropped", 0), new TextKey("ui_inventory_deleted", 0), new TextKey("ui_inventory_missing", 0), new TextKey("ui_inventory_busy", 0), new TextKey("ui_inventory_denied", 0), new TextKey("ui_inventory_failed", 0), new TextKey("ui_inventory_offline", 0), new TextKey("ui_select_item", 0), new TextKey("ui_permissions_loading", 0), new TextKey("ui_permissions_failed", 0), new TextKey("ui_permissions_empty", 0), new TextKey("ui_grant_add", 0), new TextKey("ui_help_grant_add", 0), new TextKey("ui_grant_remove", 0), new TextKey("ui_help_grant_remove", 0), new TextKey("grant_saved", 0), new TextKey("grant_unchanged", 0), new TextKey("grant_unsaved", 0), new TextKey("ui_grant_subadmin", 0), new TextKey("ui_help_grant_subadmin", 0), new TextKey("ui_subadmin_confirm_1", 1), new TextKey("ui_subadmin_confirm_2", 1), new TextKey("ui_subadmin_revoke_self", 0), new TextKey("ui_subadmin_revoke_self_hint", 0), new TextKey("ui_subadmin_revoke_self_confirm", 0) }); } } namespace ExtraPotions.Thing.Integration { public enum ThingModule { WorldTools } 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 ThingModule Module { get; } public IReadOnlyList<string> PluginNames { get; } public string ModuleId => Module.ToString().ToLowerInvariant(); internal ModuleSource(string guid, string name, ThingModule 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 ThingModule Module { get; } public string SourceName { get; } public string SourceGuid { get; } public string Key => KeyOf(Module); public StepAsideNotice(ThingModule module, string sourceName, string sourceGuid) { Module = module; SourceName = sourceName; SourceGuid = sourceGuid; } public static string KeyOf(ThingModule module) { return "step_aside_" + module.ToString().ToLowerInvariant(); } } public sealed class ModuleStepAsideDecision { private readonly HashSet<ThingModule> aside; public IReadOnlyList<ModuleSource> Present { get; } public bool Any => aside.Count > 0; public bool AdminToolLoaded => SteppedAside(ThingModule.WorldTools); public IReadOnlyList<StepAsideNotice> Notices { get; } internal ModuleStepAsideDecision(IReadOnlyList<ModuleSource> present) { Present = present; aside = new HashSet<ThingModule>(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(ThingModule module) { return aside.Contains(module); } } public static class ModuleStepAside { public static readonly ModuleSource AdminTool = new ModuleSource("com.icecub.valheimadmintool", "Valheim_Admin_Tool", ThingModule.WorldTools); public static IReadOnlyList<ModuleSource> Sources { get; } = Array.AsReadOnly(new ModuleSource[1] { AdminTool }); 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.Thing.Conversion { public readonly struct ImportValue { public string SettingId { get; } public string Value { get; } public string Source { get; } public ImportValue(string settingId, string value, string source) { SettingId = settingId; Value = value; Source = source; } } public sealed class ImportPlan { public IReadOnlyList<ImportValue> Values { get; } public IReadOnlyList<string> Notes { get; } internal ImportPlan(IReadOnlyList<ImportValue> values, IReadOnlyList<string> notes) { Values = values; Notes = notes; } public string Report() { return string.Join("\n", new string[1] { "Source: Valheim_Admin_Tool" }.Concat(Values.Select((ImportValue v) => "Set " + v.SettingId + " = " + v.Value + " (from " + v.Source + ")")).Concat(Notes.Select((string n) => "Note: " + n))); } } public readonly struct SourceEntry { public string Section { get; } public string Key { get; } public string Value { get; } public SourceEntry(string section, string key, string value) { Section = section; Key = key; Value = value; } } public static class SourceConfig { public static IReadOnlyList<SourceEntry> Parse(string? text) { List<SourceEntry> list = new List<SourceEntry>(); string section = ""; string[] array = (text ?? "").Split('\n'); for (int i = 0; i < array.Length; i++) { string text2 = array[i].Trim(); if (text2.Length == 0 || text2[0] == '#') { continue; } if (text2[0] == '[' && text2[text2.Length - 1] == ']') { section = text2.Substring(1, text2.Length - 2).Trim(); continue; } int num = text2.IndexOf('='); if (num > 0) { string text3 = text2.Substring(0, num).Trim(); if (text3.Length != 0) { list.Add(new SourceEntry(section, text3, text2.Substring(num + 1).Trim())); } } } return list; } } public static class SourceImport { public const string AdminToolFile = "com.icecub.valheimadmintool.cfg"; private const string HotkeySection = "Hotkeys"; public static IReadOnlyDictionary<string, string> Targets { get; } = new Dictionary<string, string>(StringComparer.Ordinal) { { "WardOverride", ThingSettings.Hotkeys.WardOverride.Id }, { "RemoveDrops", ThingSettings.Hotkeys.RemoveDrops.Id } }; public static ImportPlan Plan(string? text) { List<ImportValue> list = new List<ImportValue>(); List<string> list2 = new List<string>(); IReadOnlyList<SourceEntry> source = SourceConfig.Parse(text); foreach (SourceEntry item in source.Where((SourceEntry e) => e.Section == "Hotkeys")) { if (Targets.TryGetValue(item.Key, out string value) && item.Value.Length != 0 && !string.Equals(item.Value, "None", StringComparison.OrdinalIgnoreCase)) { list.Add(new ImportValue(value, item.Value, "[Hotkeys] " + item.Key)); } } if (source.Any((SourceEntry e) => e.Section == "Automatic resets")) { list2.Add("[Automatic resets]: dungeon and resource resets belong to Delve; its converter imports them"); } SourceEntry sourceEntry = source.FirstOrDefault((SourceEntry e) => e.Section == "General" && e.Key == "Toggle menu"); if (sourceEntry.Key != null) { list2.Add("[General] Toggle menu = " + sourceEntry.Value + ": not imported; that menu also holds the cheats (Hex), Thing keeps its own Admin Window Key"); } return new ImportPlan(list, list2); } } } namespace ExtraPotions.Thing.Config { public sealed class LockList { private readonly HashSet<string> locked; public static LockList Empty { get; } = new LockList(Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>()); public IReadOnlyList<string> Locked { get; } public IReadOnlyList<string> Unknown { get; } public IReadOnlyList<string> NotLockable { get; } private LockList(IReadOnlyList<string> lockedIds, IReadOnlyList<string> unknown, IReadOnlyList<string> notLockable) { Locked = lockedIds; Unknown = unknown; NotLockable = notLockable; locked = new HashSet<string>(lockedIds, StringComparer.OrdinalIgnoreCase); } public static LockList Parse(string? text) { List<string> list = new List<string>(); List<string> list2 = new List<string>(); List<string> list3 = new List<string>(); string[] array = (text ?? "").Split(','); for (int i = 0; i < array.Length; i++) { string text2 = array[i].Trim(); if (text2.Length != 0) { ThingSetting thingSetting = ThingSettings.Find(text2); List<string> list4 = ((thingSetting == null) ? list2 : (thingSetting.Lockable ? list : list3)); string text3 = thingSetting?.Id ?? text2; if (!list4.Contains<string>(text3, StringComparer.OrdinalIgnoreCase)) { list4.Add(text3); } } } if (list.Count + list2.Count + list3.Count != 0) { return new LockList(list, list2, list3); } return Empty; } public bool IsLocked(string? id) { if (id != null) { return locked.Contains(id.Trim()); } return false; } public string Describe() { string text = Locked.Count + " locked"; if (Unknown.Count > 0) { text = text + "; unknown: " + string.Join(", ", Unknown); } if (NotLockable.Count > 0) { text = text + "; not lockable: " + string.Join(", ", NotLockable); } return text; } } public static class LockedValues { public static string Encode(IEnumerable<KeyValuePair<string, string>> values) { if (values == null) { throw new ArgumentNullException("values"); } return string.Join(";", values.Select<KeyValuePair<string, string>, string>((KeyValuePair<string, string> v) => Escape(v.Key) + "=" + Escape(v.Value ?? ""))); } public static IReadOnlyDictionary<string, string> Decode(string? text) { Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); if (string.IsNullOrEmpty(text)) { return dictionary; } foreach (string item in Split(text, ';')) { List<string> list = Split(item, '=').ToList(); if (list.Count == 2) { string text2 = Unescape(list[0]); string text3 = Unescape(list[1]); if (!string.IsNullOrWhiteSpace(text2) && text3 != null && !dictionary.ContainsKey(text2.Trim())) { dictionary.Add(text2.Trim(), text3); } } } return dictionary; } public static string Compose(LockList list, Func<string, string?> ownText) { if (list == null) { throw new ArgumentNullException("list"); } if (ownText == null) { throw new ArgumentNullException("ownText"); } return Encode(from id in list.Locked select new KeyValuePair<string, string>(id, ownText(id)) into p where p.Value != null select new KeyValuePair<string, string>(p.Key, p.Value)); } private static string Escape(string text) { StringBuilder stringBuilder = new StringBuilder(text.Length); foreach (char c in text) { if (c == '\\' || c == ';' || c == '=') { stringBuilder.Append('\\'); } stringBuilder.Append(c); } return stringBuilder.ToString(); } private static IEnumerable<string> Split(string text, char separator) { int num = 0; for (int i = 0; i < text.Length; i++) { if (text[i] == '\\') { i++; } else if (text[i] == separator) { yield return text.Substring(num, i - num); num = i + 1; } } yield return text.Substring(num); } private static string? Unescape(string text) { StringBuilder stringBuilder = new StringBuilder(text.Length); for (int i = 0; i < text.Length; i++) { if (text[i] == '\\' && ++i >= text.Length) { return null; } stringBuilder.Append(text[i]); } return stringBuilder.ToString(); } } public readonly struct PlayerLock { public bool Locked { get; } public string? ServerText { get; } public PlayerLock(bool locked, string? serverText) { Locked = locked; ServerText = (locked ? serverText : null); } public override string ToString() { if (!Locked) { return "Own"; } return "Locked (" + ServerText + ")"; } } public static class PlayerLocks { public static PlayerLock Evaluate(string id, bool serverBinds, LockList list, IReadOnlyDictionary<string, string> values) { if (list == null) { throw new ArgumentNullException("list"); } if (values == null) { throw new ArgumentNullException("values"); } if (!serverBinds || !list.IsLocked(id) || !values.TryGetValue(id.Trim(), out string value)) { return new PlayerLock(locked: false, null); } return new PlayerLock(locked: true, value); } } public enum SessionKind { SinglePlayer, Host, DedicatedServer, RemoteClient } public enum ServerKind { Unknown, Vanilla, CrucibleWithoutThing, Thing } public enum BlockReason { None, SetByServer, VanillaServer, ProductDisabled, AuthorityPending } public readonly struct SessionFacts { public SessionKind Kind { get; } public ServerKind Server { get; } public bool Admin { get; } public bool IsAuthority => Kind != SessionKind.RemoteClient; public SessionFacts(SessionKind kind, ServerKind server, bool admin) { Kind = kind; Server = server; Admin = admin; } } public static class SettingAccess { public static BlockReason Decide(SettingScope scope, SessionFacts facts) { if (facts.IsAuthority || scope == SettingScope.Local) { return BlockReason.None; } switch (facts.Server) { case ServerKind.Unknown: return BlockReason.AuthorityPending; case ServerKind.Vanilla: return BlockReason.VanillaServer; case ServerKind.CrucibleWithoutThing: return BlockReason.ProductDisabled; default: if (!facts.Admin) { return BlockReason.SetByServer; } return BlockReason.None; } } } public enum SettingScope { Local, ServerAuthoritative } public abstract class ThingSetting { private static readonly IReadOnlyList<string> NoChoices = Array.Empty<string>(); public string Section { get; } public string Key { get; } public string Id => Section + "." + Key; public SettingScope Scope { get; } public string Description { get; } public IReadOnlyList<string> Choices { get; } public bool Hotkey { get; } public bool Lockable => Scope == SettingScope.Local; public abstract Type ValueType { get; } public abstract object DefaultValue { get; } public abstract object? Minimum { get; } public abstract object? Maximum { get; } private protected ThingSetting(string section, string key, SettingScope scope, string description, IReadOnlyList<string>? choices, bool hotkey) { 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 (!Enum.IsDefined(typeof(SettingScope), scope)) { throw new ArgumentException("Unknown scope.", "scope"); } if (string.IsNullOrWhiteSpace(description)) { throw new ArgumentException("Expected a description.", "description"); } if (hotkey && scope != SettingScope.Local) { throw new ArgumentException(section + "." + key + ": a keyboard shortcut is a Local setting.", "hotkey"); } Section = section; Key = key; Scope = scope; Description = description; Choices = choices ?? NoChoices; Hotkey = hotkey; } public abstract object NormalizeBoxed(object? value); public override string ToString() { return Id + " (" + Scope.ToString() + ")"; } public static ThingSetting<bool> Toggle(string section, string key, bool defaultValue, SettingScope scope, string description) { return new ThingSetting<bool>(section, key, scope, description, defaultValue, null, (bool v) => v); } public static ThingSetting<int> Integer(string section, string key, int defaultValue, int min, int max, SettingScope scope, string description) { if (min > max) { throw new ArgumentException("Minimum above maximum.", "min"); } return new ThingSetting<int>(section, key, scope, description, defaultValue, (min, max), (int v) => Math.Min(max, Math.Max(min, v))); } public static ThingSetting<TEnum> Choice<TEnum>(string section, string key, TEnum defaultValue, SettingScope scope, string description) where TEnum : struct, Enum { return new ThingSetting<TEnum>(section, key, scope, description, defaultValue, null, (TEnum v) => (!Enum.IsDefined(typeof(TEnum), v)) ? defaultValue : v, Enum.GetNames(typeof(TEnum))); } public static ThingSetting<string> Text(string section, string key, string defaultValue, SettingScope scope, string description, Func<string, string> normalize) { if (normalize == null) { throw new ArgumentNullException("normalize"); } return new ThingSetting<string>(section, key, scope, description, defaultValue, null, normalize); } public static ThingSetting<string> Shortcut(string section, string key, string defaultValue, SettingScope scope, string description) { return new ThingSetting<string>(section, key, scope, description, defaultValue, null, (string v) => (v ?? "").Trim(), null, hotkey: true); } } public sealed class ThingSetting<T> : ThingSetting { private readonly Func<T, T> normalize; private readonly (T Min, T Max)? range; public T Default { get; } public override Type ValueType => typeof(T); public override object DefaultValue => Default; public override object? Minimum { get { if (!range.HasValue) { return null; } return range.Value.Min; } } public override object? Maximum { get { if (!range.HasValue) { return null; } return range.Value.Max; } } internal ThingSetting(string section, string key, SettingScope scope, string description, T defaultValue, (T, T)? range, Func<T, T> normalize, IReadOnlyList<string>? choices = null, bool hotkey = false) : base(section, key, scope, description, choices, hotkey) { this.normalize = normalize ?? throw new ArgumentNullException("normalize"); this.range = range; if (!EqualityComparer<T>.Default.Equals(normalize(defaultValue), defaultValue)) { throw new ArgumentException(base.Id + ": the default is outside its own rule.", "defaultValue"); } Default = defaultValue; } public T Normalize(T value) { return normalize(value); } public override object NormalizeBoxed(object? value) { return (value is T value2) ? Normalize(value2) : Default; } } public enum RemoteConfigScope { MeadworksOnly, AllowList, AllConfigFiles } public static class ThingSettings { public static class General { public const string Section = "General"; public static readonly ThingSetting<string> AdminWindowKey = ThingSetting.Shortcut("General", "Admin Window Key", "", SettingScope.Local, "Opens and closes the Thing admin window (admins only; others see a short hint). Empty: none."); public static readonly ThingSetting<bool> ConfirmDestructive = ThingSetting.Toggle("General", "Confirm Destructive Actions", defaultValue: true, SettingScope.Local, "Asks before kick, ban, terrain reset, remove drops and remote configuration writes."); public static readonly ThingSetting<bool> AdminListEditable = ThingSetting.Toggle("General", "Admin List Editable In Game", defaultValue: false, SettingScope.ServerAuthoritative, "Lets existing admins add and remove admins in game. Off: adminlist.txt is changed only on the server."); public static readonly ThingSetting<bool> AllowSubAdminDelegation = ThingSetting.Toggle("General", "Allow Sub-Admin Delegation", defaultValue: true, SettingScope.ServerAuthoritative, "Allows the server host or current admin to delegate all non-destructive permissions to online players as sub-admin."); public static readonly ThingSetting<int> AuditLogDays = ThingSetting.Integer("General", "Audit Log Days", 30, 1, 365, SettingScope.ServerAuthoritative, "Days the audit log keeps its daily files."); public static readonly ThingSetting<bool> ShowPlatformIds = ThingSetting.Toggle("General", "Show Platform IDs", defaultValue: true, SettingScope.ServerAuthoritative, "Shows platform IDs in the player list (to admins only either way)."); public static readonly ThingSetting<string> LockedPlayerSettings = ThingSetting.Text("General", "Locked Player Settings", "", SettingScope.ServerAuthoritative, "Comma list of Local Thing settings (Section.Key) every player uses with the server's value.", NormalizeList); } public static class Moderation { public const string Section = "Moderation"; public static readonly ThingSetting<bool> KickReasonRequired = ThingSetting.Toggle("Moderation", "Kick Reason Required", defaultValue: false, SettingScope.ServerAuthoritative, "Kicks, bans and warnings need a reason."); public static readonly ThingS
plugins/Thing/Thing.dll
Decompiled 13 hours ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.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.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.Meadworks.Contracts; using ExtraPotions.Meadworks.Crucible; using ExtraPotions.Meadworks.Crucible.UI; using ExtraPotions.Thing.Admin; using ExtraPotions.Thing.Commands; using ExtraPotions.Thing.Communication; using ExtraPotions.Thing.Config; using ExtraPotions.Thing.ConfigEditing.Attributes; using ExtraPotions.Thing.ConfigEditing.Editors; using ExtraPotions.Thing.ConfigEditing.Values; using ExtraPotions.Thing.Conversion; using ExtraPotions.Thing.Integration; using ExtraPotions.Thing.Moderation; using ExtraPotions.Thing.RemoteConfig; using ExtraPotions.Thing.Runtime.ConfigEditing; using ExtraPotions.Thing.UI; using ExtraPotions.Thing.WorldAdmin; using HarmonyLib; using Jotunn.Entities; using Jotunn.Managers; using Jotunn.Utils; using Microsoft.CodeAnalysis; using TMPro; using UnityEngine; using UnityEngine.EventSystems; 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+source.C5CB98230C177B26BBB9D41F4869FF0DA5DE81F8C31B5A5A34E1D7BBF025C5D7")] [assembly: AssemblyProduct("Thing")] [assembly: AssemblyTitle("Thing")] [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.Thing.Runtime { public sealed class ThingCommand : ConsoleCommand { public override string Name => "thing"; public override string Help => "Thing server administration: help, status, kick, ban, tempban, unban, warn, mute, unmute, freeze <player>, unfreeze <player>, say, saytop, shutdown, cancelshutdown, save, permitted [offset], permit <platform id>, unpermit <platform id>, admins [offset], admin add|remove <platform id>, grant self, grant self remove, grant <platform id> <world|spawn|teleport|freeze|inventory|ward|subadmin> <on|off>, seed, time <HH:mm|preset> [freeze], resumetime, ward [on|off], terrain <radius>, terrain confirm, terrain cancel, drops, drops confirm, drops cancel, pickup <1-10|reset>, regenerate nature, resetresources."; public override void Run(string[] args) { //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Invalid comparison between Unknown and I4 //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Invalid comparison between Unknown and I4 //IL_00bc: Unknown result type (might be due to invalid IL or missing references) if (ThingTeleportCommands.Run(args) || ThingTerrainCommands.Run(args) || ThingDropCommands.Run(args) || ThingWard.Run(args) || ThingPickup.Run(args)) { return; } ZNet instance = ZNet.instance; ParsedCommand val = CommandParser.Parse((IReadOnlyList<string>)args); if ((int)val.Kind == 2 && (int)val.Action == 32 && !ThingModules.Active((ThingModule)0)) { Console instance2 = Console.instance; if (instance2 != null) { instance2.Print(ThingText.Text(AdminCheck.TextKey((Verdict)6))); } return; } if ((Object)(object)instance != (Object)null && !instance.IsServer() && RemoteAdmin.Sendable(val) && ThingRemoteCommands.Available && ThingModules.ProductActive(out var _)) { ThingRemoteCommands.Send(args); Console instance3 = Console.instance; if (instance3 != null) { instance3.Print(ThingText.Text("cmd_remote_sent")); } return; } foreach (string item in Execute(args, Admin(), fromLocalAuthority: true, requesterIsAdmin: true)) { Console instance4 = Console.instance; if (instance4 != null) { instance4.Print(item); } } } public static IEnumerable<string> Execute(IReadOnlyList<string> args, Actor admin, bool fromLocalAuthority, bool requesterIsAdmin, string requesterConnection = "") { //IL_000f: 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) ParsedCommand val = CommandParser.Parse(args); CommandKind kind = val.Kind; switch ((int)kind) { case 0: yield return ThingText.Text("cmd_help"); yield break; case 4: yield return ThingText.Format("cmd_unknown_verb", val.Text); yield break; case 3: yield return ThingText.Format("cmd_usage", val.Text); yield break; } ThingConfig settings = Plugin.Settings; if (settings == null || !ThingModules.ProductActive(out var _) || (Object)(object)ZNet.instance == (Object)null) { yield return ThingText.Text(AdminCheck.TextKey((Verdict)6)); yield break; } if (!ZNet.instance.IsServer() || !ThingServer.Active) { yield return ThingText.Text(AdminCheck.TextKey((Verdict)7)); yield break; } if ((int)val.Kind == 2 && (int)val.Action == 38 && val.Text == "self") { bool flag = fromLocalAuthority || requesterIsAdmin; yield return AdminPermissions.Wire(flag, (AdminRight)(flag ? 127 : ((int)ThingAccess.Of(((Actor)(ref admin)).Id)))); yield break; } if (!fromLocalAuthority && !requesterIsAdmin && (int)val.Kind == 1 && args.Count >= 2 && string.Equals(args[0], "ui-read", StringComparison.OrdinalIgnoreCase) && string.Equals(args[1], "players", StringComparison.OrdinalIgnoreCase) && AdminPermissions.Covers(ThingAccess.Of(((Actor)(ref admin)).Id), (ThingAction)39) && ThingUiQueries.TryRead(args, admin, fromLocalAuthority, requesterIsAdmin, out IReadOnlyList<string> lines, playersOnly: true)) { foreach (string item in lines) { yield return item; } yield break; } RequestFacts val2 = default(RequestFacts); ((RequestFacts)(ref val2))..ctor(Role(), fromLocalAuthority, requesterIsAdmin, ThingDelve.Present, false, (AdminRight)((!(fromLocalAuthority || requesterIsAdmin)) ? ((int)ThingAccess.Of(((Actor)(ref admin)).Id)) : 0)); if (!ActionCatalog.Visible(val.Action, ThingModules.StepAside.AdminToolLoaded)) { yield return ThingText.Text(AdminCheck.TextKey((Verdict)6)); yield break; } Verdict val3 = AdminCheck.Decide(val.Action, val2, settings.Rules()); if (!AdminCheck.Permits(val3)) { yield return ThingText.Text(AdminCheck.TextKey(val3)); yield break; } if (ThingUiQueries.TryRead(args, admin, fromLocalAuthority, requesterIsAdmin, out IReadOnlyList<string> lines2)) { foreach (string item2 in lines2) { yield return item2; } yield break; } if ((int)val.Kind == 1) { ModerationLedger ledger = ThingServer.Ledger; yield return ThingText.Format("cmd_status", ledger.Bans.Count, ledger.Bans.Values.Count((LedgerRecord b) => b.ExpiresUtc.HasValue), ledger.Mutes.Count, ledger.Warnings.Count); yield break; } if (((int)val.Action == 1 || (int)val.Action == 2 || (int)val.Action == 3) && !Reasons.Acceptable(val.Text, settings.Get<bool>(Moderation.KickReasonRequired))) { yield return ThingText.Text("cmd_reason_required"); yield break; } IReadOnlyList<OnlinePlayer> source = ThingServer.Online(); OnlinePlayer? online = null; if (val.Target.Length > 0 && val.Target != "self") { Actor? found = PlayerMatch.Find(val.Target, source.Select((OnlinePlayer o) => o.Actor)); if (found.HasValue) { online = source.First(delegate(OnlinePlayer o) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //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) Actor val6 = o.Actor; string id = ((Actor)(ref val6)).Id; val6 = found.Value; return id == ((Actor)(ref val6)).Id; }); } } ThingAction action = val.Action; Actor target; switch (action - 1) { case 33: case 34: if (val.ConfigFile == null) { yield return ThingText.Text("config_invalid"); break; } foreach (string item3 in ThingConfigFiles.Run(val.ConfigFile, admin, fromLocalAuthority, requesterConnection)) { yield return item3; } break; case 4: case 5: case 14: case 15: case 36: foreach (string item4 in ThingLists.Run(val, admin, fromLocalAuthority)) { yield return item4; } break; case 37: if (val.Target == "self") { val = new ParsedCommand(val.Kind, val.Action, ((Actor)(ref admin)).Id, val.Text, (TimeSpan?)null, 0, false, (MessageEditRequest)null, false, false, (RegenerationRequest)null, (ConfigEditRequest)null); } foreach (string item5 in ThingAccess.Edit(val, admin, fromLocalAuthority || requesterIsAdmin)) { yield return item5; } break; case 17: case 18: if (val.Message == null) { yield return ThingText.Text("message_invalid"); break; } foreach (string item6 in ThingMessages.Run(val.Message, admin, fromLocalAuthority)) { yield return item6; } break; case 24: case 25: case 26: case 27: foreach (string item7 in ThingDelve.Run(val, admin, fromLocalAuthority)) { yield return item7; } break; case 22: case 23: yield return ThingTime.Run(val, admin, fromLocalAuthority); break; case 0: case 12: if (!online.HasValue) { yield return ThingText.Format("cmd_unknown_player", val.Target); break; } if ((int)val.Action == 1) { ThingServer.Kick(online.Value, admin, val.Text); } else { ThingServer.Warn(online.Value, admin, val.Text); } yield return Done(val.Action, online.Value.Actor); break; case 1: case 2: { Actor? val4 = (online.HasValue ? new Actor?(online.GetValueOrDefault().Actor) : (LooksLikeId(val.Target) ? new Actor?(new Actor(val.Target, "")) : ((Actor?)null))); if (!val4.HasValue) { yield return ThingText.Format("cmd_unknown_player", val.Target); } else { ThingServer.Ban(val4.Value, online, admin, val.Text, ((int)val.Action == 3) ? val.Duration : ((TimeSpan?)null)); yield return Done(val.Action, val4.Value); } break; } case 3: ThingServer.Unban(val.Target, admin); yield return Done(val.Action, new Actor(val.Target, "")); break; case 8: if (!online.HasValue) { yield return ThingText.Format("cmd_unknown_player", val.Target); } else { TimeSpan? duration = (val.UntilLifted ? ((TimeSpan?)null) : new TimeSpan?(val.Duration ?? TimeSpan.FromMinutes(settings.Get<int>(Moderation.MuteDefaultMinutes)))); yield return MuteReply(ThingServer.Mute(online.Value, admin, val.Text, duration), val.Action, online.Value.Actor); } break; case 10: case 11: { if (!online.HasValue) { yield return ThingText.Format("cmd_unknown_player", val.Target); break; } string text; if (!ThingServer.SetFrozen(online.Value, admin, (int)val.Action == 11)) { object[] array = new object[1]; string verb = ActionCatalog.Info(val.Action).Verb; target = online.Value.Actor; array[0] = verb + " " + ((Actor)(ref target)).Id; text = ThingText.Format("cmd_failed", array); } else { text = ThingText.Text("ui_pending"); } yield return text; break; } case 9: { Actor? val5 = (online.HasValue ? new Actor?(online.GetValueOrDefault().Actor) : (LooksLikeId(val.Target) ? new Actor?(new Actor(val.Target, "")) : ((Actor?)null))); if (!val5.HasValue) { yield return ThingText.Format("cmd_unknown_player", val.Target); } else { yield return MuteReply(ThingServer.Unmute(val5.Value, online, admin), val.Action, val5.Value); } break; } case 16: yield return ThingText.Format(ThingServer.Say(admin, val.Text, val.TopLeft) ? "cmd_done" : "cmd_failed", val.TopLeft ? "saytop" : "say"); break; case 19: ThingServer.StartShutdown(admin, val.Minutes); yield return ThingText.Format("cmd_done", "shutdown " + val.Minutes); break; case 20: yield return ThingServer.CancelShutdown(admin) ? ThingText.Format("cmd_done", "cancelshutdown") : ThingText.Format("cmd_failed", "no shutdown is running"); break; case 21: ThingServer.SaveWorld(admin); yield return ThingText.Format("cmd_done", "save"); break; case 31: { World world = ZNet.instance.GetWorld(); if (world == null) { yield return ThingText.Text("seed_unavailable"); break; } Actor admin2 = admin; ThingAction action2 = val.Action; target = default(Actor); ThingServer.Audit(admin2, action2, target, (AuditOutcome)0, "world seed requested"); yield return ThingText.Format("seed_value", world.m_seedName); break; } default: yield return ThingText.Format("cmd_unknown_verb", ActionCatalog.Info(val.Action).Verb); break; } } private static string MuteReply(MuteWriteResult result, ThingAction action, Actor target) { //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) return result switch { MuteWriteResult.LiveOnly => ThingText.Text("mute_persistence_unknown"), MuteWriteResult.Saved => Done(action, target), _ => ThingText.Text("refused_product_off"), }; } private static string Done(ThingAction action, Actor target) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) return ThingText.Format("cmd_done", ActionCatalog.Info(action).Verb + " " + ((((Actor)(ref target)).Name.Length > 0) ? (((Actor)(ref target)).Name + " (" + ((Actor)(ref target)).Id + ")") : ((Actor)(ref target)).Id)); } private static bool LooksLikeId(string text) { if (text.Length > 0) { if (text.IndexOf('_') <= 0) { return text.All(char.IsDigit); } return true; } return false; } private static ThingRole Role() { //IL_000f: Unknown result type (might be due to invalid IL or missing references) //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_0017: Invalid comparison between Unknown and I4 //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Invalid comparison between Unknown and I4 SessionRole val = (SessionRole)((!Plugin.IsReady) ? 1 : ((int)Plugin.Session.Role)); if ((int)val != 2) { if ((int)val == 4) { return (ThingRole)2; } return (ThingRole)0; } return (ThingRole)1; } private static Actor Admin() { //IL_0045: Unknown result type (might be due to invalid IL or missing references) string text = (((Object)(object)Player.m_localPlayer != (Object)null) ? Player.m_localPlayer.GetPlayerName() : "console"); return new Actor(((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsDedicated()) ? "server" : "host", text); } } public static class ThingRemoteCommands { private sealed class Codec : IOperationCodec<IReadOnlyList<string>, IReadOnlyList<string>> { public byte[] EncodeRequest(IReadOnlyList<string> request) { return RemoteAdmin.EncodeRequest(request); } public IReadOnlyList<string> DecodeRequest(byte[] bytes) { return RemoteAdmin.DecodeRequest(bytes); } public byte[] EncodeReply(IReadOnlyList<string> reply) { return RemoteAdmin.EncodeReply((IEnumerable<string>)reply); } public IReadOnlyList<string> DecodeReply(byte[] bytes) { return RemoteAdmin.DecodeReply(bytes); } } private static IDisposable? operation; private static readonly List<Task<OperationResult<IReadOnlyList<string>>>> pending = new List<Task<OperationResult<IReadOnlyList<string>>>>(); private static MeadworksLog? log; public static bool Available => operation != null; public static void Initialize(ProductRegistration registration, MeadworksLog logger) { //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Expected O, but got Unknown log = logger; operation = Plugin.Services.Operations.Register<IReadOnlyList<string>, IReadOnlyList<string>>(registration, new OperationDescriptor("meadworks.thing.admin_command", RemoteAdmin.OperationVersion), (IOperationCodec<IReadOnlyList<string>, IReadOnlyList<string>>)(object)new Codec(), (Func<AuthorityContext, IReadOnlyList<string>, OperationExecution<IReadOnlyList<string>>>)Apply, (Func<RequestIdentity, OperationExecution<IReadOnlyList<string>>>)null); } public static void Send(string[] args) { pending.Add(Plugin.RequestClient.InvokeAsync<IReadOnlyList<string>, IReadOnlyList<string>>("meadworks.thing.admin_command", (IReadOnlyList<string>)args.Where((string a) => !string.IsNullOrWhiteSpace(a)).ToArray(), CancellationToken.None)); } public static void Tick() { //IL_0071: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Invalid comparison between Unknown and I4 if (pending.Count == 0) { return; } Task<OperationResult<IReadOnlyList<string>>>[] array = pending.ToArray(); foreach (Task<OperationResult<IReadOnlyList<string>>> task in array) { if (!task.IsCompleted) { continue; } pending.Remove(task); IEnumerable<string> enumerable; if (task.Status != TaskStatus.RanToCompletion) { enumerable = new string[1] { ThingText.Format("cmd_remote_failed", "Faulted") }; } else { OperationResult<IReadOnlyList<string>> result = task.Result; if ((int)result.State == 2) { enumerable = result.Reply; } else { string text = RemoteAdmin.TextKey(result.Code); enumerable = new string[1] { (text == "cmd_remote_failed") ? ThingText.Format(text, result.Code) : ThingText.Text(text) }; } } foreach (string item in enumerable) { Console instance = Console.instance; if (instance != null) { instance.Print(item); } } } } public static void Shutdown() { operation?.Dispose(); operation = null; pending.Clear(); log = null; } private static OperationExecution<IReadOnlyList<string>> Apply(AuthorityContext context, IReadOnlyList<string> args) { //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_0077: 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_0083: Unknown result type (might be due to invalid IL or missing references) //IL_0089: Invalid comparison between Unknown and I4 //IL_008d: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Invalid comparison between Unknown and I4 //IL_00ae: Unknown result type (might be due to invalid IL or missing references) //IL_00b4: Invalid comparison between Unknown and I4 //IL_00b6: Unknown result type (might be due to invalid IL or missing references) //IL_00ba: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Unknown result type (might be due to invalid IL or missing references) //IL_00d1: Invalid comparison between Unknown and I4 //IL_01a7: Unknown result type (might be due to invalid IL or missing references) //IL_0104: Unknown result type (might be due to invalid IL or missing references) //IL_017c: Unknown result type (might be due to invalid IL or missing references) //IL_017e: Unknown result type (might be due to invalid IL or missing references) //IL_0185: Unknown result type (might be due to invalid IL or missing references) //IL_018b: Unknown result type (might be due to invalid IL or missing references) ZNet instance = ZNet.instance; ConfigBlockReason reason; bool flag = Plugin.Settings != null && (Object)(object)instance != (Object)null && instance.IsServer() && ThingServer.Active && ThingModules.ProductActive(out reason); string hostName; Actor admin = Caller(context, out hostName); bool flag2 = context.IsLocal || (context.IsAdmin && hostName.Length > 0 && (Object)(object)instance != (Object)null && instance.IsAdmin(hostName)); ParsedCommand val = CommandParser.Parse(args); AdminRight val2 = (AdminRight)((!flag2) ? ((int)ThingAccess.Of(hostName)) : 0); bool flag3 = (int)val.Kind == 2 && (int)val.Action == 38 && val.Text == "self"; bool flag4 = (int)val.Kind == 2 && AdminPermissions.Covers(val2, val.Action); bool flag5 = (int)val.Kind == 1 && args.Count >= 2 && string.Equals(args[0], "ui-read", StringComparison.OrdinalIgnoreCase) && string.Equals(args[1], "players", StringComparison.OrdinalIgnoreCase) && AdminPermissions.Covers(val2, (ThingAction)39); string text = RemoteAdmin.Refusal(flag, flag2 || flag3 || flag4 || flag5); if (text != null) { MeadworksLog? obj = log; if (obj != null) { obj.Info("Thing remote command from " + ((Actor)(ref admin)).Id + " refused: " + text + "."); } if (flag && text == "NotAdmin") { ThingServer.Audit(admin, ActionOf(args), default(Actor), (AuditOutcome)1, "remote: not admin"); } return OperationExecution<IReadOnlyList<string>>.Rejected(text); } context.MarkDecisionMade(); return OperationExecution<IReadOnlyList<string>>.Success((IReadOnlyList<string>)ThingCommand.Execute(args, admin, context.IsLocal, flag2, context.SenderId).ToList()); } private static Actor Caller(AuthorityContext context, out string hostName) { //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0038: 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) hostName = ""; if (context.IsLocal) { return new Actor("host", ((Object)(object)Player.m_localPlayer != (Object)null) ? Player.m_localPlayer.GetPlayerName() : "console"); } ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !long.TryParse(context.SenderId, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var uid)) { return new Actor("peer:" + context.SenderId, ""); } ZNetPeer val = ((IEnumerable<ZNetPeer>)instance.GetPeers()).FirstOrDefault((Func<ZNetPeer, bool>)((ZNetPeer p) => p != null && p.m_uid == uid)); object obj; if (val == null) { obj = null; } else { ISocket socket = val.m_socket; obj = ((socket != null) ? socket.GetHostName() : null); } if (obj == null) { obj = ""; } hostName = (string)obj; return new Actor((hostName.Length > 0) ? hostName : ("peer:" + context.SenderId), val?.m_playerName ?? ""); } private static ThingAction ActionOf(IReadOnlyList<string> args) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Invalid comparison between Unknown and I4 //IL_0013: Unknown result type (might be due to invalid IL or missing references) ParsedCommand val = CommandParser.Parse(args); if ((int)val.Kind != 2) { return (ThingAction)0; } return val.Action; } } public sealed class ThingConfig { public const string LockedValuesKey = "Locked Player Values"; public const string AdminSettingsGrantKey = "Allow Admin Settings Editing"; public const string AdminSettingsStatusKey = "Admin Settings Editing Status"; private const string Product = "com.meadworks.thing"; private const float RefreshSeconds = 0.5f; private readonly ConfigFile file; private readonly MeadworksLog log; private readonly Dictionary<ThingSetting, ConfigEntryBase> entries = new Dictionary<ThingSetting, ConfigEntryBase>(); private readonly HashSet<ThingSetting> trackedLocks = new HashSet<ThingSetting>(); private readonly ThingSyncReceipt syncReceipt = new ThingSyncReceipt(); private readonly ConfigEntry<string> lockedValues; private readonly ConfigEntry<bool> settingsGrant; private readonly ConfigEntry<bool> settingsStatus; private string? lockListText; private LockList lockList = LockList.Empty; private string? valuesText; private IReadOnlyDictionary<string, string> values = new Dictionary<string, string>(); private float nextRefresh; public ConfigFile File => file; public bool AllowAdminSettingsEditing { get { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Invalid comparison between Unknown and I4 if (!Plugin.IsReady || (int)Plugin.Session.Role != 3) { return settingsGrant.Value; } if (ServerBinds()) { return settingsStatus.Value; } return false; } } public LockList Locks => lockList; public event Action? Changed; private ThingConfig(ConfigFile file, MeadworksLog log) { this.file = file; this.log = log; bool saveOnConfigSet = file.SaveOnConfigSet; file.SaveOnConfigSet = false; try { foreach (ThingSetting item in ThingSettings.All) { BindSetting(item); } settingsGrant = MeadworksConfig.BindHidden<bool>(file, "General", "Allow Admin Settings Editing", false, "Host-only: allow listed server admins to edit settings. This local server policy is never synchronized from clients.", (ConfigScope)0, (AcceptableValueBase)null); settingsStatus = MeadworksConfig.BindHidden<bool>(file, "General", "Admin Settings Editing Status", false, "Managed by Thing: display-only projection of the host's settings delegation.", (ConfigScope)1, (AcceptableValueBase)null); lockedValues = MeadworksConfig.BindHidden<string>(file, "General", "Locked Player Values", "", "Managed by Thing: the server's values for Locked Player Settings. Do not edit.", (ConfigScope)1, (AcceptableValueBase)null); } finally { file.SaveOnConfigSet = saveOnConfigSet; } file.Save(); SynchronizationManager.OnConfigurationSynchronized += OnSynchronized; } public bool IsSettingsPolicy(ConfigEntryBase entry) { if ((object)entry != settingsGrant) { return (object)entry == settingsStatus; } return true; } public bool SetAdminSettingsEditing(bool enabled, long expectedGeneration) { if (!Plugin.IsReady || !Plugin.Session.AuthorityReady || !Plugin.Session.IsAuthority || Plugin.Session.Generation != expectedGeneration || (Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return false; } settingsGrant.Value = enabled; file.Save(); PublishSettingsAccess(); return true; } private void PublishSettingsAccess() { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Invalid comparison between Unknown and I4 if (Plugin.IsReady && (int)Plugin.Session.Role != 3 && settingsStatus.Value != settingsGrant.Value) { MeadworksConfig.Amend<bool>(settingsStatus, "com.meadworks.thing", (ConfigScope)1, settingsGrant.Value, (string)null, true); } } public static ThingConfig Bind(ConfigFile file, MeadworksLog log) { //IL_0070: 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) if (file == null) { throw new ArgumentNullException("file"); } if (log == null) { throw new ArgumentNullException("log"); } ThingConfig thingConfig = new ThingConfig(file, log); foreach (KeyValuePair<ThingSetting, ConfigEntryBase> item in thingConfig.entries.Where<KeyValuePair<ThingSetting, ConfigEntryBase>>((KeyValuePair<ThingSetting, ConfigEntryBase> p) => (int)p.Key.Scope > 0)) { MeadworksBlockedOptions.Track("com.meadworks.thing", item.Value, Map(item.Key.Scope), true); } file.SettingChanged += thingConfig.OnSettingChanged; file.ConfigReloaded += thingConfig.OnReloaded; thingConfig.Refresh(force: true); log.Verbose("Configuration bound: " + ThingSettings.All.Count((ThingSetting s) => (int)s.Scope == 0) + " Local, " + ThingSettings.All.Count((ThingSetting s) => (int)s.Scope == 1) + " ServerAuthoritative, no cheats."); return thingConfig; } public static ConfigScope Map(SettingScope scope) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) if ((int)scope == 0) { return (ConfigScope)0; } return (ConfigScope)1; } public ConfigEntryBase Entry(ThingSetting setting) { if (!entries.TryGetValue(setting ?? throw new ArgumentNullException("setting"), out ConfigEntryBase value)) { throw new ArgumentException("Not a Thing setting.", "setting"); } return value; } public T Get<T>(ThingSetting<T> setting) { //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Invalid comparison between Unknown and I4 //IL_0055: 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) if (((ThingSetting)setting).Hotkey) { throw new ArgumentException(((ThingSetting)setting).Id + " is a keyboard shortcut; use Shortcut.", "setting"); } ConfigEntry<T> val = (ConfigEntry<T>)(object)Entry((ThingSetting)(object)setting); if ((int)((ThingSetting)setting).Scope == 1) { return setting.Normalize(MeadworksConfig.Effective<T>(val, "com.meadworks.thing", (ConfigScope)1, setting.Default, true)); } PlayerLock val2 = Lock((ThingSetting)(object)setting); if (((PlayerLock)(ref val2)).Locked && TryConvert<T>(((PlayerLock)(ref val2)).ServerText, out var value)) { return setting.Normalize(value); } return setting.Normalize(val.Value); } public ConfigEntry<KeyboardShortcut> Shortcut(ThingSetting<string> setting) { if (!((ThingSetting)setting).Hotkey) { throw new ArgumentException(((ThingSetting)setting).Id + " is not a keyboard shortcut.", "setting"); } return (ConfigEntry<KeyboardShortcut>)(object)Entry((ThingSetting)(object)setting); } public KeyboardShortcut ShortcutValue(ThingSetting<string> setting) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_002c: 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_0025: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) ConfigEntry<KeyboardShortcut> val = Shortcut(setting); PlayerLock val2 = Lock((ThingSetting)(object)setting); if (((PlayerLock)(ref val2)).Locked) { try { return KeyboardShortcut.Deserialize(((PlayerLock)(ref val2)).ServerText); } catch (Exception) { } } return val.Value; } public bool IsLocked(ThingSetting setting) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) if (setting != null && setting.Lockable) { PlayerLock val = Lock(setting); return ((PlayerLock)(ref val)).Locked; } return false; } public void Tick(float now) { if (!(now < nextRefresh)) { nextRefresh = now + 0.5f; Refresh(force: false); } } private void Refresh(bool force) { try { PublishSettingsAccess(); PublishLockedValues(); ReadLocks(force); foreach (ThingSetting item in ThingSettings.All.Where((ThingSetting s) => s.Lockable)) { bool flag = IsLocked(item); if (flag && trackedLocks.Add(item)) { MeadworksBlockedOptions.Track("com.meadworks.thing", entries[item], (ConfigScope)1, true); } else if (!flag && trackedLocks.Remove(item)) { MeadworksBlockedOptions.Untrack(entries[item]); } } } catch (Exception ex) { log.Warning("Configuration refresh failed: " + ex.GetType().Name + ": " + ex.Message); } } private void PublishLockedValues() { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Invalid comparison between Unknown and I4 //IL_007b: 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) if (Plugin.IsReady && (int)Plugin.Session.Role != 3) { string text = LockedValues.Compose(LockList.Parse(((ConfigEntry<string>)(object)entries[(ThingSetting)(object)General.LockedPlayerSettings]).Value), (Func<string, string>)delegate(string id) { ThingSetting val2 = ThingSettings.Find(id); ConfigEntryBase value; return (val2 == null || !entries.TryGetValue(val2, out value)) ? null : value.GetSerializedValue(); }); if (!(text == lockedValues.Value)) { ConfigAmendResult val = MeadworksConfig.Amend<string>(lockedValues, "com.meadworks.thing", (ConfigScope)1, text, (string)null, true); log.Verbose("Locked player values: " + ((object)val.Route/*cast due to .constrained prefix*/).ToString() + (val.Applied ? " applied" : " not applied")); } } } private void ReadLocks(bool force) { string text = Get<string>(General.LockedPlayerSettings); if (force || text != lockListText) { lockListText = text; lockList = LockList.Parse(text); if (lockList.Unknown.Count > 0 || lockList.NotLockable.Count > 0) { log.Warning("Locked Player Settings: " + lockList.Describe()); } else { log.Verbose("Locked Player Settings: " + lockList.Describe()); } } string text2 = MeadworksConfig.Effective<string>(lockedValues, "com.meadworks.thing", (ConfigScope)1, "", true); if (force || text2 != valuesText) { valuesText = text2; values = LockedValues.Decode(text2); } } private PlayerLock Lock(ThingSetting setting) { //IL_0018: Unknown result type (might be due to invalid IL or missing references) return PlayerLocks.Evaluate(setting.Id, ServerBinds(), lockList, values); } private bool ServerBinds() { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Invalid comparison between Unknown and I4 //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Invalid comparison between Unknown and I4 if (!Plugin.IsReady) { return false; } SessionSnapshot session = Plugin.Session; if ((int)session.Role != 3 || syncReceipt.CurrentGeneration != session.Generation) { return false; } return (int)MeadworksAuthority.Decide("com.meadworks.thing", (ConfigScope)1, true).Source == 1; } private void OnSynchronized(object sender, ConfigurationSynchronizationEventArgs args) { if (args.UpdatedPluginGUIDs != null && args.UpdatedPluginGUIDs.Contains("com.meadworks.thing") && Plugin.IsReady) { syncReceipt.Observe(); Refresh(force: true); } } private static bool TryConvert<T>(string text, out T value) { try { value = (T)TomlTypeConverter.ConvertToValue(text, typeof(T)); return true; } catch (Exception) { value = default(T); return false; } } public ServerRules Rules() { //IL_0037: Unknown result type (might be due to invalid IL or missing references) return ThingSettings.Rules(Get<bool>(General.AdminListEditable), Get<bool>(Remote.Editing), Get<bool>(World.WardOverrideAllowed), Get<bool>(World.TerrainNonAdminOwnPieces), Get<bool>(World.RemoveDropsForEveryone)); } public void Shutdown() { file.SettingChanged -= OnSettingChanged; file.ConfigReloaded -= OnReloaded; SynchronizationManager.OnConfigurationSynchronized -= OnSynchronized; trackedLocks.Clear(); foreach (ConfigEntryBase value in entries.Values) { MeadworksBlockedOptions.Untrack(value); } entries.Clear(); this.Changed = null; } private void OnSettingChanged(object sender, SettingChangedEventArgs e) { if ((object)e.ChangedSetting == settingsGrant || (object)e.ChangedSetting == settingsStatus) { PublishSettingsAccess(); } this.Changed?.Invoke(); } private void OnReloaded(object sender, EventArgs e) { this.Changed?.Invoke(); } private void BindSetting(ThingSetting 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) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0047: 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, Map(setting.Scope), (AcceptableValueBase)null); } else if (!(setting is ThingSetting<bool> setting2)) { if (!(setting is ThingSetting<int> val)) { if (!(setting is ThingSetting<string> setting3)) { if (!(setting is ThingSetting<RemoteConfigScope> setting4)) { throw new NotSupportedException(setting.Id + ": unsupported value type " + setting.ValueType.Name + "."); } value = (ConfigEntryBase)(object)Bind<RemoteConfigScope>(setting4, null); } else { value = (ConfigEntryBase)(object)Bind<string>(setting3, null); } } else { value = (ConfigEntryBase)(object)Bind<int>(val, (AcceptableValueBase?)(object)new AcceptableValueRange<int>((int)((ThingSetting)val).Minimum, (int)((ThingSetting)val).Maximum)); } } else { value = (ConfigEntryBase)(object)Bind<bool>(setting2, null); } entries.Add(setting, value); } private ConfigEntry<T> Bind<T>(ThingSetting<T> setting, AcceptableValueBase? acceptable) { //IL_0096: 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_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Expected O, but got Unknown //IL_0061: 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_0076: Expected O, but got Unknown if ((object)setting == Remote.AllowList) { return MeadworksConfig.Bind<T>(file, ((ThingSetting)setting).Section, ((ThingSetting)setting).Key, setting.Default, new ConfigDescription(((ThingSetting)setting).Description, acceptable, new object[1] { (object)new ConfigDependency((Func<bool>)(() => (int)Get<RemoteConfigScope>(Remote.Scope) == 1), (Func<string>)(() => "Requires Remote Config Scope: AllowList."), (Func<bool>)null) }), Map(((ThingSetting)setting).Scope)); } return MeadworksConfig.Bind<T>(file, ((ThingSetting)setting).Section, ((ThingSetting)setting).Key, setting.Default, ((ThingSetting)setting).Description, Map(((ThingSetting)setting).Scope), acceptable); } } internal sealed class ThingSyncReceipt { 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 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; } } } } public sealed class ThingImport : IDisposable { public const string Section = "Import"; private const string SourceName = "Valheim_Admin_Tool"; private readonly ConfigFile config; private readonly ThingConfig settings; private readonly MeadworksLog log; private readonly ConfigEntry<string> marker; private readonly IDisposable registration; private bool disposed; private static string SourcePath => Path.Combine(Paths.ConfigPath, "com.icecub.valheimadmintool.cfg"); private bool Imported => marker.Value.Split(',').Contains("Valheim_Admin_Tool"); public ThingImport(ConfigFile config, ThingConfig settings, MeadworksLog log) { //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_0088: Expected O, but got Unknown this.config = config; this.settings = settings; this.log = log; marker = MeadworksConfig.BindHidden<string>(config, "Import", "Imported Sources", "", "Managed by Thing: sources already imported.", (ConfigScope)0, (AcceptableValueBase)null); ModuleSource adminTool = ModuleStepAside.AdminTool; registration = MeadworksConversion.Register(new ConversionRegistration("thing.valheimadmintool", "com.meadworks.thing", adminTool.Guid, adminTool.Name, (Func<bool>)(() => !disposed && File.Exists(SourcePath) && !Imported && MeadworksHeadless.AllowsUi), (Func<object>)(() => Preview()), (Func<object, IReadOnlyDictionary<string, string>, bool, object>)((object document, IReadOnlyDictionary<string, string> resolutions, bool backup) => Apply((ImportPlan)(((document is ImportPlan) ? document : null) ?? throw new ArgumentException("Wrong Thing import preview.")), backup)))); } public ImportPlan Preview() { ImportPlan val = SourceImport.Plan(File.ReadAllText(SourcePath)); string text = Path.Combine(ThingPaths.DataFolder, "import-valheimadmintool.txt"); Directory.CreateDirectory(Path.GetDirectoryName(text)); File.WriteAllText(text, val.Report()); log.Info("Import preview for Valheim_Admin_Tool: " + val.Values.Count + " hotkeys, " + val.Notes.Count + " notes (" + text + ")."); return val; } private object Apply(ImportPlan plan, bool backup) { //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0083: Unknown result type (might be due to invalid IL or missing references) if (!backup) { throw new InvalidOperationException("Thing import requires a backup."); } MeadworksConversion.Target(config).Backup(); bool saveOnConfigSet = config.SaveOnConfigSet; config.SaveOnConfigSet = false; int num = 0; try { foreach (ImportValue value in plan.Values) { ImportValue current = value; if (ThingSettings.Find(((ImportValue)(ref current)).SettingId) is ThingSetting<string> val && ((ThingSetting)val).Hotkey) { try { settings.Shortcut(val).Value = KeyboardShortcut.Deserialize(((ImportValue)(ref current)).Value); num++; } catch (Exception ex) { log.Warning("Import skipped " + ((ImportValue)(ref current)).SettingId + ": " + ex.GetType().Name); } } } marker.Value = string.Join(",", (from p in marker.Value.Split(',') where p.Length > 0 select p).Append("Valheim_Admin_Tool").Distinct()); } finally { config.SaveOnConfigSet = saveOnConfigSet; } config.Save(); string text = ThingText.Format("import_done", num, "Valheim_Admin_Tool"); log.Info(text); return text; } public void Dispose() { if (!disposed) { disposed = true; registration.Dispose(); } } } public static class ThingModules { private const string TranslationProduct = "thing"; private static ModuleStepAsideDecision? stepAside; private static long noticedSession = -1L; private static long offNoticedSession = -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.thing").Any()) { MeadworksStepAside.Declare("com.meadworks.thing", "Thing", ((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("Thing " + ((object)((StepAsideNotice)(ref current)).Module/*cast due to .constrained prefix*/).ToString().ToLowerInvariant() + " module steps aside: " + ((StepAsideNotice)(ref current)).SourceName + " is installed."); } } public static bool ProductActive(out ConfigBlockReason reason) { reason = (ConfigBlockReason)0; if (Plugin.Ready) { return MeadworksAuthority.ProductActive("com.meadworks.thing", ref reason); } return false; } public static bool Active(ThingModule module) { //IL_0015: Unknown result type (might be due to invalid IL or missing references) if (Plugin.Settings != null && ProductActive(out var _)) { return !StepAside.SteppedAside(module); } return false; } public static void Tick(MeadworksLog log) { //IL_00dc: Unknown result type (might be due to invalid IL or missing references) //IL_00de: Invalid comparison between Unknown and I4 //IL_00ef: Unknown result type (might be due to invalid IL or missing references) //IL_00f1: Invalid comparison between Unknown and I4 //IL_00e0: Unknown result type (might be due to invalid IL or missing references) //IL_00e2: Invalid comparison between Unknown and I4 //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_0071: 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_00a9: Expected O, but got Unknown if (!Plugin.Ready || (Object)(object)Player.m_localPlayer == (Object)null) { return; } SessionSnapshot session = Plugin.Session; if (stepAside != null && stepAside.Any && noticedSession != session.Generation) { noticedSession = session.Generation; foreach (StepAsideNotice notice in stepAside.Notices) { StepAsideNotice current = notice; MeadworksStepAside.ReportModule("com.meadworks.thing", ((object)((StepAsideNotice)(ref current)).Module/*cast due to .constrained prefix*/).ToString().ToLowerInvariant(), new SourceMod(((StepAsideNotice)(ref current)).SourceGuid, ((StepAsideNotice)(ref current)).SourceName), "thing", ((StepAsideNotice)(ref current)).Key); } } if (offNoticedSession != session.Generation && session.AuthorityReady && !ProductActive(out var reason) && ((int)reason == 4 || (int)reason == 5)) { offNoticedSession = session.Generation; string text = ThingText.Text(((int)reason == 4) ? "off_no_crucible" : "off_no_thing"); ThingText.Centre(text, log); log.Info(text); } } public static void Reset() { stepAside = null; noticedSession = -1L; offNoticedSession = -1L; } } public static class ThingText { 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(ThingText).Assembly; foreach (string item in MeadworksLanguages.Supported) { using Stream stream = assembly.GetManifestResourceStream("Thing.localization." + item + ".json"); if (stream != null) { using StreamReader streamReader = new StreamReader(stream); dictionary.Add(item, streamReader.ReadToEnd()); } } Translations = new ProductTranslations("thing", (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); } } } [BepInPlugin("com.meadworks.thing", "Thing", "0.1.14")] [BepInDependency("com.meadworks.crucible", "0.3.33")] [BepInDependency("com.meadworks.crucible.ui", "0.3.22")] [BepInDependency("com.jotunn.jotunn", "2.30.2")] [NetworkCompatibility(/*Could not decode attribute arguments.*/)] public sealed class Plugin : BaseUnityPlugin { public const string Guid = "com.meadworks.thing"; public const string Name = "Thing"; public const string Version = "0.1.14"; public const string Family = "Runestone"; private ProductRegistration? registration; private MeadworksLog? log; private ThingImport? import; private bool started; internal static ProductRegistration? Source { get { if (!Chainloader.PluginInfos.TryGetValue("com.meadworks.thing", out var value)) { return null; } return (value.Instance as Plugin)?.registration; } } public static bool Ready { get; private set; } public static bool AllowsUi { get; private set; } public static ThingConfig? Settings { get; private set; } private void Awake() { //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Expected O, but got Unknown log = MeadworksLog.For(((BaseUnityPlugin)this).Logger, "Thing"); try { if (!Plugin.IsReady) { throw new InvalidOperationException("Required Crucible is not ready"); } registration = Plugin.Services.Register(new ProductMetadata("com.meadworks.thing", "Thing", new Version("0.1.14"), new Version(0, 1), new Version(0, 1), (MultiplayerPolicy)2, "Runestone")); AllowsUi = MeadworksHeadless.AllowsUi; Settings = ThingConfig.Bind(((BaseUnityPlugin)this).Config, log); ThingText.Initialize(((BaseUnityPlugin)this).Info.Metadata, ((BaseUnityPlugin)this).Logger); ThingNotices.Initialize(log); ThingPlayerFreeze.Initialize(log); ThingServer.Initialize(log); ThingPickup.Initialize(log); Ready = true; log.Info(AllowsUi ? "Thing registered; modules start after plugin discovery." : "Thing registered on a headless process; no UI is created."); } catch (Exception ex) { log.Error((object)("Thing initialization rejected: " + ex.GetType().Name + ": " + ex.Message)); Shutdown(); } } private void Start() { if (!Ready || log == null) { return; } try { ThingModules.Decide(log); import = new ThingImport(((BaseUnityPlugin)this).Config, Settings, log); ThingRemoteCommands.Initialize(registration, log); ThingTerrainCommands.Initialize(registration); ThingDropCommands.Initialize(registration); ThingTeleportCommands.Initialize(registration); ThingWard.Initialize(registration); ThingPoi.Initialize(registration); ThingTime.Initialize(); ThingChatMute.Initialize(); ThingHexAudit.Initialize(registration); ThingAccess.Initialize(log); ThingInventory.Initialize(registration, log); registration.AddCapability<Func<string, string, bool>>("meadworks.thing.admin_permission", AdminPermissions.CapabilityVersion, (Func<string, string, bool>)ThingAccess.Allows); registration.AddCapability<Func<string[], bool>>("meadworks.admin.command", AdminCommandContract.ContractVersion, (Func<string[], bool>)delegate(string[] args) { ((ConsoleCommand)new ThingCommand()).Run(args); return true; }); CommandManager.Instance.AddConsoleCommand((ConsoleCommand)(object)new ThingCommand()); started = true; } catch (Exception ex) { log.Error((object)("Thing modules rejected: " + ex.GetType().Name + ": " + ex.Message)); Shutdown(); } } private void Update() { //IL_00a8: Unknown result type (might be due to invalid IL or missing references) //IL_00d3: Unknown result type (might be due to invalid IL or missing references) if (!Ready) { return; } ThingText.Tick(); if (!started || log == null) { return; } Settings?.Tick(Time.unscaledTime); ThingModules.Tick(log); ThingNotices.Tick(); ThingServer.Tick(); ThingRemoteCommands.Tick(); ThingYamlFiles.Tick(); ThingTeleportCommands.Tick(); ThingInventory.Tick(); ThingWard.Tick(); if (!((Object)(object)Player.m_localPlayer != (Object)null)) { return; } ThingPlayerFreeze.Tick(); ThingTime.Tick(); ThingTerrainCommands.Tick(); ThingDropCommands.Tick(); ThingPickup.Tick(); ThingPanel.Tick(); if (Settings != null && ThingModules.Active((ThingModule)0)) { if (MeadworksInput.Pressed(Settings.ShortcutValue(Hotkeys.WardOverride), (InputContext)0)) { ThingWard.Run(new string[1] { "ward" }); } if (MeadworksInput.Pressed(Settings.ShortcutValue(Hotkeys.RemoveDrops), (InputContext)0)) { ThingDropCommands.Run(new string[1] { "drops" }); } } } private void OnDestroy() { Shutdown(); } private void Shutdown() { Ready = false; started = false; Action[] array = new Action[19] { ThingPanel.Shutdown, ThingConfigFiles.Reset, ThingYamlFiles.Reset, ThingTeleportCommands.Shutdown, ThingChatMute.Shutdown, ThingTime.Shutdown, ThingPoi.Shutdown, ThingWard.Shutdown, ThingPickup.Shutdown, ThingDropCommands.Shutdown, ThingTerrainCommands.Shutdown, ThingInventory.Shutdown, ThingRemoteCommands.Shutdown, ThingServer.Unload, ThingNotices.Shutdown, ThingPlayerFreeze.Shutdown, delegate { import?.Dispose(); }, ThingModules.Reset, ThingText.Shutdown }; foreach (Action action in array) { try { action(); } catch (Exception ex) { MeadworksLog? obj = log; if (obj != null) { obj.Warning("Thing teardown: " + ex.GetType().Name); } } } import = null; Settings?.Shutdown(); Settings = null; ProductRegistration? obj2 = registration; if (obj2 != null) { obj2.Dispose(); } registration = null; } } internal static class ThingConfigFiles { private sealed class FileItem { internal string Path = ""; internal string Relative = ""; internal string Id = ""; internal ConfigFile? File; } private sealed class HandlerErrors : ILogListener, IDisposable { private readonly int thread = Thread.CurrentThread.ManagedThreadId; internal Exception? Error { get; private set; } internal HandlerErrors() { Logger.Listeners.Add((ILogListener)(object)this); } public void LogEvent(object sender, LogEventArgs args) { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) if (Thread.CurrentThread.ManagedThreadId == thread && (args.Level & 2) != 0 && args.Data is Exception ex && Error == null) { Exception ex2 = (Error = ex); } } public void Dispose() { Logger.Listeners.Remove((ILogListener)(object)this); } } private static long generation = -1L; private static readonly HashSet<string> backedUp = new HashSet<string>(StringComparer.Ordinal); private static readonly UTF8Encoding utf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); private static bool Windows => Path.DirectorySeparatorChar == '\\'; private static StringComparison Comparison { get { if (!Windows) { return StringComparison.Ordinal; } return StringComparison.OrdinalIgnoreCase; } } private static string Canonical(string path) { if (!Windows) { return Path.GetFullPath(path); } return Path.GetFullPath(path).ToUpperInvariant(); } internal static string Id(string value) { return FileVersion.Hash(utf8.GetBytes(value)); } private static string EntryId(ConfigEntryBase entry) { return Id(entry.Definition.Section + "\0" + entry.Definition.Key); } internal static bool Authorized(Actor admin, bool local) { //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) if (!Plugin.Ready || !Plugin.IsReady || !Plugin.Session.AuthorityReady || !ThingServer.CurrentAuthority || !ThingModules.ProductActive(out var _) || Plugin.Settings == null) { return false; } if (local) { return true; } ZNet instance = ZNet.instance; if (SettingsAccess.CanConfigure(false, (Object)(object)instance != (Object)null && instance.IsAdmin(((Actor)(ref admin)).Id), Plugin.Settings.AllowAdminSettingsEditing, Plugin.Settings.Get<bool>(Remote.Editing)) && (Object)(object)instance != (Object)null) { return instance.GetPeers().Any(delegate(ZNetPeer p) { if (p != null && p.IsReady()) { ZRpc rpc = p.m_rpc; if (rpc != null && rpc.IsConnected()) { ISocket socket = p.m_socket; return ((socket != null) ? socket.GetHostName() : null) == ((Actor)(ref admin)).Id; } } return false; }); } return false; } private static FilePolicy Policy() { //IL_000a: 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) //IL_0079: Expected O, but got Unknown return new FilePolicy(Plugin.Settings.Get<RemoteConfigScope>(Remote.Scope), Plugin.Settings.Get<string>(Remote.AllowList), from f in MeadworksDataFiles.Registered select Relative(f.Path) into p where p != null select p, !Windows); } internal static string? Relative(string path) { string text = Path.GetFullPath(Paths.ConfigPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar; string fullPath = Path.GetFullPath(path); if (!fullPath.StartsWith(text, Comparison)) { return null; } return fullPath.Substring(text.Length).Replace(Path.DirectorySeparatorChar, '/'); } internal static bool Allowed(string path) { //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Invalid comparison between Unknown and I4 string text = Relative(path); if (text == null || !File.Exists(path)) { return false; } FileInfo fileInfo = new FileInfo(path); bool flag = (fileInfo.Attributes & FileAttributes.ReparsePoint) != 0; for (DirectoryInfo directoryInfo = fileInfo.Directory; directoryInfo != null; directoryInfo = directoryInfo.Parent) { flag |= (directoryInfo.Attributes & FileAttributes.ReparsePoint) != 0; if (Canonical(directoryInfo.FullName) == Canonical(Paths.ConfigPath)) { break; } } return (int)RemoteFiles.Check(new FileRequest(text, fileInfo.Length, flag), Policy()) == 0; } private static FileItem[] Discover() { Dictionary<string, FileItem> files = new Dictionary<string, FileItem>(Windows ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal); HashSet<string> ambiguous = new HashSet<string>(files.Comparer); PluginInfo[] array = Chainloader.PluginInfos.Values.Take(4096).ToArray(); for (int i = 0; i < array.Length; i++) { BaseUnityPlugin instance = array[i].Instance; if ((Object)(object)instance == (Object)null) { continue; } Add(instance.Config.ConfigFilePath, instance.Config); Type type = ((object)instance).GetType(); while (type != null && type != typeof(object)) { foreach (FieldInfo item in type.GetFields(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).Take(4096)) { if (!typeof(ConfigFile).IsAssignableFrom(item.FieldType)) { continue; } try { object? value = item.GetValue(item.IsStatic ? null : instance); ConfigFile val = (ConfigFile)((value is ConfigFile) ? value : null); if (val != null) { Add(val.ConfigFilePath, val); } } catch { } } type = type.BaseType; } } foreach (string item2 in Directory.EnumerateFiles(Paths.ConfigPath, "*.cfg", SearchOption.TopDirectoryOnly).Take(10000)) { Add(item2, null); } foreach (DataFileRegistration item3 in MeadworksDataFiles.Registered.Take(10000)) { Add(item3.Path, null); } foreach (string item4 in ambiguous) { files[item4].File = null; } return files.Values.OrderBy<FileItem, string>((FileItem f) => f.Relative, StringComparer.Ordinal).Take(10000).ToArray(); void Add(string path, ConfigFile? handle) { try { if (Allowed(path)) { string text = Canonical(path); if (!files.TryGetValue(text, out FileItem value2)) { Dictionary<string, FileItem> dictionary = files; FileItem obj2 = new FileItem { Path = Path.GetFullPath(path), Relative = Relative(path), Id = Id(text) }; value2 = obj2; dictionary[text] = obj2; } if (handle != null) { if (value2.File != null && value2.File != handle) { ambiguous.Add(text); } else { value2.File = handle; } } } } catch { } } } private static bool Sensitive(ConfigEntryBase entry) { string text = new string((entry.Definition.Section + entry.Definition.Key).Where(char.IsLetterOrDigit).Select(char.ToLowerInvariant).ToArray()); return new string[7] { "password", "passwd", "token", "secret", "apikey", "credential", "privatekey" }.Any(text.Contains); } private static ConfigEntryBase[] Entries(FileItem item) { ConfigEntryMetadata val = default(ConfigEntryMetadata); ConfigEntryMetadata val2 = default(ConfigEntryMetadata); if (item.File != null) { return (from p in (IEnumerable<KeyValuePair<ConfigDefinition, ConfigEntryBase>>)item.File select p.Value into e where !Sensitive(e) && (!MeadworksConfig.TryGetMetadata(e, ref val) || (int)val.Visibility == 0) && NativeTags.Read(e.Description.Tags).Attributes.Browsable != false && (MeadworksConfig.TryGetMetadata(e, ref val2) || !ThingExternalConfig.Inspect(e).Hidden) select e).OrderBy<ConfigEntryBase, string>((ConfigEntryBase e) => e.Definition.Section, StringComparer.Ordinal).ThenBy<ConfigEntryBase, string>((ConfigEntryBase e) => e.Definition.Key, StringComparer.Ordinal).Take(10000) .ToArray(); } return Array.Empty<ConfigEntryBase>(); } private static string Version(FileItem file, ConfigEntryBase entry) { using MemoryStream memoryStream = new MemoryStream(); byte[] array = ThingMessages.ReadBounded(file.Path); memoryStream.Write(array, 0, array.Length); using (BinaryWriter binaryWriter = new BinaryWriter(memoryStream, utf8, leaveOpen: true)) { binaryWriter.Write(Plugin.Session.Generation); binaryWriter.Write(EntryId(entry)); binaryWriter.Write(TomlTypeConverter.ConvertToString(entry.BoxedValue, entry.SettingType)); ConfigEntryMetadata val = default(ConfigEntryMetadata); binaryWriter.Write(MeadworksConfig.TryGetMetadata(entry, ref val) ? (((object)val.Decision).ToString() + "\n" + TomlTypeConverter.ConvertToString(val.SafeValue, entry.SettingType)) : ("unmanaged\n" + ThingExternalConfig.Inspect(entry).Stamp)); } return FileVersion.Hash(memoryStream.ToArray()); } private static bool Editable(NativeEntry native, out ConfigEntryMetadata metadata) { //IL_00a4: Unknown result type (might be due to invalid IL or missing references) ConfigEntryBase entry = native.Entry; ThingConfig? settings = Plugin.Settings; if (settings != null && settings.IsSettingsPolicy(entry)) { metadata = null; return false; } bool flag = MeadworksConfig.TryGetMetadata(entry, ref metadata); if (Sensitive(entry) || native.Attributes.Browsable == false || native.Attributes.ReadOnly == true || native.Attributes.CharterReadOnly || native.Attributes.GenericReadOnly || !native.Shape.HasConverter || native.Attributes.HasCodecDelegates) { return false; } if (flag) { if ((int)metadata.Visibility == 0) { return metadata.Decision.Allowed; } return false; } ThingExternalConfig.Facts facts = ThingExternalConfig.Inspect(entry); if (facts.Allowed) { return !facts.Hidden; } return false; } internal static IReadOnlyList<string> Run(ConfigEditRequest request, Actor admin, bool local, string connection = "") { //IL_000d: 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_009b: Unknown result type (might be due to invalid IL or missing references) //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_0146: Unknown result type (might be due to invalid IL or missing references) //IL_014b: Unknown result type (might be due to invalid IL or missing references) //IL_0151: Unknown result type (might be due to invalid IL or missing references) //IL_0203: Unknown result type (might be due to invalid IL or missing references) //IL_0208: Unknown result type (might be due to invalid IL or missing references) //IL_020e: Unknown result type (might be due to invalid IL or missing references) //IL_0316: Unknown result type (might be due to invalid IL or missing references) //IL_031b: Unknown result type (might be due to invalid IL or missing references) //IL_0321: Unknown result type (might be due to invalid IL or missing references) //IL_0308: Unknown result type (might be due to invalid IL or missing references) //IL_0351: Unknown result type (might be due to invalid IL or missing references) //IL_0356: Unknown result type (might be due to invalid IL or missing references) //IL_0362: Unknown result type (might be due to invalid IL or missing references) //IL_036f: Unknown result type (might be due to invalid IL or missing references) //IL_037d: Unknown result type (might be due to invalid IL or missing references) //IL_038f: Unknown result type (might be due to invalid IL or missing references) //IL_03a1: Unknown result type (might be due to invalid IL or missing references) //IL_03b3: Unknown result type (might be due to invalid IL or missing references) //IL_03ec: Unknown result type (might be due to invalid IL or missing references) //IL_0431: Unknown result type (might be due to invalid IL or missing references) //IL_0444: Unknown result type (might be due to invalid IL or missing references) //IL_044a: Invalid comparison between Unknown and I4 //IL_0499: Unknown result type (might be due to invalid IL or missing references) //IL_04a1: Unknown result type (might be due to invalid IL or missing references) //IL_04bc: Unknown result type (might be due to invalid IL or missing references) //IL_04c9: Unknown result type (might be due to invalid IL or missing references) if (!Authorized(admin, local)) { return Reply("config_unavailable"); } if (generation != Plugin.Session.Generation) { generation = Plugin.Session.Generation; backedUp.Clear(); } try { if (request.Verb.StartsWith("yaml-", StringComparison.Ordinal)) { return ThingYamlFiles.Run(request, admin, local, connection); } ThingExternalConfig.BeginRequest(); FileItem[] source = Discover(); if (request.Verb == "list") { ThingServer.Audit(admin, (ThingAction)34, default(Actor), (AuditOutcome)0, "allowed config file list"); return ThingUiPage.Encode("config_files", request.Offset, ((IEnumerable<FileItem>)source).Select((Func<FileItem, ThingUiRow>)((FileItem f) => new ThingUiRow((ThingYamlFiles.Registered(f.Path) != null) ? "yaml" : ((f.File == null) ? "readonly" : "bound"), f.Id, f.Relative, ThingText.Text((ThingYamlFiles.Registered(f.Path) != null) ? "config_yaml" : ((f.File == null) ? "config_unbound" : "config_bound"))))), ""); } FileItem fileItem = source.SingleOrDefault((FileItem f) => f.Id == request.File); if (fileItem == null || fileItem.File == null) { return Reply("config_unbound"); } ConfigEntryBase[] source2 = Entries(fileItem); if (request.Verb == "entries") { ThingServer.Audit(admin, (ThingAction)34, default(Actor), (AuditOutcome)0, "config entries " + fileItem.Relative); return ThingUiPage.Encode("config_entries", request.Offset, ((IEnumerable<ConfigEntryBase>)source2).Select((Func<ConfigEntryBase, ThingUiRow>)((ConfigEntryBase e) => new ThingUiRow("entry", EntryId(e), e.Definition.Section + "." + e.Definition.Key, e.SettingType.Name))), ""); } ConfigEntryBase val = ((IEnumerable<ConfigEntryBase>)source2).SingleOrDefault((Func<ConfigEntryBase, bool>)((ConfigEntryBase e) => EntryId(e) == request.Entry)); if (val == null) { return Reply("config_unavailable"); } NativeEntry nativeEntry = new NativeEntry(val); ConfigEntryMetadata metadata; bool flag = Editable(nativeEntry, out metadata); if (request.Verb == "list-get") { ThingServer.Audit(admin, (ThingAction)34, default(Actor), (AuditOutcome)0, "typed list read " + fileItem.Relative + " " + (object)val.Definition); return ThingConfigLists.Read(fileItem.Id, EntryId(val), Version(fileItem, val), nativeEntry, flag); } if (request.Verb == "list-set") { if (!flag) { return Reply("config_readonly"); } if (Version(fileItem, val) != request.Version) { return Reply("config_stale"); } try { request = request.WithValue(ThingConfigLists.Serialize(nativeEntry, request.ListValues)); } catch { return Reply("config_invalid"); } } if (request.Writes) { return Write(request, fileItem, nativeEntry, admin, local); } ThingServer.Audit(admin, (ThingAction)34, default(Actor), (AuditOutcome)0, "config entry " + fileItem.Relative + " " + (object)val.Definition); ConfigEntryMetadata val2 = default(ConfigEntryMetadata); return new ConfigEntryDocument { File = fileItem.Id, Entry = EntryId(val), Version = Version(fileItem, val), Section = val.Definition.Section, Key = val.Definition.Key, Description = val.Description.Description, Current = (nativeEntry.Shape.HasConverter ? nativeEntry.RawSerialized() : (val.BoxedValue?.ToString() ?? "")), Default = (nativeEntry.Shape.HasConverter ? TomlTypeConverter.ConvertToString(val.DefaultValue, val.SettingType) : (val.DefaultValue?.ToString() ?? "")), Effective = ((MeadworksConfig.TryGetMetadata(val, ref val2) && (int)val2.Decision.Source == 2) ? TomlTypeConverter.ConvertToString(val2.SafeValue, val.SettingType) : (nativeEntry.Shape.HasConverter ? nativeEntry.RawSerialized() : (val.BoxedValue?.ToString() ?? ""))), Editable = flag, Reason = (flag ? "" : ThingText.Text("config_readonly")), Shape = nativeEntry.Shape, Attributes = nativeEntry.Attributes }.Encode(); } catch { return Reply("config_unavailable"); } } private static IReadOnlyList<string> Write(ConfigEditRequest request, FileItem file, NativeEntry native, Actor admin, bool local) { //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) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_0492: Unknown result type (might be due to invalid IL or missing references) //IL_049b: Unknown result type (might be due to invalid IL or missing references) //IL_04a1: Unknown result type (might be due to invalid IL or missing references) //IL_00d1: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Invalid comparison between Unknown and I4 //IL_0294: Unknown result type (might be due to invalid IL or missing references) //IL_029b: Expected O, but got Unknown //IL_0332: Unknown result type (might be due to invalid IL or missing references) //IL_0339: Expected O, but got Unknown if (!Editable(native, out ConfigEntryMetadata metadata)) { return Reply("config_readonly"); } ConfigEntryBase entry = native.Entry; if (!Authorized(admin, local) || Version(file, entry) != request.Version) { return Reply("config_stale"); } NativeParsedValue parsed = native.Parse(request.Value); if (!parsed.Valid && (int)native.Shape.Kind == 1 && native.Shape.IsFlags && request.Value == "0") { object obj = Enum.ToObject(entry.SettingType, 0); if (entry.Description.AcceptableValues == null || entry.Description.AcceptableValues.IsValid(obj)) { parsed = new NativeParsedValue(obj, TomlTypeConverter.ConvertToString(obj, entry.SettingType), clamped: false); } } if (!parsed.Valid || utf8.GetByteCount(parsed.Serialized) > 4096) { return Reply("config_invalid"); } bool started = false; string text = "config_failed"; try { if (!Current()) { return Reply("config_stale"); } IConversionTarget target = MeadworksConversion.Target(file.File); Action action = delegate { if (!Current()) { throw new InvalidOperationException("StaleRemoteConfig"); } if (!backedUp.Contains(Canonical(file.Path))) { string destFileName = Backups.Name(file.Path, DateTime.UtcNow); File.Copy(file.Path, destFileName, overwrite: false); backedUp.Add(Canonical(file.Path)); } if (!Current()) { throw new InvalidOperationException("StaleRemoteConfig"); } started = true; using HandlerErrors handlerErrors = new HandlerErrors(); target.WriteAtomic((IReadOnlyDictionary<string, object>)new Dictionary<string, object> { [entry.Definition.Section + "." + entry.Definition.Key] = parsed.Value }); if (handlerErrors.Error != null) { throw new InvalidOperationException("OwnerHandlerFailed", handlerErrors.Error); } }; if ((metadata != null) ? MeadworksConfig.AmendBatch(metadata.ProductId, (IReadOnlyList<ConfigEntryBase>)(object)new ConfigEntryBase[1] { entry }, action, entry.Definition.Key).Applied : ThingExternalConfig.Apply(entry, action, Current)) { string text2 = native.RawSerialized(); ConfigFile obj2 = new ConfigFile(file.Path, false); ConfigEntryBase val = (ConfigEntryBase)typeof(ConfigFile).GetMethods().Single((MethodInfo m) => m.Name == "Bind" && m.IsGenericMethodDefinition && m.GetParameters().Length == 3 && m.GetParameters()[0].ParameterType == typeof(ConfigDefinition)).MakeGenericMethod(entry.SettingType) .Invoke(obj2, new object[3] { entry.Definition, entry.DefaultValue, entry.Description }); text = ((text2 == parsed.Serialized && TomlTypeConverter.ConvertToString(val.BoxedValue, entry.SettingType) == text2) ? "config_saved" : "config_unknown"); if (text == "config_saved") { foreach (string item in Backups.ToDelete(file.Path, Directory.EnumerateFiles(Path.GetDirectoryName(file.Path), Path.GetFileName(file.Path) + ".*.bak"), Plugin.Settings.Get<int>(Remote.BackupCount))) { try { File.Delete(item); } catch { } } } } else { text = (started ? "config_unknown" : "config_failed"); } } catch (InvalidOperationException ex) when (ex.Message == "StaleRemoteConfig" && !started) { text = "config_stale"; } catch { text = (started ? "config_unknown" : "config_failed"); } ThingServer.Audit(admin, (ThingAction)35, default(Actor), (AuditOutcome)((!(text == "config_saved")) ? 2 : 0), file.Relative + " " + ((object)entry.Definition)?.ToString() + ": " + text); if (text == "config_saved") { try { return ConfigWire.WriteEcho(Version(file, entry), native.RawSerialized()); } catch { return Reply("config_unknown"); } } return Reply(text); bool Current() { //IL_000e: Unknown result type (might be due to invalid IL or missing references) if (metadata == null) { ThingExternalConfig.BeginRequest(); } if (!Authorized(admin, local) || generation != Plugin.Session.Generation || !Allowed(file.Path) || !Editable(native, out ConfigEntryMetadata _)) { return false; } FileItem fileItem = Discover().SingleOrDefault((FileItem f) => f.Id == file.Id); if (fileItem != null && fileItem.File == file.File && fileItem.File[entry.Definition] == entry) { return Version(file, entry) == request.Version; } return false; } } private static IReadOnlyList<string> Reply(string key) { return new string[1] { ThingText.Text(key) }; } internal static void Reset() { backedUp.Clear(); generation = -1L; ThingExternalConfig.Reset(); } } internal static class ThingConfigLists { private static Type Element(NativeEntry native) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Invalid comparison between Unknown and I4 Type settingType = native.Entry.SettingType; if ((int)native.Shape.Kind != 12 || !native.Shape.HasConverter || native.Attributes.HasCodecDelegates) { throw new NotSupportedException(); } Type type = ((settingType.IsArray && settingType.GetArrayRank() == 1) ? settingType.GetElementType() : ((!settingType.IsAbstract && typeof(IList).IsAssignableFrom(settingType) && settingType.GetConstructor(Type.EmptyTypes) != null) ? settingType.GetGenericArguments().FirstOrDefault() : null)); if (type == null || !TomlTypeConverter.CanConvert(type)) { throw new NotSupportedException(); } return type; } private static SettingShape Shape(Type element) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0024: 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_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Invalid comparison between Unknown and I4 //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Invalid comparison between Unknown and I4 string text = element.FullName ?? element.Name; SettingKind val = SettingShape.KindOf(text); SettingShape val2 = ((element.IsEnum && (int)val != 5) ? SettingShape.Enum(text, (IEnumerable<string>)Enum.GetNames(element), element.IsDefined(typeof(FlagsAttribute), inherit: false)) : SettingShape.Of(val, text, TomlTypeConverter.CanConvert(element))); if ((int)val != 5 && (int)val != 6) { return val2; } return val2.WithKeyNames((IEnumerable<string>)Enum.GetNames(typeof(KeyCode))); } internal static IReadOnlyList<string> Read(string file, string entry, string version, NativeEntry native, bool editable) { //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_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0063: 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_0071: 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_0089: 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_00a7: Unknown result type (might be due to invalid IL or missing references) //IL_00bd: Unknown result type (might be due to invalid IL or missing references) //IL_00ce: Expected O, but got Unknown //IL_00ce: Unknown result type (might be due to invalid IL or missing references) Type element = Element(native); string[] values = (from value in (native.Entry.BoxedValue as IEnumerable)?.Cast<object>().Take(129) select TomlTypeConverter.ConvertToString(value, element)).ToArray() ?? Array.Empty<string>(); return new ConfigListDocument { Element = new ConfigEntryDocument { File = file, Entry = entry, Version = version, Shape = Shape(element), Editable = editable, Section = native.Entry.Definition.Section, Key = native.Entry.Definition.Key, Attributes = native.Attributes }, Values = values }.Encode(); } internal static string Serialize(NativeEntry native, IReadOnlyList<string> values) { Type element = Element(native); SettingShape shape = Shape(element); ConfigListDocument.PackValues(values); object[] array = values.Select(delegate(string text2) { //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Invalid comparison between Unknown and I4 ParseResult val = ValueCodec.Parse(shape, text2); if (!val.Ok && !val.NeedsModConverter && ((int)shape.Kind != 1 || !shape.IsFlags || !(text2 == "0"))) { throw new FormatException(); } return TomlTypeConverter.ConvertToValue(text2, element); }).ToArray(); object obj; if (native.Entry.SettingType.IsArray) { Array array2 = Array.CreateInstance(element, array.Length); for (int num = 0; num < array.Length; num++) { array2.SetValue(array[num], num); } obj = array2; } else { IList list = (IList)Activator.CreateInstance(native.Entry.SettingType); object[] array3 = array; foreach (object value in array3) { list.Add(value); } obj = list; } string text = TomlTypeConverter.ConvertToString(obj, native.Entry.SettingType); if (YamlTransfer.Bytes(text).Length > 4096) { throw new FormatException(); } return text; } } internal static class ThingExternalConfig { internal sealed class Facts { internal bool Allowed; internal bool Hidden; internal string Kind = "unknown"; internal object? System; internal object? Data; internal MethodInfo? Cache; internal MethodInfo? Send; internal string Stamp => Kind + ":" + Allowed + ":" + Hidden + ":" + Identity(System) + ":" + Identity(Data); } private sealed class Owner { internal object System; internal object Data; internal bool Conditional; } private const BindingFlags Members = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; private static readonly Dictionary<ConfigEntryBase, Owner> owners = new Dictionary<ConfigEntryBase, Owner>(); private static readonly HashSet<ConfigEntryBase> ambiguous = new HashSet<ConfigEntryBase>(); private static bool scanFailed = true; private static bool Authority { get { if (Plugin.IsReady && Plugin.Session.AuthorityReady && ThingServer.CurrentAuthority && (Object)(object)ZNet.instance != (Object)null) { return ZNet.instance.IsServer(); } return false; } } private static string Identity(object? value) { if (value != null) { return RuntimeHelpers.GetHashCode(value).ToString(CultureInfo.InvariantCulture); } return "-"; } internal static object? Read(object target, string name) { Type obj = (target as Type) ?? target.GetType(); object obj2 = ((target is Type) ? null : target); Type type = obj; while (type != null) { PropertyInfo property = type.GetProperty(name, BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (property != null) { return property.GetValue(obj2); } FieldInfo field = type.GetField(name, BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (field != null) { return field.GetValue(obj2); } type = type.BaseType; } return null; } private static object[] Bounded(IEnumerable sequence, int max) { object[] array = sequence.Cast<object>().Take(max + 1).ToArray(); if (array.Length > max) { throw new InvalidOperationException("ConfigRegistryBound"); } return array; } private static bool CurrentOwner(Owner owner, ConfigEntryBase entry) { if (Read(owner.Data, "BaseConfig") == entry && Read(owner.System.GetType(), "configSyncs") is IEnumerable sequence && Bounded(sequence, 4096).Any((object system) => system == owner.System) && Read(owner.System, "allConfigs") is IEnumerable sequence2) { return Bounded(sequence2, 10000).Any((object data) => data == owner.Data); } return false; } private static bool Writable(Owner owner, ConfigEntryBase entry) { if (CurrentOwner(owner, entry)) { object obj = Read(owner.System, "IsSourceOfTruth"); if (obj is bool && (bool)obj) { string name = (owner.Conditional ? "IsWritableConfig" : "isWritableConfig"); Type type = owner.System.GetType(); while (type != null) { MethodInfo methodInfo = type.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).SingleOrDefault((MethodInfo m) => m.Name == name && m.ReturnType == typeof(bool) && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType.IsInstanceOfType(owner.Data)); if (methodInfo != null) { obj = methodInfo.Invoke(methodInfo.IsStatic ? null : owner.System, new object[1] { owner.Data }); if (obj is bool) { return (bool)obj; } return false; } type = type.BaseType; } return false; } } return false; } internal static void BeginRequest() { owners.Clear(); ambiguous.Clear(); scanFailed = false; if (!Authority) { scanFailed = true; return; } try { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); if (assemblies.Length > 4096) { throw new InvalidOperationException("ConfigAssemblyBound"); } Assembly[] array = assemblies; foreach (Assembly assembly in array) { string[] array2 = new string[2] { "ServerSync.ConfigSync", "ConditionalConfigSync.ConditionalConfigSync" }; foreach (string text in array2) { Type type = assembly.GetType(text, throwOnError: false); if (type == null) { continue; } if (!(Read(type, "configSyncs") is IEnumerable sequence)) { scanFailed = true; continue; } object[] array3 = Bounded(sequence, 4096); foreach (object obj in array3) { if (obj == null) { continue; } if (!(Read(obj, "allConfigs") is IEnumerable sequence2)) { scanFailed = true; continue; } object[] array4 = Bounded(sequence2, 10000); foreach (object obj2 in array4) { if (obj2 == null) { continue; } object? obj3 = Read(obj2, "BaseConfig"); ConfigEntryBase val = (ConfigEntryBase)((obj3 is ConfigEntryBase) ? obj3 : null); if (val != null && !ambiguous.Contains(val)) { if (owners.TryGetValue(val, out Owner value) && (value.System != obj || value.Data != obj2)) { owners.Remove(val); ambiguous.Add(val); continue; } owners[val] = new Owner { System = obj, Data = obj2, Conditional = text.StartsWith("Conditional", StringComparison.Ordinal) }; } } } } } } catch { scanFailed = true; } } internal static void Reset() { owners.Clear(); ambiguous.Clear(); scanFailed = true; } internal static Facts Inspect(ConfigEntryBase entry) { Facts facts = new Facts(); if (!Authority) { return new Facts { Hidden = true }; } ConfigEntryMetadata val = default(ConfigEntryMetadata); if (MeadworksConfig.TryGetMetadata(entry, ref val)) { return facts; } try { NativeTags nativeTags = NativeTags.Read(entry.Description.Tags); if (nativeTags.Errors.Count != 0) { return new Facts { Hidden = true }; } bool flag = nativeTags.Attributes.ReadOnly == true || nativeTags.Attributes.CharterReadOnly || nativeTags.Attributes.GenericReadOnly; if (scanFailed) { return new Facts { Hidden = true }; } owners.TryGetValue(entry, out Owner value); if (ambiguous.Contains(entry)) { return new Facts { Hidden = true }; } if (value != null) { object obj = entry.Description.Tags.FirstOrDefault((object tag) => tag != null && tag.GetType().Assembly == typeof(SynchronizationManager).Assembly && tag.GetType().Name == "ConfigurationManagerAttributes"); if (obj != null) { object obj2 = Read(obj, "IsAdminOnly"); if (obj2 is bool && (bool)obj2) { MethodInfo method = typeof(SynchronizationManager).GetMethod("ShouldManageConfig", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(ConfigFile) }, null); if (SynchronizationManager.Instance != null) { obj2 = method?.Invoke(SynchronizationManager.Instance, new object[1] { entry.ConfigFile }); if (obj2 is bool && !(bool)obj2) { goto IL_01b1; } } return new Facts { Hidden = true }; } } goto IL_01b1; } object obj3 = entry.Description.Tags.FirstOrDefault((object tag) => tag != null && tag.GetType().Assembly == typeof(SynchronizationManager).Assembly && tag.GetType().Name == "ConfigurationManagerAttributes"); if (nativeTags.Attributes.IsAdminOnly == true) { if (obj3 != null) { object obj2 = Read(obj3, "IsAdminOnly"); if (obj2 is bool && (bool)obj2) { SynchronizationManager instance = SynchronizationManager.Instance; MethodInfo method2 = typeof(SynchronizationManager).GetMethod("ShouldManageConfig", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(ConfigFile) }, null); if (instance == null || !(method2?.Invoke(instance, new object[1] { entry.ConfigFile }) is bool flag2)) { return facts; } facts.Kind = (flag2 ? "Jotunn" : "NativeBound"); facts.System = instance; facts.Data = entry; if (flag2) { facts.Cache = typeof(SynchronizationManager).GetMethod("CacheConfigurationValues", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null); facts.Send = typeof(SynchronizationManager).GetMethod("SynchronizeChangedConfig", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null); facts.Allowed = !flag && facts.Cache != null && facts.Send != null; } else { facts.Allowed = !flag; } return Authority ? facts : new Facts(); } } return facts; } bool num = entry.Description.Tags.Any((object tag) => tag?.GetType().Name.StartsWith("SyncedConfigEntry", StringComparison.Ordinal) ?? false); bool flag3 = Path.GetFileName(entry.ConfigFile.ConfigFilePath).StartsWith("com.meadworks.", StringComparison.OrdinalIgnoreCase); if (num || flag3) { return facts; } facts.Kind = "NativeBound"; facts.System = entry.ConfigFile; facts.Data = entry; facts.Allowed = !flag && Authority; return facts; IL_0268: int allowed = 0; goto IL_0269; IL_01b1: facts.Kind = (value.Conditional ? "ConditionalConfigSync" : "ServerSync"); facts.System = value.System; facts.Data = value.Data; int hidden; if (value.Conditional) { object obj2 = Read(value.Data, "IsHidden"); hidden = ((!(obj2 is bool) || (bool)obj2) ? 1 : 0); } else { hidden = 0; } facts.Hidden = (byte)hidden != 0; if (flag || facts.Hidden) { goto IL_0268; } if (value.Conditional) { object obj2 = Read(value.Data, "IsPolicyStateInitialized"); if (!(obj2 is bool) || !(bool)obj2) { goto IL_0268; } } allowed = (Writable(value, entry) ? 1 : 0); goto IL_0269; IL_0269: facts.Allowed = (byte)allowed != 0; return Authority ? facts : new Facts(); } catch { return new Facts { Hidden = true }; } } private static bool Same(Facts first, Facts current) { if (first.Kind == current.Kind && current.Allowed && !current.Hidden && first.System == current.System) { return first.Data == current.Data; } return false; } internal static bool Apply(ConfigEntryBase entry, Action apply, Func<bool> current) { BeginRequest(); Facts facts = Inspect(entry); if (!facts.Allowed || facts.Hidden || !current()) { return false; } if (facts.Kind == "Jotunn") { facts.Cache.Invoke(facts.System, null); } BeginRequest(); if (!Authority || !Same(facts, Inspect(entry)) || !current()) { return false; } apply(); BeginRequest(); if (!Authority || !Same(facts, Inspect(entry))) { throw new InvalidOperationException("ConfigOwnerChangedAfterWrite"); } if (facts.Kind == "Jotunn") { facts.Send.Invoke(facts.System, null); } return true; } } internal static class ThingYamlFiles { private sealed class Document { internal string Token = ""; internal string Version = ""; internal string DiskHash = ""; internal string Connection = ""; internal Actor Admin; internal bool Local; internal long Generation; internal ZNet Net; internal ZNetPeer? Peer; internal ZRpc? Rpc; internal object? Socket; internal DataFileRegistration Owner; internal byte[] Original = Array.Empty<byte>(); internal YamlUpload? Upload; internal double Expires; } private const int Budget = 4194304; private static readonly Stopwatch clock = Stopwatch.StartNew(); private static readonly Dictionary<string, Document> documents = new Dictionary<string, Document>(StringComparer.Ordinal); private static readonly HashSet<string> backups = new HashSet<string>(StringComparer.Ordinal); private static long generation = -1L; private static int Reserved => documents.Values.Sum(delegate(Document d) { int num = d.Original.Length; YamlUpload? upload = d.Upload; return num + ((upload != null) ? upload.Length : 0); }); internal static DataFileRegistration? Registered(string path) { string full = Path.GetFullPath(path); StringComparison comparison = ((Path.DirectorySeparatorChar == '\\') ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); if (!full.EndsWith(".yml", comparison) && !full.EndsWith(".yaml", comparison)) { return null; } return ((IEnumerable<DataFileRegistration>)MeadworksDataFiles.Registered).SingleOrDefault((Func<DataFileRegistration, bool>)((DataFileRegistration f) => string.Equals(f.Path, full, comparison))); } private static ZNetPeer? Peer(Actor admin, string connection) { //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) if (!long.TryParse(connection, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var uid)) { return null; } ZNet instance = ZNet.instance; if (instance == null) { return null; } return ((IEnumerable<