Decompiled source of StreamOverlay v0.1.0

plugins/tech.rethunk.mortar.StreamOverlay.dll

Decompiled 6 hours ago
using 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";
	}
}