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 StreamOverlay v0.1.0
plugins/tech.rethunk.mortar.StreamOverlay.dll
Decompiled 6 hours agousing System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Net.Sockets; 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.Text.RegularExpressions; using System.Threading; using BepInEx; using BepInEx.Logging; using Microsoft.CodeAnalysis; using UnityEngine; [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("Rethunk Tech")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyCopyright("Copyright (c) 2026 Rethunk Tech")] [assembly: AssemblyFileVersion("0.1.0.0")] [assembly: AssemblyInformationalVersion("0.1.0+e21892669016fc1a83d42ed389f3024565597c52")] [assembly: AssemblyProduct("tech.rethunk.mortar.StreamOverlay")] [assembly: AssemblyTitle("StreamOverlay")] [assembly: AssemblyMetadata("RepositoryUrl", "https://github.com/Rethunk-Tech/mortar-lc-streamoverlay")] [assembly: InternalsVisibleTo("StreamOverlay.Tests")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.1.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace StreamOverlay { internal interface IOverlayProvider { string Game { get; } Dictionary<string, object?>? Read(); } internal static class OverlayProviders { public static string Snapshot(IOverlayProvider provider, Action<Exception> onError) { Dictionary<string, object> dictionary; try { dictionary = provider.Read(); } catch (Exception obj) { onError(obj); return "{\"inGame\":false}"; } if (dictionary == null) { return "{\"inGame\":false}"; } Dictionary<string, object> dictionary2 = new Dictionary<string, object> { ["inGame"] = true, ["game"] = provider.Game }; foreach (KeyValuePair<string, object> item in dictionary) { dictionary2[item.Key] = item.Value; } return OverlayJson.Write(dictionary2); } } internal sealed class LethalCompanyProvider(Func<string, Type?> find, Func<Type, object?> findObject) : IOverlayProvider { private static readonly Regex moonNumber = new Regex("^\\d+\\s+", RegexOptions.Compiled); public string Game => "lethal-company"; public Dictionary<string, object?>? Read() { object obj = Probe.Static(find("StartOfRound"), "Instance"); object obj2 = Probe.Member(obj, "localPlayerController"); if (obj == null || obj2 == null) { return null; } int num = 0; int num2 = 0; foreach (object item in (Probe.Member(obj, "allPlayerScripts") as IEnumerable) ?? Array.Empty<object>()) { object obj3 = Probe.Member(item, "isPlayerControlled"); bool flag = obj3 is bool && (bool)obj3; obj3 = Probe.Member(item, "isPlayerDead"); bool flag2 = obj3 is bool && (bool)obj3; if (flag || flag2) { num++; } if (flag && !flag2) { num2++; } } object target = Probe.Static(find("TimeOfDay"), "Instance"); string text = Probe.Member(Probe.Member(obj, "currentLevel"), "PlanetName")?.ToString(); Dictionary<string, object> obj4 = new Dictionary<string, object> { ["playerName"] = Probe.Member(obj2, "playerUsername")?.ToString(), ["moon"] = ((text == null) ? null : moonNumber.Replace(text, "")), ["crewAlive"] = num2, ["crewTotal"] = num, ["day"] = Probe.Int(Probe.Member(Probe.Member(obj, "gameStats"), "daysSpent")), ["quota"] = Probe.Int(Probe.Member(target, "profitQuota")), ["quotaProgress"] = Probe.Int(Probe.Member(target, "quotaFulfilled")), ["daysLeft"] = Probe.Int(Probe.Member(target, "daysUntilDeadline")) }; Type type = find("Terminal"); obj4["credits"] = Probe.Int(Probe.Member(((object)type != null) ? findObject(type) : null, "groupCredits")); return obj4; } } internal sealed class OverlayConfig(int port, string token) { public const string File = "startup/overlay.json"; internal const int MinTokenLength = 32; private static readonly Regex enabledKey = new Regex("\"OverlayEnabled\"\\s*:\\s*(true|false)", RegexOptions.Compiled); private static readonly Regex portKey = new Regex("\"OverlayPort\"\\s*:\\s*(\\d+)", RegexOptions.Compiled); private static readonly Regex tokenKey = new Regex("\"OverlayToken\"\\s*:\\s*\"([A-Za-z0-9_-]+)\"", RegexOptions.Compiled); public int Port { get; } = port; public string Token { get; } = token; public static OverlayConfig? Parse(string? json) { if (json == null) { return null; } Match match = enabledKey.Match(json); Match match2 = portKey.Match(json); Match match3 = tokenKey.Match(json); if (!match.Success || match.Groups[1].Value != "true" || !match2.Success || !match3.Success) { return null; } int result; bool flag = !int.TryParse(match2.Groups[1].Value, out result); bool flag2 = flag; if (!flag2) { bool flag3 = ((result < 1 || result > 65535) ? true : false); flag2 = flag3; } if (flag2) { return null; } if (match3.Groups[1].Value.Length < 32) { return null; } return new OverlayConfig(result, match3.Groups[1].Value); } public static string? InvalidWhenEnabled(string? json) { if (json == null) { return null; } Match match = enabledKey.Match(json); if (!match.Success || match.Groups[1].Value != "true") { return null; } if (Parse(json) != null) { return null; } Match match2 = portKey.Match(json); Match match3 = tokenKey.Match(json); if (!match2.Success || !match3.Success) { return "overlay.json is enabled but OverlayPort or OverlayToken is missing"; } int result; bool flag = !int.TryParse(match2.Groups[1].Value, out result); bool flag2 = flag; if (!flag2) { bool flag3 = ((result < 1 || result > 65535) ? true : false); flag2 = flag3; } if (flag2) { return "overlay.json OverlayPort is not a TCP port"; } return "overlay.json OverlayToken is too short"; } } internal static class OverlayJson { public static string Write(IReadOnlyDictionary<string, object?> values) { StringBuilder stringBuilder = new StringBuilder("{"); bool flag = true; foreach (KeyValuePair<string, object> item in values.Where<KeyValuePair<string, object>>((KeyValuePair<string, object> pair) => pair.Value != null)) { if (!flag) { stringBuilder.Append(','); } flag = false; stringBuilder.Append(Quote(item.Key)).Append(':'); Append(stringBuilder, item.Value); } return stringBuilder.Append('}').ToString(); } private static string Quote(string? value) { StringBuilder stringBuilder = new StringBuilder("\""); string text = value ?? ""; foreach (char c in text) { if (c >= ' ') { switch (c) { case '"': stringBuilder.Append("\\\""); break; case '\\': stringBuilder.Append("\\\\"); break; default: stringBuilder.Append(c); break; } } else { StringBuilder stringBuilder2 = stringBuilder.Append("\\u"); int num = c; stringBuilder2.Append(num.ToString("x4")); } } return stringBuilder.Append('"').ToString(); } private static void Append(StringBuilder sb, object value) { if (!(value is bool flag)) { if (!(value is string value2)) { if (!(value is int) && !(value is long) && !(value is short)) { if (!(value is double num)) { if (value is IEnumerable enumerable) { sb.Append('['); bool flag2 = true; foreach (object item in enumerable) { if (item != null) { if (!flag2) { sb.Append(','); } flag2 = false; Append(sb, item); } } sb.Append(']'); } else { sb.Append(Quote(value.ToString())); } } else { sb.Append(num.ToString("R", CultureInfo.InvariantCulture)); } } else { sb.Append(Convert.ToString(value, CultureInfo.InvariantCulture)); } } else { sb.Append(Quote(value2)); } } else { sb.Append(flag ? "true" : "false"); } } } internal sealed class OverlayRunner : MonoBehaviour { private const float IntervalSeconds = 0.5f; private OverlayServer? server; private IOverlayProvider? provider; private ManualLogSource? log; private float next; private string lastError = ""; public static void Start(OverlayServer server, IOverlayProvider provider, ManualLogSource log) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Expected O, but got Unknown GameObject val = new GameObject("StreamOverlay") { hideFlags = (HideFlags)61 }; Object.DontDestroyOnLoad((Object)(object)val); OverlayRunner overlayRunner = val.AddComponent<OverlayRunner>(); overlayRunner.server = server; overlayRunner.provider = provider; overlayRunner.log = log; } private void Update() { if (Time.unscaledTime < next || server == null || provider == null) { return; } next = Time.unscaledTime + 0.5f; server.Set(OverlayProviders.Snapshot(provider, delegate(Exception ex) { if (ex.Message != lastError) { ManualLogSource? obj = log; if (obj != null) { obj.LogWarning((object)("Overlay read failed: " + ex.Message)); } } lastError = ex.Message; })); } } internal sealed class OverlayServer : IDisposable { public const string NotInGame = "{\"inGame\":false}"; private const int MaxLineBytes = 4096; private const int MaxHeaderLines = 64; private const int MaxClients = 8; private const int ClientTimeoutMs = 5000; private const int UnauthTimeoutMs = 250; private readonly TcpListener listener = new TcpListener(IPAddress.Loopback, port); private readonly string token; private volatile string snapshot; private int active; private volatile bool stopped; public OverlayServer(int port, string token) { this.token = token; snapshot = "{\"inGame\":false}"; base..ctor(); } public void Set(string json) { snapshot = json; } public void Start() { listener.Start(); Thread thread = new Thread(AcceptLoop); thread.IsBackground = true; thread.Name = "StreamOverlay"; thread.Start(); } public void Dispose() { stopped = true; listener.Stop(); } internal string Respond(Stream stream) { string text = ReadLine(stream, 4096); Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); int num = 0; while (text != null) { string text3; string text2 = (text3 = ReadLine(stream, 4096)); if (text2 == null || text2.Length <= 0) { break; } if (++num > 64) { return Response(431, "{\"error\":\"too many headers\"}"); } int num2 = text3.IndexOf(':'); if (num2 > 0) { dictionary[text3.Substring(0, num2).Trim()] = text3.Substring(num2 + 1).Trim(); } } return Answer(text, dictionary); } internal string Answer(string? request, IReadOnlyDictionary<string, string> headers) { string[] array = (request ?? "").Split(new char[1] { ' ' }, 3, StringSplitOptions.RemoveEmptyEntries); if (array.Length != 3) { return Response(400, "{\"error\":\"bad request\"}"); } string text = array[0]; bool flag; switch (text) { case "GET": case "HEAD": case "OPTIONS": flag = true; break; default: flag = false; break; } if (!flag) { return Response(405, "{\"error\":\"method not allowed\"}"); } string text2 = array[1]; int num = text2.IndexOf('?'); if (((num < 0) ? text2 : text2.Substring(0, num)) != "/state") { return Response(404, "{\"error\":\"not found\"}"); } if (text == "OPTIONS") { return Response(204, "", "Access-Control-Allow-Methods: GET, HEAD, OPTIONS\r\nAccess-Control-Allow-Headers: Authorization\r\n"); } string provided; try { provided = TokenOf((num < 0) ? "" : text2.Substring(num + 1), headers); } catch (UriFormatException) { return Response(400, "{\"error\":\"bad request\"}"); } if (!TokenMatches(token, provided)) { return Response(401, "{\"error\":\"unauthorized\"}", "WWW-Authenticate: Bearer\r\n"); } return Response(200, snapshot, "", text == "HEAD"); } private void AcceptLoop() { while (!stopped) { TcpClient client; try { client = listener.AcceptTcpClient(); } catch (Exception ex) when (((ex is SocketException || ex is ObjectDisposedException || ex is InvalidOperationException) ? 1 : 0) != 0) { break; } if (!TryAdmit(client)) { continue; } ThreadPool.QueueUserWorkItem(delegate { try { Serve(client); } finally { Interlocked.Decrement(ref active); } }); } } private bool TryAdmit(TcpClient client) { if (Interlocked.Increment(ref active) <= 8) { return true; } Interlocked.Decrement(ref active); client.Close(); return false; } private void Serve(TcpClient client) { using (client) { try { client.ReceiveTimeout = 250; client.SendTimeout = 5000; NetworkStream stream = client.GetStream(); byte[] bytes = Encoding.UTF8.GetBytes(Respond(stream)); stream.Write(bytes, 0, bytes.Length); } catch (Exception ex) when (((ex is IOException || ex is SocketException || ex is ObjectDisposedException) ? 1 : 0) != 0) { } } } private static string? ReadLine(Stream stream, int max) { List<byte> list = new List<byte>(); int num; while ((num = stream.ReadByte()) >= 0) { if (num == 10) { return Encoding.UTF8.GetString(new ReadOnlySpan<byte>(list.ToArray())).TrimEnd('\r'); } if (list.Count >= max) { return null; } list.Add((byte)num); } return null; } private static bool TokenMatches(string expected, string? provided) { if (provided == null) { return false; } byte[] bytes = Encoding.UTF8.GetBytes(expected); byte[] bytes2 = Encoding.UTF8.GetBytes(provided); if (bytes.Length == bytes2.Length) { return CryptographicOperations.FixedTimeEquals(bytes, bytes2); } return false; } private static string? TokenOf(string query, IReadOnlyDictionary<string, string> headers) { if (headers.TryGetValue("Authorization", out string value) && value.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) { return value.Substring("Bearer ".Length).Trim(); } string[] array = query.Split(new char[1] { '&' }, StringSplitOptions.RemoveEmptyEntries); foreach (string text in array) { int num = text.IndexOf('='); if (num > 0 && UnescapeQuery(text.Substring(0, num)) == "token") { return UnescapeQuery(text.Substring(num + 1).Replace('+', ' ')); } } return null; } private static string UnescapeQuery(string s) { for (int i = 0; i < s.Length; i++) { if (s[i] == '%') { if (i + 2 >= s.Length || !Uri.IsHexDigit(s[i + 1]) || !Uri.IsHexDigit(s[i + 2])) { throw new UriFormatException(); } i += 2; } } return Uri.UnescapeDataString(s); } private static string Response(int status, string body, string extraHeaders = "", bool head = false) { string text = status switch { 200 => "OK", 204 => "No Content", 400 => "Bad Request", 401 => "Unauthorized", 404 => "Not Found", 405 => "Method Not Allowed", 431 => "Request Header Fields Too Large", _ => "Error", }; int num = ((status != 204) ? Encoding.UTF8.GetByteCount(body) : 0); string text2 = ((head || status == 204) ? "" : body); return $"HTTP/1.1 {status} {text}\r\nContent-Type: application/json\r\nContent-Length: {num}\r\nCache-Control: no-store\r\nAccess-Control-Allow-Origin: *\r\n{extraHeaders}Connection: close\r\n\r\n{text2}"; } } [BepInPlugin("tech.rethunk.mortar.StreamOverlay", "StreamOverlay", "0.1.0")] [BepInProcess("Lethal Company.exe")] public class Plugin : BaseUnityPlugin { private const int MaxTextBytes = 65536; private readonly Dictionary<Type, Object> found = new Dictionary<Type, Object>(); private OverlayServer? server; internal static ManualLogSource Log { get; private set; } private void Awake() { Log = ((BaseUnityPlugin)this).Logger; Log.LogInfo((object)"StreamOverlay 0.1.0 loaded"); string json = ReadProfileFile("startup/overlay.json"); OverlayConfig overlayConfig = OverlayConfig.Parse(json); if (overlayConfig == null) { string text = OverlayConfig.InvalidWhenEnabled(json); if (text != null) { Log.LogWarning((object)("Stream overlay is off: " + text + ".")); } return; } OverlayServer overlayServer; try { overlayServer = new OverlayServer(overlayConfig.Port, overlayConfig.Token); overlayServer.Start(); } catch (SocketException ex) { Log.LogError((object)$"Stream overlay is off: could not bind 127.0.0.1:{overlayConfig.Port}: {ex.Message}"); return; } server = overlayServer; OverlayRunner.Start(overlayServer, new LethalCompanyProvider(Probe.Find, FindObject), Log); Application.quitting += Shutdown; Log.LogInfo((object)$"Stream overlay on 127.0.0.1:{overlayConfig.Port}."); } private void Shutdown() { server?.Dispose(); server = null; } private object? FindObject(Type type) { if (!found.TryGetValue(type, out Object value) || !Object.op_Implicit(value)) { value = Object.FindObjectOfType(type); if (Object.op_Implicit(value)) { found[type] = value; } else { found.Remove(type); } } if (!Object.op_Implicit(value)) { return null; } return value; } private static string? ReadProfileFile(string relative) { string text = Path.GetDirectoryName(Paths.BepInExRootPath) ?? ""; string error; string result = ReadText(Path.Join(text.AsSpan(), relative.AsSpan()), out error); if (error != null) { Log.LogWarning((object)("Could not read " + relative + ": " + error)); } return result; } private static string? ReadText(string path, out string? error) { error = null; try { using FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); if (fileStream.Length > 65536) { error = $"file exceeds {65536} bytes"; return null; } using StreamReader streamReader = new StreamReader(fileStream); return streamReader.ReadToEnd(); } catch (Exception ex) when (((ex is FileNotFoundException || ex is DirectoryNotFoundException) ? 1 : 0) != 0) { return null; } catch (Exception ex2) when (((ex2 is IOException || ex2 is UnauthorizedAccessException) ? 1 : 0) != 0) { error = ex2.Message; return null; } } } internal static class Probe { private const BindingFlags Any = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; private static readonly ConcurrentDictionary<string, Type?> types = new ConcurrentDictionary<string, Type>(); public static Type? Find(string name) { if (types.TryGetValue(name, out Type value)) { return value; } value = (from a in AppDomain.CurrentDomain.GetAssemblies() select a.GetType(name, throwOnError: false)).FirstOrDefault((Type t) => t != null); types.TryAdd(name, value); return value; } public static object? Static(Type? type, string name) { if (!(type == null)) { return Read(type, null, name); } return null; } public static object? Member(object? target, string name) { if (target != null) { return Read(target.GetType(), target, name); } return null; } public static int? Int(object? value) { if (!(value is int value2)) { if (!(value is uint value3)) { if (!(value is long num)) { if (!(value is short value4)) { if (!(value is ushort value5)) { if (!(value is byte value6)) { if (!(value is sbyte value7)) { if (!(value is float num2)) { if (value is double num3) { return (int)num3; } return null; } return (int)num2; } return value7; } return value6; } return value5; } return value4; } return (int)num; } return (int)value3; } return value2; } private static object? Read(Type type, object? target, string name) { Type type2 = type; while (type2 != null) { FieldInfo field = type2.GetField(name, BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (field != null) { return field.GetValue(field.IsStatic ? null : target); } PropertyInfo property = type2.GetProperty(name, BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (property != null && property.GetIndexParameters().Length == 0 && property.CanRead) { return property.GetValue(property.GetGetMethod(nonPublic: true).IsStatic ? null : target); } type2 = type2.BaseType; } return null; } } internal static class MyPluginInfo { public const string Id = "tech.rethunk.mortar.StreamOverlay"; public const string Name = "StreamOverlay"; public const string Version = "0.1.0"; } }