Valheim
You are viewing a potentially older version of this package. View Latest Version
Install with App

Details

Date Uploaded
2 weeks ago
Downloads
535
Size
52KB
ADDatHost Valheim hosting
€1

EasyTeleport

大地图上的传送按钮。 打开地图(M),画面左上角出现两个按钮 —— 外观默认与游戏原生控件同化(运行时继承原版面板底图),不会显得像外挂的贴纸。

按钮 做什么
传送回床 传送到你记录过的床(在床上睡过觉设下的出生点)。
任意传送 按一下 → 按钮变绿进入待选 → 再点地图上任意位置,就传送到那里。
再按一次按钮可取消。

传送落地是安全的:陆地自动落到地面上方、点水面会浮在水上游泳、永远不会摔死。 传送在任何状态下都能用 —— 游泳、奔跑、跳跃、在船上、在水里都行。

AI 生成声明:本模组由 AI 智能体(WorkBuddy)在用户的明确需求与逐项验收下编写; 需求描述、交互设计与发布决定均由用户主导。


用法

传送回床

  1. 先在床边上按 E 睡一觉(这一步会在角色档案里记下出生点;不睡就点按钮会提示"还没有记录过床")。
  2. 按 M 打开大地图 → 右下角点 「传送回床」 → 黑屏淡入淡出 → 人已经在床边。

任意传送(地图上点哪去哪)

  1. 按 M 打开大地图。
  2. 点 「任意传送」 —— 按钮变成绿色并显示「取消 · 点地图选点」,同时左上角弹一条提示。
  3. 在地图上点任意位置 → 直接传送过去。
    • 点偏了?再点一次地图就行(待选状态会保留到你成功传送或主动取消)。
    • 不想传了?再点一次按钮取消,或者按 M 关掉地图(也会自动取消)。

⚠️ 游戏对传送有 2 秒冷却(原版传送门同款)。刚传送完立刻再传会被拒绝, 屏幕上会提示"传送被游戏拒绝 —— 通常是因为刚传送过"。此时待选状态不会丢,等一两秒直接再点地图即可。


落点是怎么保证安全的

这一块没有自己写地形算法 —— 而是利用了原版 Player.UpdateTeleport 里本就有的一套落地校正流程, 再喂给它一个"在什么地形上都成立"的高度值。所有结论都来自对 assembly_valheim.dll 的反汇编。

原版的流程(Player.UpdateTeleport,distantTeleport = true 时)

①  player.position = 我们给的 (x, Y, z)          ← 这里 Y 是我们唯一能控制的
②  等 ZNetScene.IsAreaReady(target)              ← 远处传送就是在这里等区域加载
③  if (ZoneSystem.FindFloor(target, out floor))  ← Raycast(target + up×1, down, 1000 m)
        return;                                  ← ★ 命中就结束,玩家高度 = 我们给的 Y(不再校正)
④  else(没命中)
        player.y = ZoneSystem.GetSolidHeight(target) + 0.5
                                                 ← Raycast(target + up×1000, down, 2000 m)

同时,在搬运玩家的那一刻原版会执行 Character.m_maxAirAltitude = transform.position.y; 摔落伤害 = 当前高度 − 历史最高滞空高度,所以传送落地时该差值恒为 0 ⇒ 不会摔伤。

我们取的 Y = ZoneSystem.c_WaterLevel

ZoneSystem.c_WaterLevel 是一个 public const,从 DLL 的 Constant 表里读出来是 30.0f(Valheim 海平面)。 取这个值,两条分支在地形上的表现是:

地形 ③ FindFloor 最终落点
陆地(地面高于水面) 射线从水面上方往下打够不到地面 → 未命中 走 ④ → 精确落到地面 +0.5 米
海洋 / 湖泊(水底低于水面) 射线命中水底 → 命中 高度保持水面 → 玩家浮在水面游泳

离线场景矩阵验证(scripts/verify_landing.py,12 个场景)全部通过:

