Decompiled source of DevLaunch v0.1.1

plugins/tech.rethunk.mortar.DevLaunch.dll

Decompiled 21 hours ago
using System;
using System.Collections;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Threading;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using ES3Internal;
using Microsoft.CodeAnalysis;
using MonoMod.RuntimeDetour;
using Steamworks;
using Unity.Netcode;
using Unity.Netcode.Transports.UTP;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.SceneManagement;

[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.1.0")]
[assembly: AssemblyInformationalVersion("0.1.1+2f64a2e84d6c051e6cfb058b6084defc73e778bf")]
[assembly: AssemblyProduct("tech.rethunk.mortar.DevLaunch")]
[assembly: AssemblyTitle("DevLaunch")]
[assembly: AssemblyMetadata("RepositoryUrl", "https://github.com/Rethunk-Tech/mortar-lc-devlaunch")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.1.1.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 DevLaunch
{
	internal enum Corner
	{
		TopLeft,
		TopRight,
		BottomLeft,
		BottomRight
	}
	internal sealed class Badge : MonoBehaviour
	{
		private static ConfigEntry<bool> inTitle;

		private static ConfigEntry<bool> inHud;

		private static ConfigEntry<Corner> corner;

		private static ConfigEntry<string> color;

		private static ConfigEntry<float> size;

		private GUIStyle? style;

		private string shown = "";

		private long state = long.MinValue;

		public static void Bind(ConfigFile config)
		{
			//IL_0099: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a3: Expected O, but got Unknown
			inTitle = config.Bind<bool>("Label", "Show in title", true, "Put which copy this is (Host, Client 2, ...) in the window title.");
			inHud = config.Bind<bool>("Label", "Show in game HUD", true, "Draw which copy this is over the game.");
			corner = config.Bind<Corner>("Label", "Corner in HUD", Corner.TopLeft, "Which corner of the window to draw the name in.");
			color = config.Bind<string>("Label", "Text color", "#FFFFFF", "The colour of the name drawn over the game, as a hex colour like #FFFFFF.");
			size = config.Bind<float>("Label", "Text size", 1f, new ConfigDescription("The size of the name drawn over the game, relative to the window. At 1 it's a thirtieth of the window's height.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 4f), Array.Empty<object>()));
		}

		public static void Show()
		{
			//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("DevLaunch")
			{
				hideFlags = (HideFlags)61
			};
			Object.DontDestroyOnLoad((Object)(object)val);
			val.AddComponent<Badge>();
			Tile.Add(val);
		}

		private void Awake()
		{
			inTitle.SettingChanged += delegate
			{
				state = long.MinValue;
			};
		}

		private void Update()
		{
			NetworkManager singleton = NetworkManager.Singleton;
			long num = (((Object)(object)singleton != (Object)null && singleton.IsHost) ? (-1) : (((Object)(object)singleton != (Object)null && singleton.IsConnectedClient) ? ((long)singleton.LocalClientId) : (-2L)));
			if (num != state)
			{
				state = num;
				shown = ((num == -1) ? "Host" : ((num >= 0) ? $"Client {num + 1}" : (Plugin.Launch.Host ? "Host" : "Client")));
				Plugin.Log.LogInfo((object)("This copy: " + shown));
				WindowTitle.Set(inTitle.Value ? ("Lethal Company - " + shown) : "Lethal Company");
			}
		}

		private static Color TextColor()
		{
			//IL_003a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0034: Unknown result type (might be due to invalid IL or missing references)
			string text = color.Value.Trim();
			Color result = default(Color);
			if (!ColorUtility.TryParseHtmlString(text.StartsWith("#") ? text : ("#" + text), ref result))
			{
				return Color.white;
			}
			return result;
		}

		private void OnGUI()
		{
			//IL_0068: Unknown result type (might be due to invalid IL or missing references)
			//IL_0040: Unknown result type (might be due to invalid IL or missing references)
			//IL_0045: Unknown result type (might be due to invalid IL or missing references)
			//IL_0051: Expected O, but got Unknown
			//IL_0113: Unknown result type (might be due to invalid IL or missing references)
			if (inHud.Value)
			{
				int num = Mathf.Max(12, (int)((float)Screen.height / 30f * size.Value));
				if (style == null)
				{
					style = new GUIStyle(GUI.skin.label)
					{
						fontStyle = (FontStyle)1
					};
				}
				style.fontSize = num;
				style.normal.textColor = TextColor();
				Corner value = corner.Value;
				bool flag = ((value == Corner.TopLeft || value == Corner.BottomLeft) ? true : false);
				bool flag2 = flag;
				value = corner.Value;
				flag = (uint)value <= 1u;
				bool flag3 = flag;
				style.alignment = (TextAnchor)((!flag3) ? (flag2 ? 6 : 8) : ((!flag2) ? 2 : 0));
				float num2 = num * 12;
				float num3 = num * 2;
				float num4 = (flag2 ? ((float)num) : ((float)(Screen.width - num) - num2));
				float num5 = (flag3 ? ((float)num / 2f) : ((float)Screen.height - (float)num / 2f - num3));
				GUI.Label(new Rect(num4, num5, num2, num3), shown, style);
			}
		}
	}
	internal static class Menus
	{
		private const int JoinAttempts = 10;

		private const int SaveAttempts = 40;

		private static Hook? launchModeHook;

		private static Hook? menuHook;

		private static Hook? screenModeHook;

		private static Hook? bootHook;

		private static Hook? saveHook;

		private static bool started;

		public static void Hook()
		{
			launchModeHook = Detour(typeof(PreInitSceneScript).GetMethod("SetLaunchPanelsEnabled"), new Action<Action<PreInitSceneScript>, PreInitSceneScript>(PickLaunchMode));
			menuHook = Detour(typeof(MenuManager).GetMethod("Start", BindingFlags.Instance | BindingFlags.NonPublic), new Action<Action<MenuManager>, MenuManager>(StartGame));
			screenModeHook = Detour(typeof(IngamePlayerSettings).GetMethod("SetFullscreenMode", BindingFlags.Instance | BindingFlags.NonPublic), new Action<Action<IngamePlayerSettings, int>, IngamePlayerSettings, int>(KeepTiled));
			bootHook = Detour(typeof(InitializeGame).GetMethod("Awake", BindingFlags.Instance | BindingFlags.NonPublic), new Action<Action<InitializeGame>, InitializeGame>(SkipBootUp));
			saveHook = Detour(typeof(ES3Stream).GetMethod("CreateStream", new Type[2]
			{
				typeof(ES3Settings),
				typeof(ES3FileMode)
			}), new Func<Func<ES3Settings, ES3FileMode, Stream>, ES3Settings, ES3FileMode, Stream>(SharedSave));
		}

		private static Stream SharedSave(Func<ES3Settings, ES3FileMode, Stream> orig, ES3Settings settings, ES3FileMode mode)
		{
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			int num = 1;
			while (true)
			{
				try
				{
					return orig(settings, mode);
				}
				catch (IOException ex) when (num < 40 && ex.Message.Contains("Sharing violation"))
				{
					Thread.Sleep(50);
				}
				num++;
			}
		}

		private static void SkipBootUp(Action<InitializeGame> orig, InitializeGame self)
		{
			orig(self);
			self.runBootUpScreen = false;
			self.playColdOpenCinematic = false;
			self.playColdOpenCinematic2 = false;
		}

		private static Hook? Detour(MethodInfo? method, Delegate to)
		{
			//IL_0031: Unknown result type (might be due to invalid IL or missing references)
			//IL_0037: Expected O, but got Unknown
			if (method == null)
			{
				Plugin.Log.LogError((object)("Couldn't find the game method that " + to.Method.Name + " hooks. A game update may have changed it, so that part is off"));
				return null;
			}
			return new Hook((MethodBase)method, to);
		}

		private static void KeepTiled(Action<IngamePlayerSettings, int> orig, IngamePlayerSettings self, int value)
		{
			orig(self, value);
			Tile.Apply();
		}

		private static void PickLaunchMode(Action<PreInitSceneScript> orig, PreInitSceneScript self)
		{
			bool? online = Plugin.Launch.Online;
			if (online.HasValue)
			{
				bool valueOrDefault = online == true;
				self.ChooseLaunchOption(valueOrDefault);
			}
			else
			{
				orig(self);
			}
		}

		private static void StartGame(Action<MenuManager> orig, MenuManager self)
		{
			orig(self);
			if (Plugin.Launch.Online == false && !self.isInitScene)
			{
				self.lanWarningContainer.SetActive(false);
			}
			if (started || self.isInitScene)
			{
				return;
			}
			started = true;
			if (Plugin.Launch.Host)
			{
				self.SetLoadingScreen(true, (RoomEnter)5, "");
				((MonoBehaviour)self).StartCoroutine(Host(self));
				return;
			}
			string joinAddress = Plugin.Launch.JoinAddress;
			if (joinAddress != null)
			{
				if (!GameNetworkManager.Instance.disableSteam)
				{
					Plugin.Log.LogWarning((object)"--join needs LAN mode; online games are joined through Steam");
					return;
				}
				self.SetLoadingScreen(true, (RoomEnter)5, "");
				((MonoBehaviour)self).StartCoroutine(Join(self, joinAddress));
			}
		}

		private static IEnumerator Host(MenuManager menu)
		{
			yield return null;
			Plugin.Log.LogInfo((object)"Hosting a game");
			menu.ClickHostButton();
			menu.ConfirmHostButton();
		}

		private static IEnumerator Join(MenuManager menu, string address)
		{
			((Component)NetworkManager.Singleton).GetComponent<UnityTransport>().ConnectionData.Address = address;
			for (int attempt = 1; attempt <= 10; attempt++)
			{
				yield return (object)new WaitForSeconds(2f);
				Plugin.Log.LogInfo((object)$"Joining {address}, attempt {attempt}");
				menu.menuNotification.SetActive(false);
				menu.StartAClient();
				menu.SetLoadingScreen(true, (RoomEnter)5, "");
				float giveUp = Time.realtimeSinceStartup + 12f;
				while (Time.realtimeSinceStartup < giveUp)
				{
					if (NetworkManager.Singleton.IsConnectedClient)
					{
						yield break;
					}
					yield return null;
				}
				NetworkManager.Singleton.Shutdown(false);
			}
			Plugin.Log.LogWarning((object)("No game to join at " + address));
			menu.SetLoadingScreen(false, (RoomEnter)5, "");
		}
	}
	[BepInPlugin("tech.rethunk.mortar.DevLaunch", "DevLaunch", "0.1.1")]
	[BepInProcess("Lethal Company.exe")]
	public class Plugin : BaseUnityPlugin
	{
		internal static ManualLogSource Log { get; private set; } = null;

		internal static Launch Launch { get; private set; } = new Launch();

		private void Awake()
		{
			Log = ((BaseUnityPlugin)this).Logger;
			Badge.Bind(((BaseUnityPlugin)this).Config);
			Launch = DevLaunch.Launch.From(Environment.GetCommandLineArgs());
			Log.LogInfo((object)"DevLaunch 0.1.1 loaded");
			if (Launch.Online.HasValue || Launch.Host || Launch.JoinAddress != null || Launch.Tile.HasValue)
			{
				Splash.Stop();
				Menus.Hook();
				Badge.Show();
			}
		}
	}
	internal sealed class Launch
	{
		public bool? Online { get; private set; }

		public bool Host { get; private set; }

		public string? JoinAddress { get; private set; }

		public (int Index, int Count)? Tile { get; private set; }

		public static Launch From(string[] args)
		{
			Launch launch = new Launch();
			for (int i = 0; i < args.Length; i++)
			{
				switch (args[i])
				{
				case "--online":
					launch.Online = true;
					break;
				case "--lan":
					launch.Online = false;
					break;
				case "--host":
					launch.Host = true;
					break;
				case "--join":
					if (i + 1 < args.Length)
					{
						launch.JoinAddress = args[++i];
					}
					break;
				case "--tile":
					if (i + 1 < args.Length)
					{
						string[] array = args[++i].Split('/');
						if (array.Length == 2 && int.TryParse(array[0], out var result) && int.TryParse(array[1], out var result2) && result2 > 0 && result >= 1 && result <= result2)
						{
							launch.Tile = (result - 1, result2);
						}
					}
					break;
				}
			}
			return launch;
		}
	}
	internal static class Splash
	{
		private static readonly TimeSpan wait = TimeSpan.FromSeconds(30.0);

		private static volatile bool sceneLoaded;

		public static void Stop()
		{
			SceneManager.sceneLoaded += delegate
			{
				sceneLoaded = true;
			};
			Thread thread = new Thread(Ask);
			thread.IsBackground = true;
			thread.Name = "DevLaunchSplash";
			thread.Start();
		}

		private static void Ask()
		{
			Stopwatch stopwatch = Stopwatch.StartNew();
			while (!sceneLoaded && stopwatch.Elapsed < wait)
			{
				SplashScreen.Stop((StopBehavior)0);
				Thread.Sleep(10);
			}
		}
	}
	internal sealed class Tile : MonoBehaviour
	{
		private const int TitleBar = 40;

		private static Tile? instance;

		public static void Add(GameObject holder)
		{
			holder.AddComponent<Tile>();
		}

		public static void Apply()
		{
			if ((Object)(object)instance != (Object)null)
			{
				(int, int)? tile = Plugin.Launch.Tile;
				if (tile.HasValue)
				{
					(int, int) valueOrDefault = tile.GetValueOrDefault();
					((MonoBehaviour)instance).StopAllCoroutines();
					((MonoBehaviour)instance).StartCoroutine(Place(valueOrDefault.Item1, valueOrDefault.Item2));
				}
			}
		}

		private void Awake()
		{
			instance = this;
			Apply();
		}

		private static IEnumerator Place(int index, int count)
		{
			DisplayInfo display = Screen.mainWindowDisplayInfo;
			RectInt area = (RectInt)((((RectInt)(ref display.workArea)).width > 0) ? display.workArea : new RectInt(0, 0, display.width, display.height));
			int columns = (int)Math.Ceiling(Math.Sqrt(count));
			int num = (int)Math.Ceiling((double)count / (double)columns);
			int width = ((RectInt)(ref area)).width / columns;
			int height = ((RectInt)(ref area)).height / num;
			Screen.SetResolution(width, height - 40, (FullScreenMode)3);
			yield return null;
			Vector2Int val = default(Vector2Int);
			((Vector2Int)(ref val))..ctor(((RectInt)(ref area)).x + index % columns * width, ((RectInt)(ref area)).y + index / columns * height);
			Screen.MoveMainWindowTo(ref display, val);
			Plugin.Log.LogInfo((object)$"Window {index + 1} of {count}: {width}x{height - 40} at {((Vector2Int)(ref val)).x},{((Vector2Int)(ref val)).y} on {display.name}");
		}
	}
	internal static class WindowTitle
	{
		private delegate bool WindowVisitor(IntPtr window, IntPtr data);

		private static readonly WindowVisitor visit = Rename;

		private static string title = "";

		private static uint process;

		public static void Set(string text)
		{
			title = text;
			process = (uint)Process.GetCurrentProcess().Id;
			try
			{
				EnumWindows(visit, IntPtr.Zero);
			}
			catch (Exception ex) when (((ex is DllNotFoundException || ex is EntryPointNotFoundException) ? 1 : 0) != 0)
			{
			}
		}

		private static bool Rename(IntPtr window, IntPtr data)
		{
			GetWindowThreadProcessId(window, out var processId);
			if (processId == process && IsWindowVisible(window))
			{
				SetWindowTextW(window, title);
			}
			return true;
		}

		[DllImport("user32.dll")]
		private static extern bool EnumWindows(WindowVisitor visitor, IntPtr data);

		[DllImport("user32.dll")]
		private static extern uint GetWindowThreadProcessId(IntPtr window, out uint processId);

		[DllImport("user32.dll")]
		private static extern bool IsWindowVisible(IntPtr window);

		[DllImport("user32.dll", CharSet = CharSet.Unicode)]
		private static extern bool SetWindowTextW(IntPtr window, string text);
	}
	internal static class MyPluginInfo
	{
		public const string Id = "tech.rethunk.mortar.DevLaunch";

		public const string Name = "DevLaunch";

		public const string Version = "0.1.1";
	}
}