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 EasyLogin v1.0.0
plugins/EasyLogin/EasyLogin.dll
Decompiled a week agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("EasyLogin")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("EasyLogin")] [assembly: AssemblyTitle("EasyLogin")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [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 EasyLogin { [BepInPlugin("easylogin.client.only", "EasyLogin", "1.0.0")] public class EasyLoginPlugin : BaseUnityPlugin { public const string Guid = "easylogin.client.only"; public const string PluginName = "EasyLogin"; public const string PluginVersion = "1.0.0"; internal static ManualLogSource Log; internal static ConfigEntry<bool> Enabled; internal static ConfigEntry<bool> AutoSubmit; internal static ConfigEntry<bool> ForgetOnPasswordError; internal static ConfigEntry<bool> RememberManualInput; internal static ConfigEntry<bool> MatchByJoinCode; internal static ConfigEntry<bool> EncryptWithWindowsUserKey; internal static ConfigEntry<bool> ShowPasswordInLog; internal static ConfigEntry<bool> VerboseLog; private Harmony harmony; private float backfillTimer; private void Awake() { Log = ((BaseUnityPlugin)this).Logger; Enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("1 - 自动登录", "Enabled", true, "总开关。关闭后模组完全不介入,和原版一样每次手输密码。"); AutoSubmit = ((BaseUnityPlugin)this).Config.Bind<bool>("1 - 自动登录", "AutoSubmit", true, "true = 认出服务器后直接提交密码(你会感觉「秒进」)。\nfalse = 只把密码填进输入框,弹窗保持显示,等你自己按回车确认。"); ForgetOnPasswordError = ((BaseUnityPlugin)this).Config.Bind<bool>("1 - 自动登录", "ForgetOnPasswordError", true, "服务器说密码不对时,把这条记录删掉。\n★ 强烈建议保持 true:否则房主改了密码后,你会一直自动填那个旧密码,\n 连密码框都弹不出来,等于把自己锁在门外(只能手动删 json)。"); RememberManualInput = ((BaseUnityPlugin)this).Config.Bind<bool>("1 - 自动登录", "RememberManualInput", true, "你手动敲进去的密码也记下来。\n关掉的话,只有「已经记录过的服务器」才会自动填,新服务器不会新增记录。"); MatchByJoinCode = ((BaseUnityPlugin)this).Config.Bind<bool>("2 - 服务器识别", "MatchByJoinCode", true, "除了 host 的 PlayFab ID 之外,再把跨平台联机的「房间码」当成一层兜底的识别依据。\n只在跨平台后端下生效,Steam / 直连 IP 两条路不受影响。"); EncryptWithWindowsUserKey = ((BaseUnityPlugin)this).Config.Bind<bool>("3 - 存储", "EncryptWithWindowsUserKey", true, "用 Windows 用户级密钥(DPAPI)加密密码,只有当前这台机器的这个 Windows 账号能解。\n如果运行环境不支持,会自动退化成「Base64 + 异或」的简单混淆并打一行警告,功能不受影响。\n注意:开了这一项之后,把 servers.json 拷到别的电脑/别的 Windows 账号下会读不出来。"); ShowPasswordInLog = ((BaseUnityPlugin)this).Config.Bind<bool>("3 - 存储", "ShowPasswordInLog", false, "把密码原文打进 BepInEx 日志(只在排错时开,平时千万别开)。"); VerboseLog = ((BaseUnityPlugin)this).Config.Bind<bool>("4 - 日志", "VerboseLog", false, "详细日志:每次识别服务器都把 key / 房间码 / IP 等打出来。"); Log.LogInfo((object)string.Format("{0} {1} 开始挂载 Harmony 补丁……存储文件:{2}", "EasyLogin", "1.0.0", PasswordStore.FilePath)); PasswordStore.Init(); ServerIdentity.Init(); ApplyPatches(); VerifyPatches(); Log.LogInfo((object)$"EasyLogin 就绪:已载入 {PasswordStore.Count} 条服务器密码记录。"); } private void ApplyPatches() { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Expected O, but got Unknown harmony = new Harmony("easylogin.client.only"); Type[] array = new Type[3] { typeof(Patch_ClientHandshake), typeof(Patch_OnPasswordEntered), typeof(Patch_RpcError) }; int num = 0; Type[] array2 = array; foreach (Type type in array2) { try { harmony.CreateClassProcessor(type).Patch(); Log.LogInfo((object)(" 已挂载补丁类:" + type.Name)); num++; } catch (Exception ex) { Log.LogError((object)(" 挂载失败 " + type.Name + ":" + ex.Message)); } } Log.LogInfo((object)$"Harmony 补丁挂载完成:{num}/{array.Length} 成功。"); } private void VerifyPatches() { MethodBase[] obj = new MethodBase[3] { AccessTools.Method(typeof(ZNet), "RPC_ClientHandshake", (Type[])null, (Type[])null), AccessTools.Method(typeof(ZNet), "OnPasswordEntered", (Type[])null, (Type[])null), AccessTools.Method(typeof(ZNet), "RPC_Error", (Type[])null, (Type[])null) }; Log.LogInfo((object)"---- 补丁挂载自检 ----"); MethodBase[] array = obj; foreach (MethodBase methodBase in array) { if (methodBase == null) { Log.LogWarning((object)" 找不到目标方法(可能游戏版本变了)"); continue; } Patches patchInfo = Harmony.GetPatchInfo(methodBase); int num = ((patchInfo != null && patchInfo.Prefixes != null) ? patchInfo.Prefixes.Count : 0); int num2 = ((patchInfo != null && patchInfo.Postfixes != null) ? patchInfo.Postfixes.Count : 0); string text = ((num + num2 > 0) ? "OK" : "!! 未挂上 !!"); Log.LogInfo((object)$" {methodBase.DeclaringType.Name}.{methodBase.Name}() prefix={num} postfix={num2} {text}"); } Log.LogInfo((object)"-----------------------"); } private void Update() { backfillTimer += Time.unscaledDeltaTime; if (!(backfillTimer < 5f)) { backfillTimer = 0f; PasswordStore.BackfillServerName(); } } private void OnDestroy() { PasswordStore.Flush(); if (harmony != null) { harmony.UnpatchSelf(); } } } internal static class Json { internal static void WriteString(StringBuilder sb, string value) { if (value == null) { sb.Append("null"); return; } sb.Append('"'); foreach (char c in value) { switch (c) { case '"': sb.Append("\\\""); continue; case '\\': sb.Append("\\\\"); continue; case '\b': sb.Append("\\b"); continue; case '\f': sb.Append("\\f"); continue; case '\n': sb.Append("\\n"); continue; case '\r': sb.Append("\\r"); continue; case '\t': sb.Append("\\t"); continue; } if (c < ' ') { StringBuilder stringBuilder = sb.Append("\\u"); int num = c; stringBuilder.Append(num.ToString("x4", CultureInfo.InvariantCulture)); } else { sb.Append(c); } } sb.Append('"'); } internal static void WriteField(StringBuilder sb, string name, string value, bool comma) { sb.Append(" "); WriteString(sb, name); sb.Append(": "); WriteString(sb, value); sb.Append(comma ? ",\n" : "\n"); } internal static void WriteField(StringBuilder sb, string name, long value, bool comma) { sb.Append(" "); WriteString(sb, name); sb.Append(": ").Append(value.ToString(CultureInfo.InvariantCulture)); sb.Append(comma ? ",\n" : "\n"); } internal static object Parse(string text) { int i = 0; if (text.Length > 0 && text[0] == '\ufeff') { i = 1; } SkipWs(text, ref i); object result = ParseValue(text, ref i); SkipWs(text, ref i); return result; } private static object ParseValue(string s, ref int i) { if (i >= s.Length) { throw new FormatException("JSON 意外结束。"); } switch (s[i]) { case '{': return ParseObject(s, ref i); case '[': return ParseArray(s, ref i); case '"': return ParseString(s, ref i); case 't': Expect(s, ref i, "true"); return true; case 'f': Expect(s, ref i, "false"); return false; case 'n': Expect(s, ref i, "null"); return null; default: return ParseNumber(s, ref i); } } private static Dictionary<string, object> ParseObject(string s, ref int i) { Dictionary<string, object> dictionary = new Dictionary<string, object>(); i++; SkipWs(s, ref i); if (i < s.Length && s[i] == '}') { i++; return dictionary; } while (true) { SkipWs(s, ref i); if (i >= s.Length || s[i] != '"') { throw new FormatException("JSON 对象里期待一个字符串键,位置 " + i + "。"); } string text = ParseString(s, ref i); SkipWs(s, ref i); if (i >= s.Length || s[i] != ':') { throw new FormatException("键 " + text + " 后面缺少冒号。"); } i++; SkipWs(s, ref i); dictionary[text] = ParseValue(s, ref i); SkipWs(s, ref i); if (i >= s.Length || s[i] != ',') { break; } i++; } if (i < s.Length && s[i] == '}') { i++; return dictionary; } throw new FormatException("JSON 对象里期待 , 或 },位置 " + i + "。"); } private static List<object> ParseArray(string s, ref int i) { List<object> list = new List<object>(); i++; SkipWs(s, ref i); if (i < s.Length && s[i] == ']') { i++; return list; } while (true) { SkipWs(s, ref i); list.Add(ParseValue(s, ref i)); SkipWs(s, ref i); if (i >= s.Length || s[i] != ',') { break; } i++; } if (i < s.Length && s[i] == ']') { i++; return list; } throw new FormatException("JSON 数组里期待 , 或 ],位置 " + i + "。"); } private static string ParseString(string s, ref int i) { i++; StringBuilder stringBuilder = new StringBuilder(); while (i < s.Length) { char c = s[i++]; switch (c) { case '"': return stringBuilder.ToString(); default: stringBuilder.Append(c); break; case '\\': { if (i >= s.Length) { throw new FormatException("JSON 转义符后面没有内容。"); } char c2 = s[i++]; switch (c2) { case '"': stringBuilder.Append('"'); break; case '\\': stringBuilder.Append('\\'); break; case '/': stringBuilder.Append('/'); break; case 'b': stringBuilder.Append('\b'); break; case 'f': stringBuilder.Append('\f'); break; case 'n': stringBuilder.Append('\n'); break; case 'r': stringBuilder.Append('\r'); break; case 't': stringBuilder.Append('\t'); break; case 'u': if (i + 4 > s.Length) { throw new FormatException("\\u 转义不完整。"); } stringBuilder.Append((char)Convert.ToInt32(s.Substring(i, 4), 16)); i += 4; break; default: throw new FormatException("看不懂的转义:\\" + c2); } break; } } } throw new FormatException("JSON 字符串没有收尾引号。"); } private static object ParseNumber(string s, ref int i) { int num = i; while (i < s.Length && "+-0123456789.eE".IndexOf(s[i]) >= 0) { i++; } if (num == i) { throw new FormatException("JSON 里出现非法字符 '" + s[num] + "',位置 " + num + "。"); } string text = s.Substring(num, i - num); if (!double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { throw new FormatException("不是合法的数字:" + text); } return result; } private static void Expect(string s, ref int i, string word) { if (i + word.Length > s.Length || s.Substring(i, word.Length) != word) { throw new FormatException("JSON 里期待 " + word + ",位置 " + i + "。"); } i += word.Length; } private static void SkipWs(string s, ref int i) { while (i < s.Length && (s[i] == ' ' || s[i] == '\t' || s[i] == '\r' || s[i] == '\n')) { i++; } } internal static string AsString(object value) { if (value == null) { return ""; } return (value as string) ?? ""; } internal static int AsInt(object value) { if (value is double) { return (int)(double)value; } return 0; } internal static long AsLong(object value) { if (value is double) { return (long)(double)value; } return 0L; } internal static Dictionary<string, object> AsObject(object value) { return value as Dictionary<string, object>; } internal static List<object> AsArray(object value) { return value as List<object>; } internal static object Field(Dictionary<string, object> obj, string name) { if (obj != null && obj.TryGetValue(name, out var value)) { return value; } return null; } } public sealed class ServerRecord { public string Key { get; set; } public string Kind { get; set; } public string SteamId { get; set; } public string PlayFabId { get; set; } public string Host { get; set; } public int Port { get; set; } public string JoinCode { get; set; } public string ServerName { get; set; } public string Password { get; set; } public long LastJoinedUtc { get; set; } public string LastJoinedLocal { get; set; } public int UseCount { get; set; } public string Note { get; set; } } internal static class PasswordStore { private const string FileName = "servers.json"; private static readonly List<ServerRecord> Records = new List<ServerRecord>(); private static bool dirty; private static string lastTouchedKey; internal static string FilePath { get; private set; } internal static int Count => Records.Count; internal static void Init() { try { InitAt(Path.Combine(Paths.ConfigPath, "EasyLogin")); } catch (Exception ex) { FilePath = "<不可用>"; EasyLoginPlugin.Log.LogError((object)("初始化存储失败:" + ex.Message)); } } internal static void InitAt(string dir) { Directory.CreateDirectory(dir); FilePath = Path.Combine(dir, "servers.json"); Load(); } private static void Load() { Records.Clear(); if (!File.Exists(FilePath)) { EasyLoginPlugin.Log.LogInfo((object)("存储文件还不存在,先不动它:" + FilePath)); return; } try { string text = File.ReadAllText(FilePath); if (text == null || text.Trim().Length == 0) { return; } List<object> obj = Json.AsArray(Json.Field(Json.AsObject(Json.Parse(text)) ?? throw new FormatException("根节点不是 JSON 对象。"), "Records")) ?? throw new FormatException("找不到 Records 数组。"); int num = 0; foreach (object item in obj) { Dictionary<string, object> dictionary = Json.AsObject(item); if (dictionary == null) { num++; continue; } ServerRecord serverRecord = new ServerRecord { Key = Json.AsString(Json.Field(dictionary, "Key")), Kind = Json.AsString(Json.Field(dictionary, "Kind")), SteamId = Json.AsString(Json.Field(dictionary, "SteamId")), PlayFabId = Json.AsString(Json.Field(dictionary, "PlayFabId")), Host = Json.AsString(Json.Field(dictionary, "Host")), Port = Json.AsInt(Json.Field(dictionary, "Port")), JoinCode = Json.AsString(Json.Field(dictionary, "JoinCode")), ServerName = Json.AsString(Json.Field(dictionary, "ServerName")), Password = Json.AsString(Json.Field(dictionary, "Password")), LastJoinedUtc = Json.AsLong(Json.Field(dictionary, "LastJoinedUtc")), LastJoinedLocal = Json.AsString(Json.Field(dictionary, "LastJoinedLocal")), UseCount = Json.AsInt(Json.Field(dictionary, "UseCount")), Note = Json.AsString(Json.Field(dictionary, "Note")) }; if (serverRecord.Key.Length == 0) { num++; } else { Records.Add(serverRecord); } } if (num > 0) { EasyLoginPlugin.Log.LogWarning((object)$"存储文件里有 {num} 条记录读不出来(缺 Key 或格式不对),已跳过。"); } } catch (Exception ex) { Records.Clear(); EasyLoginPlugin.Log.LogError((object)$"存储文件读不出来({ex.GetType().Name} {ex.Message}),先按空表继续。坏文件会改名保留,不会丢。"); BackupBrokenFile(); } } private static void BackupBrokenFile() { try { string text = FilePath + ".broken-" + DateTime.Now.ToString("yyyyMMdd-HHmmss"); File.Move(FilePath, text); EasyLoginPlugin.Log.LogWarning((object)("坏文件已改名为:" + text)); } catch (Exception ex) { EasyLoginPlugin.Log.LogWarning((object)("坏文件改名也失败了(" + ex.Message + "),下次保存会直接覆盖它。")); } } internal static ServerRecord Find(ServerIdentity.Identity id) { if (id == null || id.Key == null) { return null; } ServerRecord serverRecord = FirstMatching((ServerRecord r) => r.Key == id.Key); if (serverRecord != null) { return serverRecord; } if (!string.IsNullOrEmpty(id.SteamId)) { serverRecord = FirstMatching((ServerRecord r) => r.SteamId == id.SteamId); if (serverRecord != null) { return serverRecord; } } if (!string.IsNullOrEmpty(id.PlayFabId)) { serverRecord = FirstMatching((ServerRecord r) => r.PlayFabId == id.PlayFabId); if (serverRecord != null) { return serverRecord; } } if (!string.IsNullOrEmpty(id.Host)) { serverRecord = FirstMatching((ServerRecord r) => r.Host == id.Host && r.Port == id.Port); if (serverRecord != null) { return serverRecord; } } if (EasyLoginPlugin.MatchByJoinCode.Value && id.Backend == 1 && !string.IsNullOrEmpty(id.JoinCode)) { serverRecord = FirstMatching((ServerRecord r) => r.JoinCode == id.JoinCode); if (serverRecord != null) { return serverRecord; } } return null; } private static ServerRecord FirstMatching(Func<ServerRecord, bool> predicate) { for (int i = 0; i < Records.Count; i++) { if (predicate(Records[i])) { return Records[i]; } } return null; } private static ServerRecord ByKey(string key) { if (string.IsNullOrEmpty(key)) { return null; } return FirstMatching((ServerRecord r) => r.Key == key); } internal static bool Remember(ServerIdentity.Identity id, string plainPassword) { if (id == null || id.Key == null || string.IsNullOrEmpty(plainPassword)) { return false; } ServerRecord serverRecord = Find(id); bool flag = false; if (serverRecord == null) { if (!EasyLoginPlugin.RememberManualInput.Value) { EasyLoginPlugin.Log.LogInfo((object)("没有开启「记住手动输入的密码」,跳过:" + id.Describe())); return false; } serverRecord = new ServerRecord { Key = id.Key, Kind = id.Kind, SteamId = (id.SteamId ?? ""), PlayFabId = (id.PlayFabId ?? ""), Host = (id.Host ?? ""), Port = id.Port, JoinCode = (id.JoinCode ?? ""), ServerName = (id.ServerName ?? ""), Note = "" }; Records.Add(serverRecord); flag = true; } if (!string.IsNullOrEmpty(id.JoinCode) && string.IsNullOrEmpty(serverRecord.JoinCode)) { serverRecord.JoinCode = id.JoinCode; } if (!string.IsNullOrEmpty(id.ServerName) && string.IsNullOrEmpty(serverRecord.ServerName)) { serverRecord.ServerName = id.ServerName; } if (!string.IsNullOrEmpty(id.SteamId) && string.IsNullOrEmpty(serverRecord.SteamId)) { serverRecord.SteamId = id.SteamId; } if (!string.IsNullOrEmpty(id.PlayFabId) && string.IsNullOrEmpty(serverRecord.PlayFabId)) { serverRecord.PlayFabId = id.PlayFabId; } bool flag2 = Secret.Unprotect(serverRecord.Password) != plainPassword; serverRecord.Password = Secret.Protect(plainPassword); serverRecord.LastJoinedUtc = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); serverRecord.LastJoinedLocal = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); serverRecord.UseCount++; lastTouchedKey = serverRecord.Key; dirty = true; Save(); string text = (EasyLoginPlugin.ShowPasswordInLog.Value ? plainPassword : "********"); EasyLoginPlugin.Log.LogInfo((object)string.Format("{0}服务器密码:{1} → {2}(密码 {3}{4},共 {5} 条记录)", flag ? "新增" : "更新", id.Describe(), serverRecord.Key, text, flag2 ? ",内容有变" : "", Records.Count)); return true; } internal static bool Forget(ServerIdentity.Identity id, string reason) { ServerRecord serverRecord = Find(id); if (serverRecord == null && id != null) { serverRecord = ByKey(id.Key); } if (serverRecord == null) { EasyLoginPlugin.Log.LogInfo((object)("服务器报告密码错误,但本地没有找到对应记录:" + ((id == null) ? "?" : id.Describe()))); return false; } Records.Remove(serverRecord); dirty = true; Save(); EasyLoginPlugin.Log.LogWarning((object)$"服务器 {((id == null) ? serverRecord.Key : id.Describe())} 报告密码错误({reason}),已删掉这条记录 —— 下次进它会重新让你手输密码。"); return true; } internal static void BackfillServerName() { if (string.IsNullOrEmpty(lastTouchedKey)) { return; } string text = ServerIdentity.CurrentServerName(); if (!string.IsNullOrEmpty(text)) { ServerRecord serverRecord = ByKey(lastTouchedKey); if (serverRecord != null && !(serverRecord.ServerName == text)) { serverRecord.ServerName = text; dirty = true; Save(); EasyLoginPlugin.Log.LogInfo((object)("已回填服务器名字:" + serverRecord.Key + " → " + text)); } } } internal static void Save() { if (FilePath == null || FilePath == "<不可用>") { return; } try { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("{\n"); stringBuilder.Append(" \"Version\": 1,\n"); stringBuilder.Append(" \"Comment\": "); Json.WriteString(stringBuilder, "EasyLogin 自动生成。Note 字段随便写,模组不会动它;Password 字段带 dpapi:/obf:/plain: 前缀,别手改前缀。"); stringBuilder.Append(",\n"); stringBuilder.Append(" \"Records\": [\n"); for (int i = 0; i < Records.Count; i++) { ServerRecord serverRecord = Records[i]; stringBuilder.Append(" {\n"); AppendString(stringBuilder, "Key", serverRecord.Key, last: false); AppendString(stringBuilder, "Kind", serverRecord.Kind, last: false); AppendString(stringBuilder, "SteamId", serverRecord.SteamId, last: false); AppendString(stringBuilder, "PlayFabId", serverRecord.PlayFabId, last: false); AppendString(stringBuilder, "Host", serverRecord.Host, last: false); AppendNumber(stringBuilder, "Port", serverRecord.Port, last: false); AppendString(stringBuilder, "JoinCode", serverRecord.JoinCode, last: false); AppendString(stringBuilder, "ServerName", serverRecord.ServerName, last: false); AppendString(stringBuilder, "Password", serverRecord.Password, last: false); AppendNumber(stringBuilder, "LastJoinedUtc", serverRecord.LastJoinedUtc, last: false); AppendString(stringBuilder, "LastJoinedLocal", serverRecord.LastJoinedLocal, last: false); AppendNumber(stringBuilder, "UseCount", serverRecord.UseCount, last: false); AppendString(stringBuilder, "Note", serverRecord.Note, last: true); stringBuilder.Append((i < Records.Count - 1) ? " },\n" : " }\n"); } stringBuilder.Append(" ]\n"); stringBuilder.Append("}\n"); File.WriteAllText(FilePath, stringBuilder.ToString(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); dirty = false; } catch (Exception ex) { EasyLoginPlugin.Log.LogError((object)("写存储文件失败:" + ex.GetType().Name + " " + ex.Message)); } } private static void AppendString(StringBuilder sb, string name, string value, bool last) { sb.Append(" "); Json.WriteString(sb, name); sb.Append(": "); Json.WriteString(sb, value); sb.Append(last ? "\n" : ",\n"); } private static void AppendNumber(StringBuilder sb, string name, long value, bool last) { sb.Append(" "); Json.WriteString(sb, name); sb.Append(": "); sb.Append(value.ToString(CultureInfo.InvariantCulture)); sb.Append(last ? "\n" : ",\n"); } internal static void Flush() { if (dirty) { Save(); } } } [HarmonyPatch(typeof(ZNet), "RPC_ClientHandshake")] internal static class Patch_ClientHandshake { private static MethodInfo mOnPasswordEntered; private static void Postfix(ZNet __instance, bool needPassword) { try { if (!needPassword || (Object)(object)__instance == (Object)null || !EasyLoginPlugin.Enabled.Value) { return; } if (!__instance.InPasswordDialog()) { if (EasyLoginPlugin.VerboseLog.Value) { EasyLoginPlugin.Log.LogInfo((object)"弹窗已关闭(原版自己提交过了),不介入。"); } return; } ServerIdentity.Identity identity = ServerIdentity.Current(); if (identity == null) { return; } ServerRecord serverRecord = PasswordStore.Find(identity); if (serverRecord == null) { EasyLoginPlugin.Log.LogInfo((object)("没存过这个服务器的密码,等你手输:" + identity.Describe())); return; } string text = Secret.Unprotect(serverRecord.Password); if (string.IsNullOrEmpty(text)) { EasyLoginPlugin.Log.LogWarning((object)("有记录但密码解不出来,等你手输:" + identity.Describe())); } else if (EasyLoginPlugin.AutoSubmit.Value) { if (mOnPasswordEntered == null) { mOnPasswordEntered = AccessTools.Method(typeof(ZNet), "OnPasswordEntered", (Type[])null, (Type[])null); } if (mOnPasswordEntered == null) { EasyLoginPlugin.Log.LogError((object)"找不到 ZNet.OnPasswordEntered(游戏版本变了吗?),无法自动提交。"); return; } EasyLoginPlugin.Log.LogInfo((object)$"自动登录:{identity.Describe()} → 直接提交已保存的密码(第 {serverRecord.UseCount + 1} 次使用)。"); mOnPasswordEntered.Invoke(__instance, new object[1] { text }); } else { FillOnly(__instance, text); PasswordStore.Remember(identity, text); EasyLoginPlugin.Log.LogInfo((object)$"已把密码填进输入框(未提交):{identity.Describe()} —— 按回车即可进入。"); } } catch (Exception ex) { EasyLoginPlugin.Log.LogError((object)("自动填密码时出错(不影响你手动输入):" + ex)); } } private static void FillOnly(ZNet net, string pwd) { RectTransform passwordDialog = net.m_passwordDialog; if ((Object)(object)passwordDialog == (Object)null) { EasyLoginPlugin.Log.LogWarning((object)"找不到密码弹窗对象,无法填入。"); return; } Type type = AccessTools.TypeByName("GuiInputField"); if (type == null) { EasyLoginPlugin.Log.LogWarning((object)"找不到 GuiInputField 类型(游戏版本变了吗?),无法填入。"); return; } Component component = ((Component)passwordDialog).GetComponent(type); if ((Object)(object)component == (Object)null) { EasyLoginPlugin.Log.LogWarning((object)"密码弹窗上没有 GuiInputField 组件,无法填入。"); return; } PropertyInfo property = type.GetProperty("text"); if (property == null || !property.CanWrite) { EasyLoginPlugin.Log.LogWarning((object)"GuiInputField 没有可写的 text 属性,无法填入。"); } else { property.SetValue(component, pwd, null); } } } [HarmonyPatch(typeof(ZNet), "OnPasswordEntered")] internal static class Patch_OnPasswordEntered { private static void Postfix(string pwd) { try { if (EasyLoginPlugin.Enabled.Value && !string.IsNullOrEmpty(pwd)) { ServerIdentity.Identity identity = ServerIdentity.Current(); if (identity != null) { PasswordStore.Remember(identity, pwd); } } } catch (Exception ex) { EasyLoginPlugin.Log.LogError((object)("记录密码时出错:" + ex)); } } } [HarmonyPatch(typeof(ZNet), "RPC_Error")] internal static class Patch_RpcError { private const int ErrorPassword = 6; private static void Postfix(int error) { try { if (error == 6 && EasyLoginPlugin.Enabled.Value) { if (!EasyLoginPlugin.ForgetOnPasswordError.Value) { EasyLoginPlugin.Log.LogWarning((object)"服务器报告「密码错误」,但配置里关掉了自动遗忘,记录保留 —— 下次还会自动填同样的密码,注意别再被锁在外面。"); } else { PasswordStore.Forget(ServerIdentity.Current(), "服务端返回 Error(6) = ConnectionStatus.ErrorPassword"); } } } catch (Exception ex) { EasyLoginPlugin.Log.LogError((object)("处理密码错误时出错:" + ex)); } } } internal static class Secret { private const string XorKey = "EasyLogin/1.0/valheim-server-password-store"; private static readonly byte[] XorBytes = Encoding.UTF8.GetBytes("EasyLogin/1.0/valheim-server-password-store"); private static bool dpapiUsable = true; public static string Protect(string plain) { if (plain == null) { plain = ""; } if (EasyLoginPlugin.EncryptWithWindowsUserKey.Value && dpapiUsable) { try { byte[] inArray = DpapiProtect(Encoding.UTF8.GetBytes(plain)); return "dpapi:" + Convert.ToBase64String(inArray); } catch (Exception ex) { dpapiUsable = false; EasyLoginPlugin.Log.LogWarning((object)("DPAPI 不可用,退化为简单混淆存储(功能不受影响):" + ex.GetType().Name + " " + ex.Message)); } } return "obf:" + Convert.ToBase64String(Xor(Encoding.UTF8.GetBytes(plain))); } public static string Unprotect(string stored) { if (string.IsNullOrEmpty(stored)) { return ""; } int num = stored.IndexOf(':'); if (num <= 0) { return stored; } string text = stored.Substring(0, num); string text2 = stored.Substring(num + 1); try { switch (text) { case "plain": return text2; case "obf": return Encoding.UTF8.GetString(Xor(Convert.FromBase64String(text2))); case "dpapi": return Encoding.UTF8.GetString(DpapiUnprotect(Convert.FromBase64String(text2))); default: EasyLoginPlugin.Log.LogWarning((object)("看不懂的密码存储格式:" + text + ",按明文处理。")); return stored; } } catch (Exception ex) { EasyLoginPlugin.Log.LogError((object)("解密密码失败(" + text + "):" + ex.GetType().Name + " " + ex.Message + "。如果你换过 Windows 账号或换过电脑,把 servers.json 里这条记录删掉重输即可。")); return ""; } } private static byte[] DpapiProtect(byte[] data) { return ProtectedData.Protect(data, (byte[])null, (DataProtectionScope)0); } private static byte[] DpapiUnprotect(byte[] data) { return ProtectedData.Unprotect(data, (byte[])null, (DataProtectionScope)0); } private static byte[] Xor(byte[] data) { byte[] array = new byte[data.Length]; for (int i = 0; i < data.Length; i++) { array[i] = (byte)(data[i] ^ XorBytes[i % XorBytes.Length]); } return array; } } internal static class ServerIdentity { internal sealed class Identity { public string Key; public string Kind = "Unknown"; public string SteamId = ""; public string PlayFabId = ""; public string Host = ""; public string JoinCode = ""; public string ServerName = ""; public int Port; public int Backend = -1; internal string BackendName => Backend switch { 0 => "Steam", 1 => "PlayFab", 2 => "EOS", 3 => "CustomSocket", _ => "?", }; internal string Describe() { string text = Kind switch { "Steam" => "Steam ID " + SteamId, "PlayFab" => "跨平台 host " + PlayFabId + ((JoinCode.Length > 0) ? ("(房间码 " + JoinCode + ")") : ""), "Code" => "跨平台房间码 " + JoinCode, "IP" => "IP " + Host + ":" + Port, _ => "未知(" + BackendName + " 后端,字段都是空的)", }; if (!string.IsNullOrEmpty(ServerName)) { text = text + "「" + ServerName + "」"; } return text; } } internal const int BackendSteam = 0; internal const int BackendPlayFab = 1; private static FieldInfo fSteamId; private static FieldInfo fPlayFabId; private static FieldInfo fHost; private static FieldInfo fPort; private static PropertyInfo pJoinCode; private static bool joinCodeGivenUp; internal static void Init() { fSteamId = AccessTools.Field(typeof(ZNet), "m_serverSteamID"); fPlayFabId = AccessTools.Field(typeof(ZNet), "m_serverPlayFabPlayerId"); fHost = AccessTools.Field(typeof(ZNet), "m_serverHost"); fPort = AccessTools.Field(typeof(ZNet), "m_serverHostPort"); if (fSteamId == null || fPlayFabId == null || fHost == null || fPort == null) { EasyLoginPlugin.Log.LogError((object)("读不到 ZNet 的服务器标识字段(游戏版本可能变了)。steam=" + (fSteamId != null) + " playfab=" + (fPlayFabId != null) + " host=" + (fHost != null) + " port=" + (fPort != null))); } } internal static Identity Current() { //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Expected I4, but got Unknown try { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null) { return null; } Identity identity = new Identity(); identity.Backend = (int)ZNet.m_onlineBackend; ulong num = ReadUlong(fSteamId); identity.SteamId = ((num == 0L) ? "" : num.ToString()); identity.PlayFabId = ReadString(fPlayFabId); identity.Host = ReadString(fHost); identity.Port = ReadInt(fPort); identity.JoinCode = ReadJoinCode(); identity.ServerName = WorldName(instance); if (identity.Backend == 0 && identity.SteamId.Length > 0) { identity.Key = "steam:" + identity.SteamId; identity.Kind = "Steam"; } else if (identity.Backend == 1) { if (identity.PlayFabId.Length > 0) { identity.Key = "playfab:" + identity.PlayFabId; identity.Kind = "PlayFab"; } else if (identity.JoinCode.Length > 0) { identity.Key = "code:" + identity.JoinCode; identity.Kind = "Code"; } } if (identity.Key == null && identity.Host.Length > 0) { identity.Key = "ip:" + identity.Host + ":" + identity.Port; identity.Kind = "IP"; } if (identity.Key == null) { EasyLoginPlugin.Log.LogWarning((object)("识别不出当前服务器的身份(后端 " + identity.BackendName + "):steamId='" + identity.SteamId + "' playfabId='" + identity.PlayFabId + "' host='" + identity.Host + "' 房间码='" + identity.JoinCode + "'")); return null; } if (EasyLoginPlugin.VerboseLog.Value) { EasyLoginPlugin.Log.LogInfo((object)$"当前服务器:{identity.Describe()} | key={identity.Key} steamId='{identity.SteamId}' playfabId='{identity.PlayFabId}' host='{identity.Host}' port={identity.Port} 房间码='{identity.JoinCode}' 世界名='{identity.ServerName}'"); } return identity; } catch (Exception ex) { EasyLoginPlugin.Log.LogError((object)("识别服务器身份时出错:" + ex.GetType().Name + " " + ex.Message)); return null; } } internal static string CurrentServerName() { try { return WorldName(ZNet.instance); } catch (Exception) { return ""; } } private static string WorldName(ZNet net) { if ((Object)(object)net == (Object)null) { return ""; } string worldName = net.GetWorldName(); if (worldName != null) { return worldName; } return ""; } private static string ReadString(FieldInfo f) { if (f == null) { return ""; } object value = f.GetValue(null); if (value != null) { return (string)value; } return ""; } private static ulong ReadUlong(FieldInfo f) { if (f == null) { return 0uL; } object value = f.GetValue(null); if (value == null) { return 0uL; } try { return Convert.ToUInt64(value); } catch (Exception) { return 0uL; } } private static int ReadInt(FieldInfo f) { if (f == null) { return 0; } object value = f.GetValue(null); if (value == null) { return 0; } try { return Convert.ToInt32(value); } catch (Exception) { return 0; } } private static string ReadJoinCode() { if (joinCodeGivenUp) { return ""; } try { if (pJoinCode == null) { Type type = AccessTools.TypeByName("ZPlayFabMatchmaking"); if (type == null) { joinCodeGivenUp = true; return ""; } pJoinCode = type.GetProperty("JoinCode", BindingFlags.Static | BindingFlags.Public); if (pJoinCode == null) { joinCodeGivenUp = true; return ""; } } object value = pJoinCode.GetValue(null, null); return (value == null) ? "" : ((string)value); } catch (Exception ex) { joinCodeGivenUp = true; EasyLoginPlugin.Log.LogWarning((object)("读房间码失败,之后不再尝试:" + ex.GetType().Name + " " + ex.Message)); return ""; } } } }