高山(Deep North 峰)   地面 420.0 → GetSolidHeight 校正 → 落点 420.5  OK 落在地面 +0.5 米
山地                    地面 180.0 → GetSolidHeight 校正 → 落点 180.5  OK 落在地面 +0.5 米
平原 / 草原             地面  50.0 → GetSolidHeight 校正 → 落点  50.5  OK 落在地面 +0.5 米
略高于水面的海岸         地面  32.5 → GetSolidHeight 校正 → 落点  33.0  OK 落在地面 +0.5 米
刚好在水面              地面  30.0 → FindFloor 命中      → 落点  30.0  OK
浅滩(水下 5 米)        地面  25.0 → FindFloor 命中      → 落点  30.0  OK 落在水面(可游泳)
沼泽低地(水下 12 米)    地面  18.0 → FindFloor 命中      → 落点  30.0  OK 落在水面(可游泳)
浅湖底(水下 25 米)     地面   5.0 → FindFloor 命中      → 落点  30.0  OK 落在水面(可游泳)
深海海床                地面 -55.0 → FindFloor 命中      → 落点  30.0  OK 落在水面(可游泳)
最深海床                地面 -95.0 → FindFloor 命中      → 落点  30.0  OK 落在水面(可游泳)
世界最高峰(极端假设)    地面 900.0 → GetSolidHeight 校正 → 落点 900.5  OK 落在地面 +0.5 米
超出校正范围的极端高      地面1100.0 → 两条都失败          → 落点1030.0  FAIL(Valheim 无此地形)

⚠️ 这也解释了为什么 UseDistantTeleport 必须保持 true: 只有远距离传送模式才会在 FindFloor 未命中时执行第 ④ 步校正。 把它关掉,跨半张地图传送就可能"人到了但地面还没来",或者停在未校正的高度上。


位置与外观

位置:画面左上角,用比例锚点

按钮组的左上角钉在画面的 (LeftAnchor, TopAnchor) 处,默认 0.084 / 0.392, 也就是 1920×1080 下大约第 (162, 423) 像素 —— 位于地图上方那片空白区域, 不会压到地图本身的内容、图例文字、左边和右边的两列图钉图标。

用比例而不是像素边距,是为了换分辨率时位置不会跑偏(像素边距在 4K 下会显得贴边)。 嫌位置不合适就改这两个值:TopAnchor 越小越靠上,LeftAnchor 越小越靠左。

外观:继承游戏原生 UI

UseGameStyle = true(默认)时,模组会在运行时从大地图已有的控件里翻一个"面板/按钮底图" sprite 过来复用:

优先级 来源
① 名字里带 button / panel / background / _bg / frame 的 Image
② 有九宫格边界(Sprite.border != 0)的 Image —— 这种才是可拉伸的面板底图
③ 任意带图的 Image
④ 都没有 → 退回纯色半透明深灰

拿到之后就按九宫格(Image.Type.Sliced)铺满按钮,所以圆角、渐变、描边都跟原版控件一致。 文字用米白色 + 1 像素黑描边,和原版 UI 的文字处理方式一致。

⚠️ 开着 Verbose 可以在日志里看到底图具体取自哪个控件(按钮底图取自 … sprite=… 九宫格=True)。 万一某次更新后取到的图不合适(比如拿到一个小图标铺开变形),把 UseGameStyle 改成 false 即可退回干净的纯色底 —— 这时按钮的长相由 ArmedColor 等配置控制。

为什么中文字体不能跟着游戏走

游戏内置字体 AveriaSansLibre 不含 CJK 字形,用它画中文会是一排方块。 所以字体默认走系统字体(微软雅黑),并从系统字体清单里按名字找。 观感上和原版 UI 已经很接近(都是无衬线体),但确实不是同一个字体 —— 这是中文环境下的必然取舍。


配置

BepInEx/config/easyteleport.client.only.cfg

