46 KiB
部署指南
目标:在一台家用 Windows 小主机上长期开服,让朋友通过局域网或公网来玩。macOS / Linux / Docker 放在后面。
所有命令都在项目根目录执行。遇到问题先运行 node tools/doctor.mjs(只读诊断)。
0. 资源需求
| 项目 | 说明 |
|---|---|
| 服务器 CPU | 战斗在各玩家浏览器里模拟(DESIGN §14),服务器只负责回合、经济和校验:每个房间每个作战回合约 1 ms CPU。AI 队友 / 掉线玩家的战场由服务器模拟:作战开始时 3 个 AI 战场在开发机上约 0.2–0.5 s CPU,小主机上可能要几秒(分成 8 ms 小片执行,不会卡住其他房间)。SP_VERIFY=all 会复算每个真人战场,CPU 明显增加,小主机建议保持 off 或 sample。 |
| 服务器内存 | 空闲约 100 MB,每个进行中的对局再增加几 MB。 |
| 网络 | 4 人对局中服务器每回合下行约 0.25 MB(DESIGN §14 实测;0.2.6 起 1 KB 以上的 WebSocket 消息用 permessage-deflate 压缩,实测一局单人对局的服务器消息从 173 KB 降到 60 KB,约为原来的三分之一)。首次进入游戏时浏览器要从主机下载所需的图片 / Spine 模型 / 音频(按需加载,之后走浏览器缓存),公网隧道带宽小时第一次会慢一些。0.2.6 起服务器启动后在后台给素材算内容哈希(第一次约 1–20 秒,看机器和硬盘;之后只检查大小和修改时间),下发的素材清单里每个地址带上哈希:浏览器把图片、模型和音频缓存一年,服务器更新、重新解压或 Docker 重建之后,玩家只重新下载内容真的变了的文件。哈希存在 .cache/asset-hashes.json,删掉会重新计算;目录不可写(例如只读的容器)时只在内存里算。 |
| 磁盘 | 素材约 550 MB(public/assets,含中文约 65 MB、日文约 85 MB 两套干员语音)+ 依赖约 125 MB(node_modules;整合包只带运行依赖,约 65 MB);可选的本地提取约 40 MB(.venv-extract)+ 70 MB 贴图(见第 6 节)。完整包解压后约 790 MB。 |
| 玩家设备 | 支持 WebGL 的现代浏览器(Chrome / Edge / Firefox / Safari 最新版),电脑或手机平板(横屏)。老旧设备可在设置里调低画质或访问 /?board=2d。 |
服务器无状态:房间和对局只存在内存里,没有数据库和存档,不需要备份。重启服务器会结束正在进行的对局(包括断线后本可在 24 小时内回来继续的独立模拟)。唯一例外是打开可选的森空岛账号绑定(SP_SKLAND=1,第 3.2 节):它的凭据库需要持久化和备份。
1. Windows 小主机:一步步
1.1 安装与首次启动
- 安装 Node.js 22 LTS 和 Git(在 PowerShell 或「终端」里;用下面的整合包时不需要 Git):
装完关闭并重新打开终端,
winget install OpenJS.NodeJS.LTS winget install Git.Gitnode -v应显示 v22 或更高(winget 的 LTS 目前是 v24.x,同样可用)。没有 winget 时从 https://nodejs.org/zh-cn/download 和 https://git-scm.com/download/win 下载安装。 - 下载,三选一。建议放在一个固定、短、不在 OneDrive 同步范围内的目录,例如
C:\Stronghold-Protocol:- 完整包(推荐):在仓库的 Releases 页面下载最新版本的
Stronghold-Protocol-v<版本>.zip(约 560 MB,解压后约 790 MB;已含运行依赖、前端库和全部素材,包括中文、日文两套干员语音和官方 3D 棋盘、各战场官方场景等本地客户端素材),解压后把里面的Stronghold-Protocol文件夹放到上述位置。不需要 Git,首次启动也不用再下载素材。素材版权归上海鹰角网络 / Yostar,仅限非商业使用,见 NOTICE.md。 - 精简包:同一页面的
Stronghold-Protocol-v<版本>-lite.zip(约 22 MB)。代码、运行依赖和前端库与完整包相同,但不带素材:美术、Spine 模型、音频(含两套干员语音)、字体、表情和「玩法说明」教程图在首次启动时由 setup 从公开镜像下载(约 550 MB,显示进度,可中断续传;镜像设置见下面的「国内镜像下载」)。官方 3D 棋盘等本地客户端素材需要用本机客户端提取,或从同一版本的完整包复制(第 6 节)。适合下载大文件不方便、或想先下一个小包的情况;放置方式同完整包。 - 源码:
git clone https://github.com/sganggs/Stronghold-Protocol.git C:\Stronghold-Protocol
- 完整包(推荐):在仓库的 Releases 页面下载最新版本的
- 双击
C:\Stronghold-Protocol\scripts\start-windows.bat。首次会:安装依赖(npm ci;整合包已含,跳过)→ 复制前端库(整合包已含,跳过)→ 下载约 550 MB 素材(完整包已含,跳过;精简包和源码在这一步下载,显示进度,中断后再次启动会续传)→ 若检测到本机的明日方舟客户端,询问是否提取官方贴图(可跳过)→ 启动服务器并打开浏览器。 - 窗口里会打印朋友可用的地址,例如
http://192.168.1.23:3000。用另一台设备打开它确认能进入。关闭窗口即停止服务器。
等价的手动命令:npm ci、node tools/setup.mjs、npm start。
国内镜像下载
Setup 默认使用「GitHub 原始源 → jsDelivr」,不查询公网 IP,也不请求 gh-proxy.com。GitHub 下载失败时会提示如何手动开启镜像;仅添加提示,不自动切换到第三方代理。
镜像方法是在完整 GitHub 链接前加 https://gh-proxy.com/,例如:
https://gh-proxy.com/https://raw.githubusercontent.com/OWNER/REPO/BRANCH/file.png
手动开启后顺序为「前缀镜像 → 原始源 → jsDelivr」。索引、图片、Spine、音频和字体都使用此规则(音频 voice 分支跳过 jsDelivr)。镜像是第三方代理;当前只校验格式和大小,没有内容哈希校验,请自行决定是否信任并启用。npm / pip 依赖不使用 GitHub 前缀。
node tools/setup.mjs --asset-source=mirror # 手动优先国内镜像
node tools/setup.mjs --asset-source=direct # 默认:仅原始源和 jsDelivr,不使用前缀代理
$env:SP_ASSET_SOURCE = 'mirror' # 也可用环境变量显式启用
node tools/fetch-assets.mjs 同样支持 --asset-source=direct|mirror。命令行优先于 SP_ASSET_SOURCE。默认镜像前缀为 https://gh-proxy.com/,可通过 SP_GITHUB_PROXY 指定其他 HTTPS 前缀;仅配置前缀不会启用镜像。将 SP_GITHUB_PROXY 设为空字符串(或全空格)可彻底禁用前缀代理,即使选择了 mirror 模式;未设置此变量与显式设空不同,前者使用默认前缀。Windows PowerShell 的某些版本会将空值视为删除变量,可设置 $env:SP_GITHUB_PROXY = ' ' 或使用 --asset-source=direct 来明确禁用。前缀只处理 GitHub 下载链接,不重复添加。
镜像请求每个 URL 只尝试一次,响应头超时 8 秒,响应体有独立的空闲超时,失败即尝试原始源。连续 3 次网络错误、HTTP 错误或无效内容会在本次运行中关闭镜像,后续索引、素材和字体共享该状态;正在进行的镜像请求也会中止并回退。成功会清零连续失败次数;404 / 410 是资源不存在,不触发熔断。再次运行脚本会重新尝试手动启用的镜像。原始源的重试、已有文件跳过和 0.1.1 的清单缩减保护保持不变。
从历史下载记录派生的 Spine 补充贴图也按本次设置重新选择来源,禁用后不会沿用旧代理地址。
1.2 防火墙
- 第一次启动时 Windows 会弹出「Windows 安全中心警报」:勾选专用网络并点「允许访问」。
- 没弹窗或点错了,用管理员 PowerShell 添加规则(下面的开机自启脚本也会自动添加):
netsh advfirewall firewall add rule name="Stronghold Protocol" dir=in action=allow protocol=TCP localport=3000 profile=private,domain - 家里的网络要是「公用网络」,Windows 会拦截入站连接。改成专用(管理员 PowerShell;网卡名用
Get-NetConnectionProfile查看):Set-NetConnectionProfile -InterfaceAlias "以太网" -NetworkCategory Private node tools/doctor.mjs会显示规则是否存在、每个网络的类型,以及朋友可用的地址。
1.3 固定局域网 IP(推荐)
主机 IP 变了,朋友收藏的地址就失效。推荐在路由器后台的「DHCP 静态分配 / 地址保留」里把小主机的 MAC 地址绑定到固定 IP(如 192.168.1.50)。也可以在 Windows「设置 → 网络和 Internet → 属性 → IP 分配 → 编辑」里手动设置(IP、子网掩码、网关、DNS 与路由器一致,且不要与别的设备冲突)。
1.4 开机自动在后台运行
先关闭 start-windows.bat 的窗口(否则端口冲突),然后在项目目录运行(会自动请求管理员权限):
powershell -ExecutionPolicy Bypass -File scripts\install-service-windows.ps1
它会:运行一次 tools/setup.mjs → 把设置写入 scripts\service.env.cmd(node.exe 路径、端口等)→ 注册计划任务 StrongholdProtocol(开机 20 秒后以 SYSTEM 身份运行 scripts\run-server.cmd,无需登录;服务器退出后 5 秒自动重启)→ 添加防火墙规则 → 立即启动并显示状态。日志在 logs\server.log(超过 10 MB 自动轮换)。
| 需求 | 命令(都加在 powershell -ExecutionPolicy Bypass -File scripts\install-service-windows.ps1 之后) |
|---|---|
| 换端口 / 其他设置 | -Port 8080、-Verify sample、-Combat server、-BindHost 127.0.0.1(只给反向代理用) |
| 公用网络也放行 | -AllowPublicNetwork(一般不需要;Tailscale 网卡被识别为公用网络时可能需要) |
| 查看状态和最近日志 | -Status |
| 重启(更新代码后) | -Restart |
| 停止 | -Stop(下次开机仍会自动启动) |
| 卸载 | -Uninstall(删除计划任务、防火墙规则和 service.env.cmd) |
建议同时关闭睡眠,否则小主机会在无人操作时休眠:powercfg /change standby-timeout-ac 0。
替代方案:用 NSSM 注册成真正的 Windows 服务
winget install NSSM.NSSM # 或从 https://nssm.cc 下载
nssm install StrongholdProtocol "C:\Program Files\nodejs\node.exe" server\index.js
nssm set StrongholdProtocol AppDirectory C:\Stronghold-Protocol
nssm set StrongholdProtocol AppEnvironmentExtra PORT=3000 HOST=::
nssm set StrongholdProtocol AppStdout C:\Stronghold-Protocol\logs\server.log
nssm set StrongholdProtocol AppStderr C:\Stronghold-Protocol\logs\server.log
nssm start StrongholdProtocol
防火墙规则仍需按 1.2 手动添加。两种方式只选一种。
1.5 更新
cd C:\Stronghold-Protocol
powershell -ExecutionPolicy Bypass -File scripts\install-service-windows.ps1 -Stop # 装了开机自启时
git checkout -- data/assets.json # 素材清单由 setup 重新生成,先还原以免 git pull 冲突
git pull
npm ci
node tools/setup.mjs # 补下载新增的素材(已有文件会跳过)
powershell -ExecutionPolicy Bypass -File scripts\install-service-windows.ps1 -Restart
没装开机自启的话,最后一步改成重新双击 start-windows.bat。
用整合包安装的:更新包
0.2.1 起,Releases 里每个版本除了完整包和精简包,还有更新包 Stronghold-Protocol-v<版本>-update.zip:只含比之前的 0.2.x 版本改动过的文件(程序、数据、运行依赖,以及改动过的素材),通常只有几 MB。它用来升级用 0.2.0 及以后的完整包或精简包装好的文件夹(适用的版本写在 Releases 说明里);全新安装、0.1.x 和 GitHub「Download ZIP」源码包请用完整包或精简包,git clone 的用上面的 git pull。
cd C:\Stronghold-Protocol
powershell -ExecutionPolicy Bypass -File scripts\install-service-windows.ps1 -Stop # 装了开机自启时;没装就关掉服务器窗口
Expand-Archive -Force <下载目录>\Stronghold-Protocol-v<版本>-update.zip C:\ # 解压到安装文件夹的上一层,合并进 C:\Stronghold-Protocol、覆盖同名文件
powershell -ExecutionPolicy Bypass -File scripts\install-service-windows.ps1 -Restart
没装开机自启的,最后一步改成双击 start-windows.bat。也可以在资源管理器里打开 zip,把其中 Stronghold-Protocol 文件夹的全部内容复制到安装文件夹,选「替换目标中的文件」。macOS / Linux:停止服务器后运行 unzip -o Stronghold-Protocol-v<版本>-update.zip -d <安装文件夹的上一层>(安装文件夹名为 Stronghold-Protocol);不要用访达拖放,它会把同名文件夹整个替换掉。
启动时(双击启动、npm start、开机自启的计划任务、NSSM / systemd 都一样)服务器先完成更新:按 MANIFEST.json 核对全部程序文件(代码、数据、运行依赖、前端库和说明文档;素材归 setup 管,不在其中),删除 UPDATE.json 列出的、新版本不再使用的旧文件(只删内容与旧版本发布时完全一样的文件:自己改过的文件、自己装的内容包、logs、.cache 都不会动),再把 UPDATE.json 改名为 .update-applied.json,然后照常启动,日志里有一行「已更新到 v<版本>」。
- 文件夹不是更新包对应的版本(例如 0.1.x、只解压了一部分、程序文件被改过):服务器不启动,提示「这个更新包只能覆盖在 v0.2.0 … 的整合包安装上(检测到 N 个文件与 v<版本> 不一致或缺失)」并举出几个文件(开机自启的,
-Status显示的日志里能看到)。新旧文件混在一起运行,容易出现看起来像游戏 bug 的错误,所以宁可不启动。下载完整包重新安装,或把更新包重新完整解压一遍再启动(UPDATE.json还在,会重新检查;在此之前什么都不删)。 - 只有说明文档、
scripts\、tools\里的文件不一致:照常启动,日志里列出是哪些文件。UPDATE.json损坏:跳过并提示,按现有文件启动。 - 精简包装的也能用:程序部分与完整包完全相同;更新包带着完整包里改动过的素材,其余素材仍由 setup 补齐。如果更新包带来了新的本地客户端素材清单(
data/local-assets.json)而这台电脑没有对应的素材,启动时会提示:从同一版本的完整包复制public/assets/local/(第 6 节),或运行node tools/setup.mjs --local提取,不需要时删掉data/local-assets.json即可。 - 更新包不联网、不会自动更新,和完整包一样从 Releases 手动下载。
npm run doctor的「文件校验 MANIFEST.json」一行随时显示全部程序文件是否与这个版本一致(源码目录没有这个文件,不做校验)。
不用更新包也可以:停止服务器,把新版本的整合包解压到新目录后从那里启动(完整包已含素材;装了开机自启的,在新目录重新运行一次 install-service-windows.ps1)。用精简包或 GitHub「Download ZIP」源码包的:解压新版本后,把旧目录里的 public\assets、public\fonts、.cache 和 data\local-assets.json(若有)复制过去,可避免重新下载(setup 只补下新增的素材)。
2. 让不在同一网络的朋友加入
2.1 Tailscale / ZeroTier(推荐给家用小主机)
组一个虚拟局域网:不需要公网 IP、不需要改路由器、不暴露到互联网。
- Tailscale:主机和朋友都安装 https://tailscale.com/download(Windows:
winget install Tailscale.Tailscale)并登录。朋友用自己的账号时,在 Tailscale 管理后台把这台主机「Share」给他们,或邀请他们加入你的 tailnet。朋友访问http://<主机的 100.x.y.z 地址>:3000(tailscale ip -4查看;开了 MagicDNS 也可以用http://<主机名>:3000)。 - ZeroTier:在 https://my.zerotier.com 创建网络,主机和朋友安装客户端并加入同一个 Network ID,在后台勾选授权成员;访问
http://<主机的 ZeroTier IP>:3000。 - 连不上时运行
node tools/doctor.mjs:看 VPN 网卡是否被 Windows 识别为「公用网络」,是的话按 1.2 改为专用,或安装自启时加-AllowPublicNetwork。
2.2 cloudflared 临时隧道(朋友什么都不用装)
winget install --id Cloudflare.cloudflared # macOS: brew install cloudflared
cloudflared tunnel --url http://localhost:3000
把输出的 https://xxxx.trycloudflare.com 发给朋友。页面是 https 时客户端自动改用 wss://,不需要任何配置;服务器会通过隧道转发的 CF-Connecting-IP 识别真实来源(TRUST_PROXY=auto)。临时隧道每次启动地址都不同,且没有可用性保证;需要固定地址请使用 Cloudflare 账号 + 自己域名的「命名隧道」。
固定访问地址能省很多流量:浏览器的缓存、设置、统计和断线令牌都按地址(域名或 IP + 端口)分开保存。地址一换,对玩家来说就是一个新网站:每个人进游戏都要把图片、模型和音频重新下载一遍(第一局约 20–50 MB),设置和本机统计也看不到了。临时隧道每次重启都换域名,所以固定朋友圈子长期开服时,建议用命名隧道、Tailscale(2.1)或固定的公网 IP / 局域网 IP(1.3),让大家一直用同一个地址。
2.3 路由器端口转发
仅当你有公网 IPv4(很多宽带是运营商级 NAT,没有公网 IP,此时请用 2.1 / 2.2):
- 先按 1.3 固定主机的局域网 IP。
- 路由器「虚拟服务器 / 端口转发」:外部端口 3000(或任意端口)→ 内部
主机IP:3000,TCP。 - 朋友访问
http://<你的公网 IP>:外部端口。
本分支的标题入口要求绑定账号(第 3.2 节);启用模块后,服务器也会校验房间/对局操作对应的账号角色。服务器对来自互联网的连接有按网络的数量限制(每个网络最多 64 个连接,房间 / 对局数量也有上限),建议不玩时关掉转发,或优先用 Tailscale。公网账号登录使用 HTTPS(2.4)。
2.4 反向代理与 HTTPS(有域名时)
必须部署在域名根路径(客户端使用 /data/、/vendor/、/ws 等绝对路径,不支持挂在子路径下)。代理需要转发 WebSocket 升级(路径 /ws)。建议让服务器只监听本机:HOST=127.0.0.1(Windows 自启:-BindHost 127.0.0.1)。
Caddy(自动申请 HTTPS 证书,WebSocket 无需额外配置):
game.example.com {
reverse_proxy 127.0.0.1:3000
}
Nginx:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 443 ssl http2; # 新版 Nginx(1.25.1 起)写成 listen 443 ssl; 加一行 http2 on;
server_name game.example.com;
ssl_certificate /etc/letsencrypt/live/game.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/game.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 1h; # WebSocket 长连接
}
}
HTTP/2:客户端由数百个小脚本模块组成(0.2.0 起进入对局时约 340 个),隔着公网时建议让代理以 HTTP/2 提供页面:所有模块走同一条连接,远距离玩家首次进入对局明显更快。Caddy 默认就是 HTTP/2;Nginx 见上面的 http2。服务器本身只说 HTTP/1.1,局域网或本机游玩不受影响。HTTP/2 省的是往返等待,不减少字节数。
压缩:服务器自己压缩脚本、样式、数据和 Spine 的 .skel / .atlas:浏览器支持 brotli 时用 brotli(0.2.6 起,质量 5,比 gzip 快且更小:实测 .skel 小约 27%、游戏数据 JSON 小约 26%、前端库小约 14%、脚本小约 5%),否则用 gzip;图片和音频本来就是压缩格式,原样发送。浏览器只在 HTTPS 页面(以及 localhost)上声明支持 brotli,所以通过代理或隧道的 https 地址访问时用 brotli,局域网里的 http://192.168.x.x:3000 这类地址用 gzip。代理不需要另开压缩(Caddy 的 encode、Nginx 的 gzip 遇到已经压缩的响应会跳过)。
Cloudflare 缓存(命名隧道或自己的域名经过 Cloudflare 时):Cloudflare 默认只缓存常见扩展名,.skel、.atlas 和没有扩展名的音频地址 /media/… 都不在其中,每个请求都会回到开服的电脑。在 Cloudflare 控制台的「缓存规则」里加一条:主机名是你的游戏域名、且 URI 路径以 /assets/、/fonts/ 或 /media/ 开头时「符合缓存条件」,边缘 TTL 选「使用源站的 Cache-Control」。0.2.6 起素材清单里的地址带内容哈希(/assets/~<哈希>/…),服务器给这类地址一年的 immutable,换了内容的文件会换地址,所以长期缓存是安全的;/data/、页面和脚本不要加进规则(它们要随服务器更新)。注意:边缘公开缓存官方素材会增加被投诉的风险,大量媒体文件也要留意 Cloudflare 的服务条款;只给自己人玩时不加这条规则也可以。
https / wss 说明:页面通过 https 打开时客户端自动连接 wss://同一域名/ws;http 时用 ws://。服务器本身只提供 http,证书由代理 / 隧道负责。代理与服务器在同一台机器或内网时,TRUST_PROXY=auto 会信任它的 X-Forwarded-For / X-Real-IP;代理在公网另一台机器上时设 TRUST_PROXY=1(同时确保游戏端口只对代理开放)。
3. Docker
3.1 一条命令(docker compose)
仓库根目录的 docker-compose.yml 已经把构建、端口、重启策略和数据卷写好,在项目根目录执行:
docker compose up -d # 构建镜像并后台启动(首次会构建;HTTP 服务在 http://<主机>:3000)
docker compose logs -f # 跟随日志;docker compose ps 看状态
docker compose down # 停止并删除容器(数据卷 skland-data 保留)
不用 compose 时等价的原始命令(FETCH_ASSETS=1 在构建时下载素材,约 550 MB,需要联网;不下载则用占位图,或改用 B 挂载宿主机素材):
# A) 构建时下载素材
docker build -t stronghold-protocol --build-arg FETCH_ASSETS=1 .
docker run -d --name stronghold -p 3000:3000 --restart unless-stopped stronghold-protocol
# B) 不把素材打进镜像:先在宿主机运行 node tools/setup.mjs,然后挂载
docker build -t stronghold-protocol .
docker run -d --name stronghold -p 3000:3000 --restart unless-stopped \
-v "$PWD/public/assets:/app/public/assets:ro" stronghold-protocol
镜像从源码(git clone)构建,Releases 的整合包不含 Dockerfile,也不含 docker-compose.yml。镜像基于 node:22-alpine,多阶段构建,只含生产依赖;public/vendor 在构建时生成。.dockerignore 排除了 public/assets(不会把宿主机素材打进构建上下文);public/fonts、data/assets.json 和 data/local-assets.json 若存在会被复制进去。环境变量同 README(-e SP_VERIFY=sample 等),另有第 3.2 节的森空岛两项。健康检查:GET /healthz。
3.2 森空岛账号绑定与数据卷
可选的账号绑定(server/skland/,见 skland.md)把账号凭据、JWT 签名密钥和可撤销的登录记录存进同一个 SQLite 文件;这是服务器需要跨重启保留的账号数据(房间和对局仍然只在内存里,第 0 节)。JWT 以 HttpOnly Cookie 保存 30 天,刷新、重新开页面或重启服务不用重新绑定;解绑会使旧 JWT 失效。不设置 SP_SKLAND 时模块不启用、不建库,/skland/* 统一返回 JSON 404;本分支的标题入口仍要求绑定,因此开服需要启用它。需要含 node:sqlite 的 Node 22.5 或更新版本。
| 环境变量 | 默认 | 说明 |
|---|---|---|
SP_SKLAND |
空(关闭) | 设为 1 时打开账号绑定模块 |
SP_SKLAND_DB |
data/skland.db |
SQLite 凭据库的路径(相对路径相对项目根目录);父目录不存在时自己创建 |
SP_AUTH_SECRET |
自动生成并存入数据库 | 可选的 JWT 签名密钥,至少 32 字节;不要每次启动随机改变,轮换会使现有登录失效 |
SP_PUBLIC_ORIGIN |
按请求来源判定 | 可选的固定访问地址,例如 https://game.example.com,用于同源校验和 HTTPS Cookie |
SP_SKLAND_DIAGNOSTIC_KEY |
空(关闭) | 仅管理员诊断接口使用;调用需 X-SP-Diagnostic-Key,不要填进前端或发给玩家 |
docker-compose.yml 里这两个变量都指向挂载的具名卷:
environment:
SP_SKLAND: "1"
SP_SKLAND_DB: /app/skland-data/skland.db
volumes:
- skland-data:/app/skland-data
- 为什么用卷:容器以
USER node运行,删除容器、docker compose down或重建镜像都会丢掉镜像层里的文件;数据库放在具名卷skland-data上,重启、docker compose down和docker compose up --build之后绑定关系都还在。docker compose down -v(或docker volume rm)才会删除它。 - 为什么用具名卷而不是绑定挂载:Windows / macOS / Linux 上行为一致,不需要先建宿主机目录、也不用管容器内
node(uid 1000)对宿主机目录的权限。要在宿主机上直接读这个文件时,把挂载换成- ./skland-data:/app/skland-data(目录已在.gitignore里)。 - 关掉模块:把
SP_SKLAND和SP_SKLAND_DB两行删掉或注释掉即可(卷可以留着不用),服务器就不再保存任何账号凭据。手工docker run时对应地去掉-e SP_SKLAND=1 -e SP_SKLAND_DB=…两个参数。 - 改用别的路径:
SP_SKLAND_DB换成卷里的其它位置(例如/app/skland-data/skland-2.db)。内置静态服务会拒绝下载当前配置的库及 WAL/SHM 文件,但仍建议放在所有静态目录之外,避免其它文件服务器或代理规则泄露。
公网使用 HTTPS;代理要保留 Host 并传递 X-Forwarded-Proto(2.4),也可显式设置 SP_PUBLIC_ORIGIN。
HTTPS 下 Cookie 带 Secure;本地 127.0.0.1 开发使用 HTTP。游戏内用角色 UID + 区服标识账号,昵称可以重复。
登录记录不保存密码,JWT 也不携带鹰角/森空岛凭据。
随账号跨设备恢复:新设备登录同一账号并选择同一区服角色后,可在标题页点击「恢复账号对局」,无需复制 本机缓存。查询对局或自动恢复登录不会挤掉旧设备;点击「开始」或确认恢复后才接续原席位,旧设备断开且不会 自动抢回连接。恢复仍受原有断线期限限制(独立模拟 24 小时、同盟对局 10 分钟,等候房间另受 60 秒宽限期限制), 不包含服务器重启后的对局存档。只会查询当前角色,昵称相同或其他区服不会串号。
这个数据库是凭据,按密钥对待。
skland.db里是明文保存的access_token、长期cred和签名密钥cred_token,拿到文件等于拿到对应账号的接口权限(skland.md §9):
- 限制权限:只让运行服务器的账号可读,例如
chmod 700 skland-data && chmod 600 skland-data/skland.db*(WAL 模式下还会有-wal/-shm两个附属文件,同样处理)。在宿主机上运行而非容器里时默认路径是data/skland.db,权限同理。- 绝不通过 HTTP 暴露:卷挂在
/app/skland-data,不在public/、data/、shared/任何静态挂载之下;反向代理也不要为它开规则。- 不提交、不进镜像:
.gitignore忽略了data/skland.db、-wal/-shm和skland-data/;构建上下文只会把data/复制进镜像,所以容器部署要把库放在data/之外(compose 已经这么做)。- 备份要当密码处理:
docker compose stop后再从卷里拷贝(WAL 写入中直接拷可能拿到不一致的副本),备份文件加密保存、限权,不要放进网盘 / 仓库 / 聊天记录;不再使用时删除卷(docker compose down -v)。
4. macOS / Linux 常驻
-
临时开服:
scripts/start.sh(或npm start),保持终端窗口打开。macOS 首次会询问是否允许 node 接受传入连接,选「允许」。 -
Linux systemd(
/etc/systemd/system/stronghold.service,路径与用户按实际修改):[Unit] Description=Stronghold Protocol game server After=network-online.target Wants=network-online.target [Service] WorkingDirectory=/opt/Stronghold-Protocol ExecStart=/usr/bin/node server/index.js Environment=PORT=3000 HOST=:: Restart=always RestartSec=5 User=stronghold [Install] WantedBy=multi-user.targetsudo systemctl daemon-reload && sudo systemctl enable --now stronghold;日志journalctl -u stronghold -f;防火墙sudo ufw allow 3000/tcp。
5. 排错
| 现象 | 处理 |
|---|---|
| 任何问题 | node tools/doctor.mjs:Node 版本、依赖、素材完整性、端口、局域网地址、防火墙、网络类型 |
端口已被占用 / EADDRINUSE |
已经有一个服务器在运行(自启任务?)或其他程序占用 3000:换端口 scripts\start-windows.bat --port 3001 |
| 朋友打不开页面 | 防火墙规则 / 网络类型(1.2);确认用的是 LAN 地址而不是 localhost;访客 Wi-Fi 常开启「AP 隔离」;不在同一网络请看第 2 节 |
| 画面是占位图、没有声音 | 素材没下完:重新运行 node tools/setup.mjs(会续传);缺失明细在 .cache/assets-report.json。默认仅原始源和 jsDelivr;可用 --asset-source=mirror 手动开启前缀镜像(见上文) |
| 素材下载很慢 / 失败 | 网络问题可随时中断,重新运行会跳过已完成的文件;node tools/fetch-assets.mjs --concurrency=4 降低并发。有文件没下载成功时,素材清单 data/assets.json 保持不变(脚本列出缺少的条目并以非零状态结束;游戏里缺的图片用占位图,缺的声音不播放),重新运行即可补齐 |
| 表情显示成默认图标、「玩法说明」只有文字要点 | 素材没下载完整:重新运行 node tools/setup.mjs(表情和教程图随其他素材一起从公开镜像下载,不需要客户端);缺失明细在 .cache/assets-report.json |
| 本地提取失败 | 游戏照常运行,只是第 6 节表格里的几样换成替代样式。确认客户端已下载全部资源;Python 版本太新导致依赖安装失败时,安装 Python 3.12 后删除 .venv-extract 再运行 node tools/setup.mjs --local |
| 3D 棋盘没出现 | 需要本地提取的棋盘贴图(node tools/doctor.mjs 会显示「3D 棋盘可用」),以及支持 WebGL2 的浏览器。没有客户端的服务器可以从同一版本的整合包复制本地素材(第 6 节) |
素材地址里的 ~ 加一串字母数字 |
/assets/~<哈希>/… 是 0.2.6 起的带版本地址(见第 0 节「网络」),浏览器可以长期缓存,属于正常现象。怀疑它导致问题时,可以用 SP_ASSET_INDEX=0 启动服务器临时关闭(清单改回普通地址),并请在 issue 里说明 |
| 想知道开服电脑上传了多少流量 | 浏览器打开 http://localhost:3000/healthz?traffic=1(换成你的端口):traffic.http 是服务器启动以来各类内容(assets 图片和模型、audio 音频、code 页面和脚本、data 游戏数据和素材清单)发出的字节数和请求数(压缩后的实际字节,notModified 是只回了「没变」的 304),traffic.ws 是 WebSocket 的线上字节(wireOut / wireIn,压缩后)和压缩前的消息字节(payloadOut / payloadIn)。数字只在内存里,重启后从 0 开始;只有本机和局域网(含虚拟局域网地址)的访问能看到,公网访问者(包括经隧道 / 反代转发来的)只拿到普通的 /healthz。玩家那边:设置 →「问题反馈」复制的诊断里有一行 traffic:,是这个页面从打开起下载的字节数(cached 是浏览器缓存直接给的,不占流量;external 是其他网站,例如 Google 字体) |
| 断线 | 同盟模拟 10 分钟内、独立模拟 24 小时内(config.constants.singleReconnectTime)用同一浏览器重新打开页面,自动回到原座位。同盟掉线期间按原阵容自动作战、到时自动准备(不会代为购买;想让 AI 代打请用「离开模拟 → 暂离(AI 托管)」);独立模拟不计时,等你回来 |
6. 本地客户端素材(可选)
public/assets/local/ 和 data/local-assets.json 是从本机安装的《明日方舟》客户端里提取的官方素材(tools/local-extract,DESIGN §13):node tools/setup.mjs 检测到客户端时会询问是否提取,之后可以用 node tools/setup.mjs --local 重新提取,或用 --game "<…/StreamingAssets/AB/Windows>" 指定客户端目录。setup 从公开镜像下载的素材不包含这部分,所以在没有客户端的电脑上(例如 Linux 服务器)用源码或精简包部署时不会有它;Releases 的完整包里已经带上了。
没有本地素材时游戏照常运行,只是下面几样换成替代样式:
| 内容 | 没有本地素材时 |
|---|---|
| 官方 3D 棋盘(贴图、模型、地图特效) | 2D 棋盘,地块由程序绘制 |
| 各战场的官方场景(沙地等 11 个战场,0.2.5 素材包;完整包在提取后带上,精简包和源码可在本机下载并提取,见下) | 3D 棋盘用程序生成的金属地块 |
| 部分官方界面图标与底板:交流按钮和表情面板的边框、暂停面板、装备替换窗口、干员调配界面、队友状态与漏怪标记、模组类型图标等 | 样式相近的替代图形、图标或文字 |
| 灼热 / 炽焰源石虫的官方模型 | 染成橙色 / 红橙色的普通源石虫 |
| 39 个召唤物的官方模型(多数自选召唤物,以及凯瑟琳的爬行号·防护单元、凛御银灰的风雪之眼;公开镜像没有) | 召唤物头像(菱形底板) |
表情(6 套 × 6 个)和「玩法说明」的 19 页教程图公开镜像也有:node tools/setup.mjs 会和其他素材一起下载(约 21 MB),不需要客户端;有本地素材时优先显示本地的。
没有客户端的服务器想要上表中的官方素材:从同一版本的完整包(Releases)里,把 public/assets/local/ 文件夹和 data/local-assets.json 复制到服务器项目目录下的相同位置。服务器每次请求都会重新读取这两处,不必重启,玩家刷新页面即可。一定要用与服务器代码相同版本的完整包:各版本提取的内容和清单可能不同(例如灼热 / 炽焰源石虫的模型是 0.1.0 之后才加入的,召唤物模型是 0.2.0 加入的),混用其他版本的文件会缺图或用错图。复制后 node tools/doctor.mjs 会显示本地素材的条目数和「3D 棋盘可用」。
0.2.0 之前提取过的:召唤物模型是 0.2.0 新增的提取项,旧的提取结果里没有(node tools/setup.mjs 会提示「缺少新版的自选召唤物模型」)。有客户端的电脑运行 node tools/setup.mjs --local 重新提取即可,只想补这一项也可以在提取用的 Python 环境里运行 tools/local-extract/extract.py --only spine/token(新文件写入 public/assets/local/spine/token/,清单里其他条目保持不变)。
各战场的官方场景(0.2.5 素材包,维护者 2026-10-12:可以进完整包;精简包和源码安装可以在本机下载):官方棋盘是每个战场一个做好的场景(11 个战场,其中 9 个是沙地)。场景包只在赛季开放期间随客户端分发,赛季外的客户端里没有。仓库不存放这些官方文件。有客户端的电脑可以 node tools/setup.mjs --scenes(询问或强制下载并提取;--no-scenes 跳过)。也可以分两步:python3 tools/local-extract/fetch_scenes.py(从官方 CDN 下载赛季版本 26-04-13-12-31-42_e9b560 的 33 个场景包,共 22,801,308 字节,放进 .cache/official-scenes/;--list 只看清单不下载),再用提取环境的 Python 运行 tools/local-extract/extract.py --only scene(材质和贴图仍取自本机客户端;--game 可指定客户端目录)。没有 Python 3.8+,或 UnityPy / lz4 / Pillow 没装好时,setup 只提示并继续,3D 棋盘用金属地块。没有客户端时贴图取不出来,从同一版本的完整包复制 public/assets/local/ 和 data/local-assets.json。作者估计每个场景在进对局时另下载约 2–7 MB,各场景共用贴图约 3 MB;本机没有跑过提取,完整包因此增加的解压体积没有实测。
3D 棋盘贴图的下载量:每位玩家进入对局时都要从开服的电脑下载 3D 棋盘的 12 张贴图。提取时会给这 12 张各写一份 WebP(颜色贴图有损、质量 95,法线和数据贴图无损),清单里列的是 WebP,同名 PNG 留在旁边给裁切工具和 setup 用。这部分下载量从约 6.7 MB 降到约 2 MB,网速慢的远程联机最明显。只有 PNG 的本地素材(例如在这一改动之前提取的)可以用提取时的 Python 环境运行 tools/local-extract/extract.py --webp 就地补上,只需要 Pillow,不需要客户端。
「玩法说明」教程图的下载量(0.2.6 起):本地提取的 19 页教程图每页 1.2–1.5 MB,玩家每翻一页都要从开服的电脑下载。提取时同样给每页写一份 WebP(有损,质量 90,放大两倍对比看不出差别),清单列 WebP,PNG 留在旁边;19 页合计从约 25.6 MB 降到约 4.2 MB。看说明时只预载下一页(以前前后两页都预载)。已经提取过的同样用 tools/local-extract/extract.py --webp 补上。
7. 打包发布(维护者)
Releases 的 zip(完整包、精简包,0.2.1 起还有更新包)由 tools/package.mjs 生成,在源码仓库里运行(整合包里没有这个工具):
npm run package -- --dry-run --list # 只检查:列出每个文件和大小,不写任何文件(精简包加 --lite)
npm run package -- --out <目录> # 完整包 Stronghold-Protocol-v<版本>.zip
npm run package:lite -- --out <目录> # 精简包 Stronghold-Protocol-v<版本>-lite.zip
npm run package -- --update --from <旧版本的完整包>[,<…>] --out <目录> # 更新包 Stronghold-Protocol-v<版本>-update.zip
完整包在打包机已经提取战场场景时带上 public/assets/local/scene/(--dry-run 摘要里的 battlefield scenes 一行)。还没有提取时完整包仍能打,棋盘用金属地块。要带上 11 个官方场景,先运行 python3 tools/local-extract/fetch_scenes.py,再用提取环境的 Python 运行 tools/local-extract/extract.py --only scene(或 node tools/setup.mjs --scenes),然后再 npm run package。精简包不带场景。.cache/official-scenes/ 在拒绝名单里,不会打进任何包。tools/local-extract/scenes.py 和 fetch_scenes.py 必须先提交:打包读的是 git ls-files,未提交的脚本不会进玩家包。
- 打进去的:
git ls-files里的server/、shared/、data/、public/(不含public/dev/)、packs/(随仓库提交的内容包;只在本机安装、没提交的不打进去)、启动脚本、玩家会运行的工具(setup、vendor、fetch-assets 与tools/assets/、doctor,以及 setup 调用的tools/local-extract/和crop-board-atlas.mjs)、服务器和 fetch-assets 读取的 4 张研究数据表(docs/research/的03-operators、05-enemies、05-maps、07-assets四个 JSON)、package.json/package-lock.json、许可证与说明(LICENSE、NOTICE.md、THIRD-PARTY-NOTICES.md、README.md、CHANGELOG.md)、docs/PLAYING.md和本文;然后在临时目录里生成packs/index.json(打进去的语言包和内容包的列表,供纯静态托管使用;服务器自己会实时列出,见 PACKS.md),再npm ci --omit=dev装上运行依赖和public/vendor。完整包再加上data/assets.json列出的素材、public/fonts,以及本地提取的public/assets/local/和data/local-assets.json。磁盘上有、清单却没列出的文件不打进去(例如 0.2.0 移出自选的焰狐龙梓兰的旧素材)。日文干员语音(audio.voiceJp,public/assets/audio/voice/jp/的 2674 个文件,约 85 MB,zip 后约 76 MB)默认也打进完整包;tools/package.mjs里的开关FULL_ZIP_JP_VOICE改成false时,完整包(以及由它比较出的更新包)不带这些文件:玩家首次启动时 setup 会像精简包那样下载它们,下载完成前选「日本語」会播中文语音,更新包也不会删除玩家已有的日文语音。 - 不打进去的:
test/、维护用的工具(数据构建、golden、botbench、i18n、导入检查、本工具等)、scripts/make-windows-bundle.mjs(Windows 便携包,见 WINDOWS.md)、其他文档、研究笔记和docs/img/、handoff/、.github/、types/、lint / 编辑器 / Docker 配置。和 0.1.x 的整树打包(全部跟踪文件加上public/assets的全部内容)相比,0.2.0 的完整包少了约 640 个文件、解压后小约 26 MB,zip 小约 8 MB。 - 打包前的检查(
--dry-run也全部做一遍):拒绝名单(pv、review、.cache、.claude、.git、logs、.env、scripts/service.env.cmd、handoff、test等);每个打进去的模块的相对导入、玩家用的 npm 脚本(start / setup / doctor / launch / postinstall / vendor / assets)都指向包里的文件;完整包里data/assets.json和data/local-assets.json列出的文件都在(缺了先运行node tools/fetch-assets.mjs);没有只差大小写的两个路径;包里的文件(二进制素材也查)不含个人目录路径(/Users/…、C:\Users\…、/home/…)或本机的账户名(运行时从系统读取;SP_PACKAGE_SCAN_NAMES=a,b可以再加名字);打进去的已跟踪文件没有未提交的改动(重新生成的data/assets.json要先提交)。有任何问题都会列出原因、以非零状态结束,不写 zip;正式打包时还会核对临时目录里的文件和计划完全一致。 - 玩家包里的脚本和文档链接:临时目录里的
package.json只留下每条node <文件>都指向包内文件、并且至少指出一个文件的脚本(start、dev、setup、doctor、launch、postinstall、vendor、assets)。test、golden、数据构建、lint、本工具等不留,避免玩家运行npm test时在没有测试文件的情况下得到「0 个测试、成功退出」。README.md、CHANGELOG.md、NOTICE.md、THIRD-PARTY-NOTICES.md、docs/PLAYING.md和本文里,指向包内没有的文件的相对链接改成该版本 GitHub 标签上的同一路径(版本号去掉末尾的-dev后加v;图片用raw,其余用blob);../../releases/latest改为仓库的 Releases 地址。源码仓库里的这些文件仍是相对链接。 - MANIFEST.json:三种包的根目录都有(
npm ci之后写入):除素材(public/assets/、public/fonts/、data/assets.json、data/local-assets.json,归 setup 管)以外每个文件的大小和 sha256,同一版本的三种包内容相同。npm run doctor和更新包的启动检查(server/update.js)用它核对安装。 - 更新包(0.2.1 起,每个版本都发):
--from后面列出之前每个 0.2.x 版本的完整包(Releases 上的 zip,或它解压出来、没动过的文件夹;逗号分隔或写多个--from),例如发布 0.2.2 时--from Stronghold-Protocol-v0.2.0.zip,Stronghold-Protocol-v0.2.1.zip。工具照常构建完整包的临时目录(npm ci等),逐个文件(大小 + sha256)和每个旧版本比较:和任何一个旧版本不同、或旧版本没有的文件都打进去,所以一个更新包能覆盖在列出的每个版本上;旧版本有、新版本没有的文件记进UPDATE.json的removed(连同各旧版本里的 sha256,玩家那边只删内容一致的文件;只差大小写的同名文件和.env之类的本机文件名不删,摘要里会列出)。旧版本必须比当前版本旧、带素材(不能是精简包或更新包)、每个版本只给一次,--dry-run只读取并检查旧版本。更新包里还有新版本的MANIFEST.json和UPDATE.json(适用的旧版本、文件数、字节数、文件列表和removed),同样经过拒绝名单和个人信息检查;摘要列出和每个旧版本相比改动 / 新增 / 删除的文件数和更新包里各类文件的数量。发布时在 Releases 说明里写明更新包适用的版本(即--from列出的版本)。--keep-stage保留更新包目录和完整包的临时目录(<目录>/Stronghold-Protocol-v<版本>-update/里的Stronghold-Protocol/和.full/)。 - 需要:已下载素材的仓库(完整包)——打包前先联网运行一次
node tools/fetch-assets.mjs,补齐清单计划但本机还没有的素材(清单只列出磁盘上有的文件,打包工具看不出缺了哪些;data/assets.json有变化就先提交);能访问 npm 的网络(npm ci);zip(或 bsdtar 的tar,Windows 10 起自带);更新包还要之前各版本的完整包(工具自己读 zip,不需要unzip;Releases 上可以重新下载)。--out默认是系统临时目录下的stronghold-protocol-release,不能在仓库里面;--force覆盖已有的 zip,--keep-stage保留打包用的目录供检查。