FVTT 多用户共享平台:让每个人都有自己的跑团世界
自从入了 FVTT(Foundry Virtual Tabletop)的坑,跑团的体验上了好几个台阶。动态光影、自动判定、角色卡联动——比 Roll20 舒服太多了。
但问题来了:我有好几个跑团圈子,大家都想用我的 FVTT。FVTT 本身是单实例设计,一个进程只能服务一组玩家。总不能让大家排队吧?于是有了这个项目——让每个人都拥有自己独立的 FVTT 世界。
思路
核心需求很明确:多人共用一台服务器,每人一个独立 FVTT 实例,互不干扰,还能从公网访问。
拆解下来就是几个子问题:怎么管理多个 FVTT 进程?怎么给每人分配独立端口?怎么让外网能访问到本机的这些端口?怎么让用户自己注册、自己管理实例而不用每次找我操作?
最终的方案:Node.js 做 Web 门户负责用户系统和进程调度,每个用户分配独立端口 + 独立数据目录,FRP 做内网穿透把端口映射到公网。
进程管理是核心难点
FVTT 是一个 Electron 应用,通过 child_process.spawn 启动。每个实例需要指定独立的 --port 和 --dataPath。进程管理器负责跟踪每个实例的状态:运行中、已停止、启动中、异常退出。
最棘手的是启动检测。FVTT 启动不是瞬间完成的——它要先加载模块、读取世界数据、初始化数据库,这个过程可能要 15-30 秒。怎么知道它真正"就绪"了?我用了轮询检测:每隔 2 秒尝试请求 FVTT 的 /api/status 端点,直到返回 200 才算启动完成,最长等待 30 秒后超时报错。
还有一个坑:FVTT 进程有时会悄悄挂掉(内存不足、世界数据损坏等等)。所以加了健康检查——定时 ping 每个运行中的实例,发现挂掉的自动标记状态并在仪表盘提示用户。
预置池——让等待消失
最初的设计是新用户注册时才创建实例,但用户要等数据复制完成(FVTT 的 Data 目录有几百 MB),体验很不好。
后来加了一个"预置池"机制:系统后台始终保持一个提前复制好的实例模板。新用户注册时直接从池子里取,秒级分配。然后后台异步再准备下一个预置实例,形成流水线。这个改动让注册体验从"等 30 秒"变成了"即时可用"。
FRP 动态配置
每个 FVTT 实例都需要一个公网端口。FRP 的配置通常是静态写在 frpc.ini 里的,但这里端口是动态分配的(每启动一个实例就多一个端口映射)。
解决方法是动态生成 FRP 配置:每次用户启动实例时,frpManager.js 在 FRP 配置目录下新建一个 {用户名}.ini,写入该实例的端口映射规则,然后 reload FRP 客户端。用户停止实例时删除对应的配置文件并再次 reload。FRP 的 include 指令支持加载目录下的所有配置,正好用来实现这个按需加载的机制。
安全方面
用户密码用 bcrypt(12 轮哈希)存储,session 有过期时间。每个用户只能操作自己的实例——API 路由层做了权限校验,不能越权启动或停止别人的 FVTT。
FVTT 进程本身绑定在 127.0.0.1 上,不直接暴露给外网,只通过 FRP 隧道对外。这样即使有人扫到了公网端口,也无法绕过 FRP 直接访问本机服务。管理面板有独立的权限控制,只有管理员账号能查看全局状态和操作日志。
用户端体验
仪表盘设计得很简洁:一个状态指示灯(绿色运行中 / 灰色已停止)、启动/停止按钮、以及公网访问链接。用户在仪表盘点击"启动 FVTT",几秒钟后就能拿到一个 http://106.14.80.115:XXXXX 的链接,点进去就是属于他自己的 FVTT 世界。
新用户自动获得一个预置好的世界模板(cn-wild-sheep-chase,一个入门级的 D&D 冒险),不用从零开始搭建。首次进入 FVTT 时自行设置 GM 密码,之后就完全是自己的独立空间了。
项目结构
D:\fvtt free\
├── fvtt\ # FVTT 程序本体 + 模板数据
├── portal\ # Node.js Web 门户
│ ├── server.js # 主入口
│ ├── services\ # 进程管理、FRP 配置、预置池、端口分配
│ ├── routes\ # 认证、仪表盘、管理面板
│ └── views\ # EJS 页面模板
├── instances\ # 用户实例目录(自动生成,每人独立)
└── run_portal.bat # 一键启动
搭建步骤
环境要求 Windows + Node.js 18+。首次运行先装依赖:
cd "D:\fvtt free\portal"
npm install
node setup-admin.js # 创建管理员账户
node server.js # 启动门户,默认端口 8888
然后访问 http://localhost:8888 就能看到注册页面了。公网访问需要一台有公网 IP 的服务器跑 FRP 服务端,我这用的是阿里云轻量服务器。
写在最后
这个项目断断续续写了差不多两周,踩过的坑包括但不限于:FVTT 进程偶尔僵尸、FRP reload 的竞态条件、Windows 下 spawn 的路径编码问题……但最终效果还不错,几个跑团圈子同时在线完全没问题。
后续想做的:容器化(每个实例跑在 Docker 里,资源隔离更好)、自动备份(定时把用户的世界数据打包存到云端)、以及一个简单的世界市场(用户可以分享自己做的好玩模组)。
代码是开源的,仅供学习交流。如果你也想给跑团群搭建一个共享 FVTT 平台,希望这篇文章能帮你少踩几个坑。