分区 键 默认 说明
1 - General Enabled true 总开关
2 - Buttons BackToBedText 传送回床 按钮文字
AnywhereText 任意传送 按钮平时显示的文字
ArmedText 取消 · 点地图选点 待选状态显示的文字
ArmedColor 深绿 待选状态下按钮的绿色调(UseGameStyle = false 时作为底色)
UseGameStyle true 按钮外观继承游戏原生 UI 底图(见下节)
LeftAnchor 0.084 按钮组左上角的横向比例位置(0=最左,1=最右)
TopAnchor 0.392 按钮组左上角的纵向比例位置(0=最上,1=最下)
ButtonWidth / ButtonHeight 190 / 40 按钮尺寸(像素)
Spacing 6 两按钮间距
FontSize 16 字号
FontName Microsoft YaHei UI 字体(见下)
3 - Teleport CloseMapOnTeleport true 传送后自动关掉大地图(回到小地图)
UseDistantTeleport true 远距离传送 —— 落地安全的前提,别关
ShowMessages true 屏幕左上角提示
4 - Debug Verbose false 详细日志

关于字体:游戏内置字体(AveriaSansLibre)不含中文字形,直接画中文会变方块, 所以默认走系统字体(微软雅黑)。查找顺序: 配置里的名字 → 一组常见中文字体 → 游戏内置字体 → 已加载的任意字体。 想让按钮跟游戏 UI 完全统一(但只能显示英文),把 FontName 改成 AveriaSansLibre, 同时把按钮文字改成英文即可。

⚠️ 改完配置必须重启游戏(BepInEx 只在插件 Awake 里读一次)。 装了 BepInEx.ConfigurationManager 的话可以按 F1 直接改,那种是即时生效的。


兼容性

  • 纯客户端:无 Jotunn、无 ServerSync、无 NetworkCompatibilityAttribute(= NotEnforced)。
  • 联机:主机和其他玩家都不需要装;没装的人完全不受影响。 本模组只调用游戏自己的接口(Player.TeleportTo),不改任何主机权威数据。
  • 与地图类模组(如 BetterMap)不冲突 —— 按钮挂在游戏自己的大地图根节点下, 只占屏幕右下角一块;位置撞了改 MarginBottom / MarginRight 即可。
  • 传送门附近也可以用,本模组与本机已装的 QuickTeleport 无交互。

实现要点(全部来自 IL 实证,2026-09-18)

床的坐标从哪来

PlayerProfile.HaveCustomSpawnPoint()   → bool      (public)
PlayerProfile.GetCustomSpawnPoint()    → Vector3   (public)

两者的 IL 都只有三行:ZNet.instance.GetWorldUID() → GetWorldData(uid) → 读 WorldPlayerData.m_haveCustomSpawnPoint / m_spawnPoint。 ⇒ 按世界 UID 分开存,换世界不会串坐标。 原版 Minimap.UpdateProfilePins(地图上那个"床"图标)用的也是这两个方法。

⚠️ 没有 Player.m_spawnPoint 这种字段(Player 里跟 spawn 有关的只有 m_spawnEffects)。

地图点击 → 世界坐标

原版 Minimap.OnMapLeftClick(public)里的两步:

ZInput.pointerPosition  →  Vector3(屏幕坐标)
Minimap.ScreenToWorldPoint(屏幕坐标)  →  Vector3(世界坐标,Y 恒为 0,只有 XZ 有效)

ScreenToWorldPoint 是 private —— 本模组用反射调用它(和原版走完全相同的换算: RectTransformUtility.ScreenPointToLocalPointInRectangle → Rect.PointToNormalized → 按 RawImage.uvRect 映射 → MapPointToWorld)。

ZInput 定义在 assembly_utils.dll 里(不是 assembly_valheim), 用 AccessTools.TypeByName("ZInput") 反射取 pointerPosition,拿不到时回退 Input.mousePosition。

怎么"吞掉"那次点击

Minimap.OnMapLeftClick 是 public,直接挂 Prefix:

[HarmonyPatch(typeof(Minimap), "OnMapLeftClick")]
internal static class MinimapLeftClickPatch
{
    [HarmonyPrefix]
    private static bool Prefix() => MapTeleportButtons.HandleMapLeftClick();
}

返回 false 时原版不会继续走"选中 / 取消选中点到的图钉"那段逻辑。

顺带一提:原版放图钉是中键(Minimap.OnMapMiddleClick),所以待选状态下按中键不受影响。

