using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using System.Threading; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Newtonsoft.Json.Linq; using Photon.Pun; using Photon.Realtime; using Steamworks; using Steamworks.Data; using UnityEngine; using com.github.zehsteam.LocalMultiplayer.Helpers; using com.github.zehsteam.LocalMultiplayer.Managers; using com.github.zehsteam.LocalMultiplayer.Objects; using com.github.zehsteam.LocalMultiplayer.Patches; [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("Zehs")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyCopyright("Copyright © 2026 Zehs")] [assembly: AssemblyDescription("Play multiplayer locally with one Steam account.")] [assembly: AssemblyFileVersion("1.5.0.0")] [assembly: AssemblyInformationalVersion("1.5.0+6f2e63edded88122654bbbefe774d456103895b8")] [assembly: AssemblyProduct("LocalMultiplayer")] [assembly: AssemblyTitle("com.github.zehsteam.LocalMultiplayer")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.5.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.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 com.github.zehsteam.LocalMultiplayer { internal static class ConfigManager { public static ConfigFile ConfigFile { get; private set; } public static ConfigEntry ExtendedLogging { get; private set; } public static ConfigEntry Photon_AppIdRealtime { get; private set; } public static ConfigEntry Photon_AppIdVoice { get; private set; } public static void Initialize(ConfigFile configFile) { ConfigFile = configFile; BindConfigs(); } private static void BindConfigs() { ExtendedLogging = ConfigFile.Bind("General", "ExtendedLogging", false, "Enable extended logging."); Photon_AppIdRealtime = ConfigFile.Bind("Photon", "AppIdRealtime", "", "The App ID of your Photon Pun application."); Photon_AppIdVoice = ConfigFile.Bind("Photon", "AppIdVoice", "", "The App ID of your Photon Voice application."); } } internal static class Logger { public static ManualLogSource ManualLogSource { get; private set; } public static void Initialize(ManualLogSource manualLogSource) { ManualLogSource = manualLogSource; } public static void LogDebug(object data, bool extended = false) { Log((LogLevel)32, data, extended); } public static void LogInfo(object data, bool extended = false) { Log((LogLevel)16, data, extended); } public static void LogMessage(object data, bool extended = false) { Log((LogLevel)8, data, extended); } public static void LogWarning(object data, bool extended = false) { Log((LogLevel)4, data, extended); } public static void LogError(object data, bool extended = false) { Log((LogLevel)2, data, extended); } public static void Log(LogLevel logLevel, object data, bool extended = false) { //IL_0015: Unknown result type (might be due to invalid IL or missing references) if (!extended || IsExtendedLoggingEnabled()) { ManualLogSource manualLogSource = ManualLogSource; if (manualLogSource != null) { manualLogSource.Log(logLevel, data); } } } public static bool IsExtendedLoggingEnabled() { if (ConfigManager.ExtendedLogging == null) { return false; } return ConfigManager.ExtendedLogging.Value; } } [BepInPlugin("com.github.zehsteam.LocalMultiplayer", "LocalMultiplayer", "1.5.0")] internal class Plugin : BaseUnityPlugin { private readonly Harmony _harmony = new Harmony("com.github.zehsteam.LocalMultiplayer"); public static Plugin Instance { get; private set; } public ConfigFile Config { get; private set; } public static JsonSave GlobalSave { get; private set; } private void Awake() { Instance = this; Logger.Initialize(Logger.CreateLogSource("com.github.zehsteam.LocalMultiplayer")); Logger.LogInfo("LocalMultiplayer has awoken!"); Config = Utils.CreateGlobalConfigFile((BaseUnityPlugin)(object)this); GlobalSave = new JsonSave(Utils.GetPluginPersistentDataPath(), "GlobalSave"); _harmony.PatchAll(typeof(SteamClient_Patches)); _harmony.PatchAll(typeof(DataDirector_Patches)); _harmony.PatchAll(typeof(NetworkManager_Patches)); _harmony.PatchAll(typeof(SteamManager_Patches)); _harmony.PatchAll(typeof(InputManager_Patches)); _harmony.PatchAll(typeof(MenuPageMain_Patches)); _harmony.PatchAll(typeof(PlayerAvatar_Patches)); ConfigManager.Initialize(Config); } } internal static class Utils { public static string GetPluginPersistentDataPath() { return Path.Combine(Application.persistentDataPath, "LocalMultiplayer"); } public static ConfigFile CreateConfigFile(BaseUnityPlugin plugin, string path, string name = null, bool saveOnInit = false) { //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Expected O, but got Unknown BepInPlugin metadata = MetadataHelper.GetMetadata((object)plugin); if (name == null) { name = metadata.GUID; } name += ".cfg"; return new ConfigFile(Path.Combine(path, name), saveOnInit, metadata); } public static ConfigFile CreateLocalConfigFile(BaseUnityPlugin plugin, string name = null, bool saveOnInit = false) { return CreateConfigFile(plugin, Paths.ConfigPath, name, saveOnInit); } public static ConfigFile CreateGlobalConfigFile(BaseUnityPlugin plugin, string name = null, bool saveOnInit = false) { string pluginPersistentDataPath = GetPluginPersistentDataPath(); if (name == null) { name = "global"; } return CreateConfigFile(plugin, pluginPersistentDataPath, name, saveOnInit); } public static string GetCurrentStackTrace(int skipFrames = 0, bool includeFileInfo = true) { StackTrace stackTrace = new StackTrace(skipFrames + 1, includeFileInfo); return stackTrace.ToString(); } public static string GetCallStackMethods(int skipFrames = 1) { StackTrace stackTrace = new StackTrace(skipFrames, fNeedFileInfo: false); StackFrame[] frames = stackTrace.GetFrames(); if (frames == null || frames.Length == 0) { return "No stack trace available."; } StringBuilder stringBuilder = new StringBuilder(); int num = 1; StackFrame[] array = frames; foreach (StackFrame stackFrame in array) { MethodBase method = stackFrame.GetMethod(); if (!(method == null)) { string name = method.Name; string arg = ((method.DeclaringType != null) ? method.DeclaringType.FullName : ""); stringBuilder.AppendLine($"{num}: {arg}.{name}()"); num++; } } return stringBuilder.ToString(); } } public static class MyPluginInfo { public const string PLUGIN_GUID = "com.github.zehsteam.LocalMultiplayer"; public const string PLUGIN_NAME = "LocalMultiplayer"; public const string PLUGIN_VERSION = "1.5.0"; } } namespace com.github.zehsteam.LocalMultiplayer.Patches { [HarmonyPatch(typeof(DataDirector))] internal static class DataDirector_Patches { [HarmonyPatch("PhotonSetAppId")] [HarmonyPostfix] private static void PhotonSetAppId_Patch() { AppSettings appSettings = PhotonNetwork.PhotonServerSettings.AppSettings; appSettings.AppIdRealtime = ConfigManager.Photon_AppIdRealtime.Value; appSettings.AppIdVoice = ConfigManager.Photon_AppIdVoice.Value; } [HarmonyPatch("SaveSettings")] [HarmonyPrefix] private static bool SaveSettings_Patch() { if (!SteamAccountManager.IsUsingSpoofAccount) { return true; } return false; } [HarmonyPatch("ColorSetBody")] [HarmonyPrefix] private static bool ColorSetBody_Patch(int colorID) { if (!SteamAccountManager.IsUsingSpoofAccount) { return true; } SteamAccountManager.SetCurrentSpoofAccountColor(colorID); return false; } [HarmonyPatch("ColorGetBody")] [HarmonyPrefix] private static bool ColorGetBody_Patch(ref int __result) { if (!SteamAccountManager.IsUsingSpoofAccount) { return true; } __result = SteamAccountManager.SpoofAccount.ColorId; return false; } } [HarmonyPatch(typeof(InputManager))] internal static class InputManager_Patches { [HarmonyPatch("SaveDefaultKeyBindings")] [HarmonyPrefix] private static bool SaveDefaultKeyBindings_Patch() { return !SteamAccountManager.IsUsingSpoofAccount; } [HarmonyPatch("SaveCurrentKeyBindings")] [HarmonyPrefix] private static bool SaveCurrentKeyBindings_Patch() { return !SteamAccountManager.IsUsingSpoofAccount; } } [HarmonyPatch(typeof(MenuPageMain))] internal static class MenuPageMain_Patches { [HarmonyPatch("ButtonEventJoinGame")] [HarmonyPrefix] private static bool ButtonEventJoinGame_Patch() { //IL_001a: 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) //IL_0024: Unknown result type (might be due to invalid IL or missing references) SteamAccountManager.AssignSpoofAccount(); SteamManager instance = SteamManager.instance; if (instance != null) { instance.OnGameLobbyJoinRequested(new Lobby(SteamId.op_Implicit(GlobalSaveHelper.SteamLobbyId.Value)), SteamClient.SteamId); } return false; } } [HarmonyPatch(typeof(NetworkManager))] internal static class NetworkManager_Patches { [HarmonyPatch("OnDisconnected")] [HarmonyPostfix] private static void OnDisconnected_Patch() { SteamAccountManager.UnassignSpoofAccount(); } } [HarmonyPatch(typeof(PlayerAvatar))] internal static class PlayerAvatar_Patches { [HarmonyPatch("AddToStatsManager")] [HarmonyPrefix] private static void AddToStatsManager_Patch() { PhotonNetwork.NickName = SteamClient.Name; } } [HarmonyPatch(typeof(SteamClient))] internal static class SteamClient_Patches { [HarmonyPatch(/*Could not decode attribute arguments.*/)] [HarmonyPrefix] private static bool Name_Patch(ref string __result) { if (!SteamAccountManager.IsUsingSpoofAccount) { return true; } __result = SteamAccountManager.SpoofAccount.Username; return false; } [HarmonyPatch(/*Could not decode attribute arguments.*/)] [HarmonyPrefix] private static bool SteamId_Patch(ref SteamId __result) { //IL_0014: 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 (!SteamAccountManager.IsUsingSpoofAccount) { return true; } __result = SteamId.op_Implicit(SteamAccountManager.SpoofAccount.SteamId); return false; } } [HarmonyPatch(typeof(SteamManager))] internal static class SteamManager_Patches { [HarmonyPatch("Awake")] [HarmonyPostfix] [HarmonyPriority(800)] private static void Awake_Patch() { SteamAccountManager.Initialize(); } [HarmonyPatch("Start")] [HarmonyPostfix] private static void Start_Patch() { if (!SteamHelper.IsValidClient()) { Application.Quit(); } } [HarmonyPatch("OnLobbyCreated")] [HarmonyPostfix] private static void OnLobbyCreated_Patch(ref Result _result, ref Lobby _lobby) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) if ((int)_result == 1) { GlobalSaveHelper.SteamLobbyId.Value = SteamId.op_Implicit(((Lobby)(ref _lobby)).Id); SteamAccountManager.ResetSpoofAccountsInUse(); } } [HarmonyPatch("SendSteamAuthTicket")] [HarmonyPrefix] private static bool SendSteamAuthTicket_Patch() { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Expected O, but got Unknown PhotonNetwork.AuthValues = new AuthenticationValues(Guid.NewGuid().ToString()); return false; } } } namespace com.github.zehsteam.LocalMultiplayer.Objects { internal class JsonSave : IDisposable { private JObject _data; private readonly Mutex _mutex; private const int _mutexTimeoutMs = 5000; private bool _disposed; public string DirectoryPath { get; private set; } public string FileName { get; private set; } public string FilePath => Path.Combine(DirectoryPath, FileName); public JsonSave(string directoryPath, string fileName) { DirectoryPath = directoryPath; FileName = fileName; string name = "Global\\JsonSave_" + fileName.Replace(Path.DirectorySeparatorChar, '_'); _mutex = new Mutex(initiallyOwned: false, name); RefreshData(); Application.quitting += Dispose; } public bool KeyExists(string key) { RefreshData(); JObject data = _data; if (data == null) { return false; } return data.ContainsKey(key); } public T Load(string key, T defaultValue = default(T)) { if (!TryLoad(key, out var value)) { return defaultValue; } return value; } public bool TryLoad(string key, out T value) { value = default(T); RefreshData(); if (_data == null) { Logger.LogError("TryLoad: Data is null. Key: " + key); return false; } JToken val = default(JToken); if (_data.TryGetValue(key, ref val)) { try { value = val.ToObject(); return true; } catch (Exception ex) { Logger.LogError("TryLoad: Failed to deserialize key '" + key + "'. " + ex.Message); } } return false; } public bool Save(string key, T value) { //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Expected O, but got Unknown bool flag = false; try { flag = _mutex.WaitOne(5000); if (!flag) { Logger.LogWarning("Save: Could not acquire mutex."); return false; } RefreshData(); if (_data == null) { _data = new JObject(); } _data[key] = JToken.FromObject((object)value); return WriteFile(_data); } catch (Exception ex) { Logger.LogError("Save: Error saving key '" + key + "'. " + ex.Message); return false; } finally { if (flag) { _mutex.ReleaseMutex(); } } } private JObject ReadFile() { //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Expected O, but got Unknown //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Expected O, but got Unknown try { if (!File.Exists(FilePath)) { Logger.LogWarning("ReadFile: Save file not found at \"" + FilePath + "\". Creating new."); return new JObject(); } using FileStream stream = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); using StreamReader streamReader = new StreamReader(stream, Encoding.UTF8); return JObject.Parse(streamReader.ReadToEnd()); } catch (Exception ex) { Logger.LogError("ReadFile: Failed to read file \"" + FilePath + "\". " + ex.Message); return new JObject(); } } private bool WriteFile(JObject data) { try { if (!Directory.Exists(DirectoryPath)) { Directory.CreateDirectory(DirectoryPath); } File.WriteAllText(FilePath, ((object)data).ToString(), Encoding.UTF8); return true; } catch (Exception ex) { Logger.LogError("WriteFile: Failed to write file \"" + FilePath + "\". " + ex.Message); return false; } } private void RefreshData() { //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Expected O, but got Unknown _data = ReadFile(); if (_data == null) { Logger.LogError("RefreshData: Data is null. Creating new."); _data = new JObject(); } } public void Dispose() { if (!_disposed) { _mutex?.Dispose(); _disposed = true; } } } internal class JsonSaveValue { public JsonSave JsonSave { get; private set; } public string Key { get; private set; } public T DefaultValue { get; private set; } public T Value { get { return Load(); } set { Save(value); } } public bool HasValue { get { T value; return TryLoad(out value); } } public JsonSaveValue(JsonSave jsonSave, string key, T defaultValue = default(T)) { JsonSave = jsonSave; Key = key; DefaultValue = defaultValue; base..ctor(); } public T Load() { return JsonSave.Load(Key, DefaultValue); } public bool TryLoad(out T value) { return JsonSave.TryLoad(Key, out value); } public void Save(T value) { JsonSave.Save(Key, value); } } public struct SteamAccount : IEquatable { public string Username; public ulong SteamId; public int ColorId; public SteamAccount(string username, ulong steamId) { ColorId = 0; Username = username; SteamId = steamId; } public SteamAccount(string username, ulong steamId, int colorIndex) : this(username, steamId) { ColorId = colorIndex; } public bool Equals(SteamAccount other) { return SteamId == other.SteamId; } public override bool Equals(object obj) { if (obj is SteamAccount other) { return Equals(other); } return false; } public static bool operator ==(SteamAccount a, SteamAccount b) { return a.Equals(b); } public static bool operator !=(SteamAccount a, SteamAccount b) { return !a.Equals(b); } public override int GetHashCode() { return SteamId.GetHashCode(); } } } namespace com.github.zehsteam.LocalMultiplayer.Managers { internal static class SteamAccountManager { public static SteamAccount SpoofAccount; private static bool _initialized; public static SteamAccount RealAccount { get; private set; } public static bool IsUsingSpoofAccount => SpoofAccount != default(SteamAccount); public static void Initialize() { //IL_000d: Unknown result type (might be due to invalid IL or missing references) if (!_initialized) { RealAccount = new SteamAccount(SteamClient.Name, SteamId.op_Implicit(SteamClient.SteamId)); CreateSpoofAccounts(); Application.quitting += UnassignSpoofAccount; _initialized = true; } } private static void CreateSpoofAccounts() { List value = GlobalSaveHelper.SpoofSteamAccounts.Value; int num = 5; if (value.Count < num) { for (int i = value.Count; i < num; i++) { value.Add(new SteamAccount($"Player {i + 2}", SteamHelper.GenerateRandomSteamId())); } GlobalSaveHelper.SpoofSteamAccounts.Value = value; } } public static void AssignSpoofAccount() { Logger.LogInfo($"SteamAccountManager: AssignSpoofAccount(); IsUsingSpoofAccount: {IsUsingSpoofAccount}"); if (!IsUsingSpoofAccount) { SteamAccount availableSpoofAccount = GetAvailableSpoofAccount(); AddSpoofAccountInUse(availableSpoofAccount); SpoofAccount = availableSpoofAccount; PhotonNetwork.NickName = availableSpoofAccount.Username; Logger.LogInfo($"SteamAccountManager: Set spoof account (Username: \"{availableSpoofAccount.Username}\", SteamID: {availableSpoofAccount.SteamId})"); } } public static void UnassignSpoofAccount() { Logger.LogInfo($"SteamAccountManager: UnassignSpoofAccount(); IsUsingSpoofAccount: {IsUsingSpoofAccount}"); if (IsUsingSpoofAccount) { RemoveSpoofAccountInUse(SpoofAccount); SpoofAccount = default(SteamAccount); PhotonNetwork.NickName = RealAccount.Username; Logger.LogInfo("SteamAccountManager: UnassignSpoofAccount(); Unassigned spoof account."); } } public static void ResetSpoofAccountsInUse() { GlobalSaveHelper.SpoofSteamAccountsInUse.Value = new List(); } public static bool TryGetSpoofAccount(ulong steamId, out SteamAccount steamAccount) { List value = GlobalSaveHelper.SpoofSteamAccounts.Value; foreach (SteamAccount item in value) { if (item.SteamId == steamId) { steamAccount = item; return true; } } steamAccount = default(SteamAccount); return false; } public static void SetCurrentSpoofAccountColor(int id) { if (IsUsingSpoofAccount) { SpoofAccount.ColorId = id; UpdateCurrentSpoofAccountData(); } } private static void UpdateCurrentSpoofAccountData() { if (!IsUsingSpoofAccount) { return; } List value = GlobalSaveHelper.SpoofSteamAccounts.Value; for (int i = 0; i < value.Count; i++) { if (value[i] == SpoofAccount) { value[i] = SpoofAccount; break; } } GlobalSaveHelper.SpoofSteamAccounts.Value = value; } private static List GetAvailableSpoofAccounts() { List list = new List(); List value = GlobalSaveHelper.SpoofSteamAccountsInUse.Value; foreach (SteamAccount item in GlobalSaveHelper.SpoofSteamAccounts.Value) { if (!value.Contains(item)) { list.Add(item); } } return list; } private static SteamAccount GetAvailableSpoofAccount() { List availableSpoofAccounts = GetAvailableSpoofAccounts(); if (availableSpoofAccounts.Count == 0) { Logger.LogWarning("SteamAccountManager: No cached spoof steam accounts available. Generating new spoof steam account."); return new SteamAccount($"Player {Random.Range(100, 999)}", SteamHelper.GenerateRandomSteamId()); } return availableSpoofAccounts[0]; } private static void AddSpoofAccountInUse(SteamAccount accountToAdd) { List value = GlobalSaveHelper.SpoofSteamAccountsInUse.Value; if (!value.Contains(accountToAdd)) { value.Add(accountToAdd); GlobalSaveHelper.SpoofSteamAccountsInUse.Value = value; } } private static void RemoveSpoofAccountInUse(SteamAccount accountToRemove) { List value = GlobalSaveHelper.SpoofSteamAccountsInUse.Value; GlobalSaveHelper.SpoofSteamAccountsInUse.Value = value.Where((SteamAccount x) => x.SteamId != accountToRemove.SteamId).ToList(); } } } namespace com.github.zehsteam.LocalMultiplayer.Helpers { internal static class GlobalSaveHelper { public static JsonSave JsonSave => Plugin.GlobalSave; public static JsonSaveValue SteamLobbyId { get; private set; } public static JsonSaveValue> SpoofSteamAccounts { get; private set; } public static JsonSaveValue> SpoofSteamAccountsInUse { get; private set; } static GlobalSaveHelper() { SteamLobbyId = new JsonSaveValue(JsonSave, "SteamLobbyId", 0uL); SpoofSteamAccounts = new JsonSaveValue>(JsonSave, "SpoofSteamAccounts", new List()); SpoofSteamAccountsInUse = new JsonSaveValue>(JsonSave, "SpoofSteamAccountsInUse", new List()); } } internal static class SteamHelper { public static ulong GenerateRandomSteamId() { ulong num = 76561197960265728uL; Random random = new Random(); uint num2 = (uint)random.Next(0, int.MaxValue); num2 += (uint)random.Next(0, int.MaxValue); return num + num2; } public static bool IsValidClient() { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) if (SteamClient.Name == "IGGGAMES") { return false; } if (SteamId.op_Implicit(SteamClient.SteamId) == 12345678) { return false; } if (AppId.op_Implicit(SteamClient.AppId) == 480) { return false; } return true; } } } namespace com.github.zehsteam.LocalMultiplayer.Extensions { internal static class StringExtensions { public static ulong ToUlong(this string value, ulong defaultValue = 0uL) { if (!ulong.TryParse(value, out var result)) { return defaultValue; } return result; } } } namespace System.Runtime.CompilerServices { [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] internal sealed class IgnoresAccessChecksToAttribute : Attribute { public IgnoresAccessChecksToAttribute(string assemblyName) { } } }