Source-checkedQuality of life & UI

EasyLogin

by s6652289 · for Valheim

[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.

209Downloads
48d agoLast updated
BepInExLoaders
ThunderstoreSource

About EasyLogin

From Thunderstore

[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<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:你的密码" 即可,模组认这个前缀。

Read the full description on Thunderstore →

Highlights

From source data

Loaders

Runs on BepInEx.

Footprint

Rated a light load on a server.

Last update

Its author last shipped an update 48d ago.

EasyLogin on Nxeon

From our catalogue
One-click install
Supported on Nxeon Valheim servers, for BepInEx servers. We download it from Thunderstore, place it, and restart the server.
Server type needed
BepInEx
Server load
light
Smallest plan we suggest
Valheim — 4 GB (4 GB RAM). This follows our own load rating above, not a benchmark.
Latest build
1.0.0, released 2026-09-17

Game versions by loader

LoaderGame versionsNewest
BepInEx

From 1 recorded build of EasyLogin.

Dependencies

Declared by the newest build that lists any. Required ones are installed with it on one-click installs.

Run EasyLogin on a Valheim server

Pick a server or deploy a new one above; it is installed in one click.

What kind of mod it is

From the catalog
Listed on Thunderstore as “Content”

Quality of life & UI

Small conveniences, tweaks and interface improvements players notice straight away.

Compatibility

Game
Valheim
Loaders
BepInEx
Requires
One-click install needs a BepInEx server.

Verified at the source

  • ✓ThunderstoreOfficial listing, checked 2026-10-01Open ↗
  • ✓Authors6652289, as published on Thunderstore
  • ✓LicenseNot published by the sourceFree

EasyLogin, answered

Generated from the mod’s own source data.

Which loaders does it support?

BepInEx.

Is it free?

Yes. EasyLogin is free to download from Thunderstore.

How do I add it to my Nxeon server?

Use “Add to a server” on this page, or open NxLabs from your dashboard. We install it, restart your server and switch it on.

NxLabs

Run EasyLogin on a Valheim server.

Deploy in about a minute from $9.99 a month, then add EasyLogin in one click.