传送走哪条路

Player.TeleportTo(Vector3 pos, Quaternion rot, bool distantTeleport)   (public,返回 bool)

IL 分两支:

if (!m_nview.IsOwner()) {
    m_nview.InvokeRPC("RPC_TeleportTo", pos, rot, distantTeleport);
    return false;                      // ← RPC 已发出去了,false 不代表失败
}
if (IsTeleporting())          return false;
if (m_teleportCooldown < 2f)  return false;      // ← 2 秒冷却在这里
m_teleporting = true;
m_teleportFromPos   = transform.position;
m_teleportTargetPos = pos;
return true;

⇒ 本模组的返回值处理:自己是主人时 true = 真的开始传送了; 不是主人时忽略返回值。玩家自己的角色对象正常就是自己拥有的,这条只是兜底。

按钮挂在哪

Minimap.m_largeRoot : GameObject   (public)
Minimap.IsOpen() 的 IL = s_instance != null && (m_largeRoot.activeSelf || m_hiddenFrames <= 2)

挂在 m_largeRoot 下的好处:地图关掉 → 根节点失活 → 按钮自动隐藏, 不用自己监听地图开关。

⚠️ 三个已经踩过的坑:

  1. MapMode 是 Minimap 的嵌套类型(和 PinData / PinType 一样), 写裸的 MapMode 会 CS0103「当前上下文中不存在名称 MapMode」—— 看着像"类型不存在",其实是嵌套类型没加外部类名。
  2. 不用 IMGUI 画按钮:大地图是全屏 uGUI Canvas,OnGUI 画的东西会被整个盖住 (看不见也点不到)。所以走原生 uGUI。
  3. Minimap.m_nameInput 的类型 GuiInputField 继承 TMP_InputField(TextMeshPro), 在 gui_framework.dll 里,想从它拿字体会连锁引 gui_framework + Unity.TextMeshPro, 而且它没有 .textComponent(那是 uGUI InputField 的)—— 不值得,改用系统字体。

为什么按钮不会误触发地图操作

原版全程序集里 EventSystem.IsPointerOverGameObject 只有 FejdStartup.UpdateCharacterRotation 一个调用者 —— 地图自己不做"鼠标是否落在 UI 上"的判断。 所以防误触靠的是层级隔离:地图的可点击区域(UIInputHandler)挂在 m_mapLarge 上, 按钮挂在它的兄弟节点 m_largeRoot 下,点击传不进地图的输入处理。


已知限制

  • 只能在已探索的地图区域点选 —— 大地图本身只显示探索过的范围,点到未探索区域等于盲传 (游戏仍会正确落地,但你不知道那是哪)。
  • 传送到床需要角色在床上睡过至少一次。
  • 床的落点是上次睡觉时记录的那个位置;之后把床拆了 / 挪了,原版记录不会自动更新(要重新睡一觉)。
  • 待选状态在关闭地图时会自动取消;退出游戏时会连同按钮状态一起重置(不落盘)。
  • 极少数带反作弊的多人服务器可能拒绝客户端自行改位置;自用 / 自建服无此问题。

安装

r2modman「Install from file」选 EasyTeleport-1.1.0.zip, 或把 EasyTeleport.dll 丢进 BepInEx/plugins/。

前置:BepInExPack_Valheim 5.4.2350。

版本

  • 1.2.0(2026-09-18):按钮从左下角移到画面左上角(比例锚点定位,可配置), 外观改为继承游戏原生 UI 底图(UseGameStyle),尺寸与字号按原版控件收窄。
  • 1.1.0(2026-09-18):把「返回出发地」换成**「任意传送」**(地图上点哪去哪), 并接入原版的落地校正流程(水面高度 + FindFloor / GetSolidHeight),永不埋地、不摔死。
  • 1.0.0(2026-09-18):首个版本(回床 + 返回出发地)。
DatHost — Valheim server hosting, first month for one euro
DatHost — Valheim server hosting, first month for one euro
DatHost — Valheim server hosting, first month for one euro
DatHost — Valheim server hosting, first month for one euro
DatHost — Valheim server hosting, first month for one euro