EasyLogin
[AI-generated] Remembers each server's join password and auto-fills/auto-submits it. Per-server records, auto-forget on wrong password, DPAPI-encrypted. Client-only, no server sync.
模組介紹 作者原文
EasyLogin —— Valheim 聯機密碼管家(純客戶端)
AI 生成聲明:本模組由 AI 智能體(WorkBuddy)在用戶的明確需求與逐項驗收下編寫;
功能設計、真機測試與發佈決定均由用戶主導。
進過一次的服務器,密碼自動記住;下次再進自動填/自動提交。
Valheim 原版沒有這個能力——不是沒做好,是根本沒寫。本模組用三個 Harmony 補丁補上,
不改遊戲數據、不加網絡校驗、主機和隊友裝不裝都完全不受影響。
一、效果
| 場景 | 原版 | 裝了 EasyLogin |
|---|---|---|
| 第一次進某人的帶密碼服務器 | 手輸密碼 | 照常手輸(模組會記下來) |
| 第二次進同一臺服務器 | 再輸一遍 | 自動填好並提交,直接進 |
| 房主改了密碼 | 提示密碼錯誤 | 自動忘掉舊密碼,下次讓你重新輸(然後記住新的) |
| 換另一臺服務器 | 每次都要輸 | 各存各的,互不干擾 |
每個服務器分開記錄,匹配依據是 Steam ID / 跨平臺 host ID / 房間碼 / IP:端口,
不是"上一次輸的密碼"這種全局變量。
二、安裝
把 EasyLogin.dll 丟進 BepInEx/plugins/ 即可(或者用 r2modman 導入 EasyLogin-1.0.0.zip)。
依賴只有 BepInExPack_Valheim。不需要 Jotunn,不需要任何前置框架。
三、原版爲什麼做不到(IL 實證)
進服密碼唯一的入口是 ZNet.RPC_ClientHandshake(ZRpc rpc, bool needPassword, string serverPasswordSalt),
由服務端在握手時發起。1.0 正式版裏的實現(assembly_valheim.dll,IL 偏移爲函數內相對偏移):
0000 m_connectingDialog.gameObject.SetActive(false)
0011 m_serverPasswordSalt = serverPasswordSalt ← 服務端下發的隨機鹽
0017 if (!needPassword) { SendPeerInfo(rpc, ""); return; } → 0077
001A m_passwordDialog.gameObject.SetActive(true) ← 彈出要求輸密碼的框
002B var input = m_passwordDialog.GetComponent()
0037 input.text = ""
0042 input.ActivateInputField()
0047 input.OnInputSubmit.AddListener(OnPasswordEntered)
005D m_tempPasswordRPC = rpc
0064 call FejdStartup::get_ServerPassword
0069 brfalse.s → 0083 ← 原版唯一的"自動填充"
006B OnPasswordEntered(FejdStartup.ServerPassword)
也就是說:原版確實內置了自動填充,條件是 FejdStartup.ServerPassword!= null。
那這個 ServerPassword 是誰寫的?查全程序集的引用:
引用了 ['FejdStartup::set_ServerPassword'] 的方法共 1 個:
FejdStartup ParseArguments
只有 ParseArguments 一處——也就是命令行參數。FejdStartup.ParseArguments 裏認的正是
程序集字符串堆裏那個 -password(另有 $error_password / $menu_passwordinvalid 等提示文案)。
所以只有像 valheim.exe -password xxx 這樣啓動纔會自動填,
正常點着玩的時候這個字段永遠是 null,每次都得手打。
順帶一個必須知道的細節:密碼是 MD5(ASCII(明文 + 鹽))(ZNet.HashPassword,IL 就是
Encoding.ASCII.GetBytes(password + salt) → MD5CryptoServiceProvider.ComputeHash),
而鹽是服務端每次握手現生成的 16 字節隨機數(ZNet.ServerPasswordSalt() 裏
RandomNumberGenerator.GetBytes(16))。
所以本地沒法預先算好哈希存起來,只能存明文 —— 任何"記住聯機密碼"的模組都繞不開這一點。
本模組會盡量別讓它以明文躺在硬盤上(見第五節)。
四、三個補丁(全部只改本地行爲)
| 補丁 | 目標方法 | 做什麼 |
|---|---|---|
| ① 自動填 | ZNet.RPC_ClientHandshake postfix | 需要密碼、彈窗還開着、本地存過 → 調原版 ZNet.OnPasswordEntered(pwd),完全複用原版那條提交路徑(含 MD5+鹽),不繞過任何校驗 |
| ② 記錄 | ZNet.OnPasswordEntered postfix | 把這次真正用到的密碼記下來(首次手輸、後來改的密碼、自動提交都經過這裏) |
| ③ 遺忘 | ZNet.RPC_Error postfix | error == 6 時刪掉該服務器的記錄 |
補丁 ① 裏那個"彈窗還開着"的判斷用的是 ZNet.InPasswordDialog(),
正好把"原版已經靠 -password 填過了"這種情況排除掉,不會重複提交。
補丁 ③ 的 6 是 ConnectionStatus.ErrorPassword。服務端校驗在 ZNet.RPC_PeerInfo 裏:
0411 ldsfld ZNet::m_serverPassword ← 服務端設的密碼
0416 ldloc.s 13 ← 客戶端發來的哈希
0418 String::op_Inequality
041D brfalse → 04C8 ← 相等就放行
048E ZRpc::Invoke("Error", (int)6) ← 不等就發 Error(6)
客戶端 ZNet.RPC_Error(rpc, int error) 的實現只有 m_connectionStatus = error + 一行日誌。
⚠️ ConnectionStatus 這個枚舉在程序集裏是 tdNotPublic,
C# 里根本寫不出 ConnectionStatus.ErrorPassword(編譯期就報"找不到類型"),
所以源碼裏只能寫魔法數字 6。
這一條特別重要:不刪記錄的話,房主一改密碼,你就會永遠自動填那個舊密碼、
連密碼框都不彈出來,直接把自己鎖在門外,只能手動去刪 json。
如果實在不想讓它忘,配置裏的 ForgetOnPasswordError 可以關掉(不推薦)。
五、服務器是怎麼認出來的
ZNet 上有三個 SetServerHost 重載,全程序集只有 FejdStartup.JoinServer 會調用
(三個 lambda:b__74_5、<>c__DisplayClass74_0::b__4、
<>c__DisplayClass74_1::b__6),而且每次開頭都先 ZNet.ResetServerHost() 清一遍。
所以在"服務端要求輸密碼"那一刻,這些字段必然就是當前這臺服務器的:
| 入口 | 寫入的字段 | 本模組的 key |
|---|---|---|
| Steam 聯機(好友/服務器列表) | m_serverSteamID (ulong) | steam: |
| 跨平臺聯機 | m_serverPlayFabPlayerId (string) | playfab: |
| 跨平臺聯機(沒有 host ID 時) | ZPlayFabMatchmaking.JoinCode | code: |
| 直連 IP / 專用服務器 | m_serverHost + m_serverHostPort | ip:: |
ZNet.m_onlineBackend(OnlineBackendType:0=Steamworks、1=PlayFab、2=EOS、3=CustomSocket)
用來判定走哪條路,避免把上一次聯機的房間碼套到這一次上。
一個必須說清楚的限制:服務器名字當不了判據
ZNet.m_ServerName 全程序集只有三處寫入 —— SetServer、OpenServer、...MoveNext,全是開服側。
客戶端走的是 FejdStartup.JoinServer 裏的 ZNet.SetServer(false, false, false, "", "", null),
名字傳的是空串,客戶端這個字段永遠是空的。
客戶端要拿名字只能用 ZNet.GetWorldName()(實現就是 m_world == null? null: m_world.m_name),
而 m_world 是認證成功之後由 RPC_PeerInfo 建起來的 ——
也就是說握手那一刻根本不知道服務器叫什麼,名字只能事後回填。
所以:
- servers.json 裏的 ServerN
安裝與使用 安裝:BepInEx 外掛
- 本站下載的壓縮包就是模組本體,通常是 BepInEx 外掛(解壓後得到 .dll 或外掛資料夾)。
- 建議流程:用社群模組管理器(如 r2modman)選中本遊戲後匯入該模組,管理器會自動裝好 BepInEx 前置並處理依賴。
- 手動流程:先在遊戲目錄裝好 BepInEx 載入器,再把解壓出的 .dll 或外掛資料夾放進 BepInEx/plugins/。
- 啟動遊戲,BepInEx 控制台會列印載入日誌;確認這個模組出現在日誌裡即為安裝成功。
帶「前置依賴」的模組需要一併安裝,否則不會載入。功能開關通常在遊戲內或外掛自帶的設定檔裡調整。 多人連線時,主機與所有用戶端需要安裝一致的模組。
下面這些內容需要先安裝,否則該模組不會生效: