s6652289-EasyLogin icon

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.

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-5.4.2350 icon
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.2350

README

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 填过了"这种情况排除掉,不会重复提交。

补丁 ③ 的 6ConnectionStatus.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_onlineBackendOnlineBackendType:0=Steamworks、1=PlayFab、2=EOS、3=CustomSocket) 用来判定走哪条路,避免把上一次联机的房间码套到这一次上。

一个必须说清楚的限制:服务器名字当不了判据

ZNet.m_ServerName 全程序集只有三处写入 —— SetServerOpenServer<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 声明。