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.
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.
By s6652289
| Last updated | a week ago |
| Total downloads | 130 |
| Total rating | 0 |
| Categories | Mods AI Generated |
| Dependency string | s6652289-EasyLogin-1.0.0 |
| Dependants | 0 other packages depend on this package |
This mod requires the following mods to function
denikson-BepInExPack_Valheim
BepInEx pack for Valheim. Preconfigured with the correct entry point for mods and preferred defaults for the community.
Preferred version: 5.4.2350README
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<GuiInputField>()
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:<JoinServer>b__74_5、<>c__DisplayClass74_0::<JoinServer>b__4、
<>c__DisplayClass74_1::<JoinServer>b__6),而且每次开头都先 ZNet.ResetServerHost() 清一遍。
所以在"服务端要求输密码"那一刻,这些字段必然就是当前这台服务器的:
| 入口 | 写入的字段 | 本模组的 key |
|---|---|---|
| Steam 联机(好友/服务器列表) | m_serverSteamID (ulong) |
steam:<steamid> |
| 跨平台联机 | m_serverPlayFabPlayerId (string) |
playfab:<id> |
| 跨平台联机(没有 host ID 时) | ZPlayFabMatchmaking.JoinCode |
code:<房间码> |
| 直连 IP / 专用服务器 | m_serverHost + m_serverHostPort |
ip:<ip>:<端口> |
ZNet.m_onlineBackend(OnlineBackendType:0=Steamworks、1=PlayFab、2=EOS、3=CustomSocket)
用来判定走哪条路,避免把上一次联机的房间码套到这一次上。
一个必须说清楚的限制:服务器名字当不了判据
ZNet.m_ServerName 全程序集只有三处写入 —— SetServer、OpenServer、
<OnSteamServerRegistered>...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里的ServerName只是给你看的标签(认证成功后几秒内回填),- 它不参与匹配。想按名字认服务器的方案在这个 API 下走不通。
六、密码怎么存
文件:BepInEx/config/EasyLogin/servers.json(UTF-8 无 BOM,手工可编辑)
{
"Version": 1,
"Comment": "EasyLogin 自动生成。Note 字段随便写,模组不会动它;Password 字段带前缀,别手改前缀。",
"Records": [
{
"Key": "steam:76561198000000001",
"Kind": "Steam",
"SteamId": "76561198000000001",
"PlayFabId": "",
"Host": "",
"Port": 0,
"JoinCode": "",
"ServerName": "老王的英灵殿",
"Password": "dpapi:AQAAANCMnd8BFdERjHoAwE/Cl+sBAAAA...",
"LastJoinedUtc": 1789488735,
"LastJoinedLocal": "2026-09-16 00:12:15",
"UseCount": 3,
"Note": ""
}
]
}
Password 字段带前缀,三种格式可以共存:
| 前缀 | 含义 | 换电脑能读吗 |
|---|---|---|
dpapi: |
Windows DPAPI(ProtectedData,CurrentUser 作用域),真正用你当前 Windows 账号的密钥加密 |
❌ 不能 |
obf: |
固定密钥异或 + Base64。只是防肩窥,不是加密 | ✅ 能 |
plain: |
明文。只能手工改出来(模组自己不会写) | ✅ 能 |
游戏 Managed 目录下自带 System.Security.dll,实测里面有
System.Security.Cryptography.ProtectedData.Protect/Unprotect,所以默认走 DPAPI。
万一运行环境不支持,会自动退化到 obf: 并打一行警告,功能不受影响
(Secret.cs 里把 DPAPI 的两个调用隔离成独立方法,就是为了让类型解析失败的异常能被 catch 到)。
想完全明文(方便自己看/改):把某条记录的
"Password": "dpapi:xxx"手工改成"Password": "plain:你的密码"即可,模组认这个前缀。
为什么不用 Newtonsoft.Json(游戏自带 13.0.0):实测把游戏那份拿到 .NET Framework 下加载会
FileLoadException / 0xCORSEC_E_INVALID_STRONGNAME(强名称验证失败)。
Mono 一般不校验强名称、理论上没问题,但这一点我没法离线证实 ——
万一真加载不了整个模组就废了。所以改成自带的 src/Json.cs(一百来行),
好处是所有逻辑都能离线跑测试。UnityEngine.JsonUtility 是原生实现,脱离 Unity 跑不了,同样排除。
七、配置(BepInEx/config/easylogin.client.only.cfg)
| 分区 | 键 | 默认 | 说明 |
|---|---|---|---|
| 1 - 自动登录 | Enabled |
true |
总开关 |
| 1 - 自动登录 | AutoSubmit |
true |
true 直接提交;false 只把密码填进输入框,等你按回车 |
| 1 - 自动登录 | ForgetOnPasswordError |
true |
服务器说密码不对就删记录。强烈建议保持 true,否则改密码后会被锁在门外 |
| 1 - 自动登录 | RememberManualInput |
true |
手动输的密码也记下来;关掉后新服务器不新增记录(已有的仍会更新) |
| 2 - 服务器识别 | MatchByJoinCode |
true |
跨平台后端下用房间码兜底匹配 |
| 3 - 存储 | EncryptWithWindowsUserKey |
true |
用 DPAPI 加密;关掉则退化为 obf: 混淆(可跨机移植) |
| 3 - 存储 | ShowPasswordInLog |
false |
把密码原文打进 BepInEx 日志,只在排错时开 |
| 4 - 日志 | VerboseLog |
false |
每次识别服务器都打一行 key / 房间码 / IP |
八、故障排查
启动时 BepInEx/LogOutput.log 里应该能看到(这是自证清单,出问题先看这几行):
EasyLogin 1.0.0 开始挂载 Harmony 补丁……存储文件:...\BepInEx\config\EasyLogin\servers.json
已挂载补丁类:Patch_ClientHandshake
已挂载补丁类:Patch_OnPasswordEntered
已挂载补丁类:Patch_RpcError
Harmony 补丁挂载完成:3/3 成功。
---- 补丁挂载自检 ----
ZNet.RPC_ClientHandshake() prefix=0 postfix=1 OK
ZNet.OnPasswordEntered() prefix=0 postfix=1 OK
ZNet.RPC_Error() prefix=0 postfix=1 OK
-----------------------
EasyLogin 就绪:已载入 N 条服务器密码记录。
| 现象 | 看日志里有没有这句 | 原因 |
|---|---|---|
| 完全没反应 | 没有 开始挂载 Harmony 补丁 |
DLL 没装上,或不是这个版本 |
| 补丁没挂上 | 某行是 !! 未挂上 !! |
游戏版本变了,方法签名对不上 |
| 不自动填 | 没存过这个服务器的密码,等你手输:... |
正常,第一次就该手输 |
| 不自动填 | 有记录但密码解不出来 |
换过 Windows 账号/电脑,DPAPI 解不开 → 删掉该条重输 |
| 不自动填 | 识别不出当前服务器的身份(后端 ...) |
Steam ID / host ID / IP / 房间码全是空的,把 VerboseLog 打开看细节 |
| 老说密码错 | 服务器 ... 报告密码错误 |
房主改密码了 → 记录已自动删除,重进一次手输新密码即可 |
| DPAPI 不能用 | DPAPI 不可用,退化为简单混淆存储 |
已自动退化,功能正常 |
九、联机安全
- 没有
[NetworkCompatibility]声明(=NotEnforced,不参与连接校验) - 没有 Jotunn 依赖,没有内嵌 ServerSync / ConditionalConfigSync
- 不碰任何游戏数据、不改
ObjectDB、不发任何自定义网络包
本模组做的只是"把你自己知道的密码重新敲一遍",所以:主机不装、队友不装,都不影响你进服,
也不影响别人进你的服。(判据见 valheim-mod-compat-check 技能的实测结论。)
十、已知边界
- 只处理"服务端要求密码"这一条路(
RPC_ClientHandshake)。这是 Valheim 唯一的联机密码入口。 - 专用服务器(无头服务端)的密码是服务端自己配的,客户端这边一样适用。
ServerName只能事后回填,认证失败时会是空的 —— 不影响匹配。- 房间码(
ZPlayFabMatchmaking.JoinCode)是反射读的:直接静态引用那个类会牵出整套 PlayFab 初始化,反射更省事也更安全。读不到就当作没有,Steam / 直连 IP 两条路不受影响。
十一、开发与验证
EasyLogin/
├── EasyLogin.csproj
├── src/
│ ├── EasyLoginPlugin.cs 插件本体、配置项、补丁挂载 + 自检
│ ├── Patches.cs 三个 Harmony 补丁(含 IL 实证注释)
│ ├── ServerIdentity.cs 服务器识别(反射读 ZNet 私有静态字段)
│ ├── PasswordStore.cs servers.json 读写 + 匹配/遗忘
│ ├── Secret.cs DPAPI / 混淆 / 明文 三种存储
│ └── Json.cs 零依赖 JSON 编解码
├── _test/ 离线测试工程(不进模组)
├── _tools/ dnfile 查 API 用的小脚本
├── _make_icon.py 手写 PNG 图标(带像素自检)
└── _build.py 打包 dist/ + 写进 r2modman
离线测试(不进游戏就能验证核心逻辑,41 项检查):
cd _test && dotnet build -c Release && ./bin/Release/EasyLoginTests.exe
覆盖:DPAPI/混淆/明文的往返、自写 JSON 的转义与容错(BOM/CRLF/多余字段/坏输入)、
写入→读回→匹配→遗忘→回填、含引号换行中文的密码、手工改成 plain: 前缀、坏文件优雅降级。
交付前自查:
python <skills>/valheim-mod-dev/scripts/verify_dll.py bin/Release/EasyLogin.dll
python <skills>/valheim-mod-compat-check/scripts/scan_compat.py bin/Release
期望:3 个类级 [HarmonyPatch]、依赖表干净(无 Jotunn)、无 NetworkCompatibility 声明。