新增 scripts/gen-market.py 生成市场 JSON,替代手写。模板数可传参调整。 端口排布与容量上限: 四组端口各自从常规基址每实例 +10。最先相撞的是 RCON(21114 起)追上 查询端口基址(27165),故实例数硬上限为 606,超出会让不同实例的不同 服务抢同一端口。脚本对超限直接报错并说明原因。 已校验:24 个模板占用 144 个端口(24×6)零冲突;RCON 端口与 rconPort 字段一致;pingConfig 指向查询端口而非模板默认的 Minecraft 25565; networkMode 均为 host;memory 均非 0;镜像均非 Docker Hub;编码均 utf8。 边界用例 607 被拒、606 可生成且末个 RCON 恰好不越界。 模板按每 8 个分组,避免市场列表过长难找。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
156 lines
6.5 KiB
Python
Executable File
156 lines
6.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
生成 MCSManager 自定义市场 JSON —— 为每个 Squad 实例预排好端口。
|
|
|
|
为什么需要自建市场:官方市场自带的 Squad Docker 模板有三个问题,
|
|
在「无法访问 Docker Hub + 多实例」场景下必然失败。详见 PANEL.md §2。
|
|
|
|
用法:
|
|
./gen-market.py # 默认 24 个模板
|
|
./gen-market.py 48 # 生成 48 个
|
|
./gen-market.py 24 - # 输出到 stdout 而非写文件
|
|
|
|
SQUAD_IMAGE=<私有仓库>/steam-game-runtime:latest ./gen-market.py
|
|
|
|
端口排布(每实例 +10):
|
|
游戏 7787 + 10(N-1) 并占用 +1
|
|
查询 27165 + 10(N-1) 并占用 +1
|
|
Beacon 15000 + 10(N-1)
|
|
RCON 21114 + 10(N-1)
|
|
|
|
间隔取 10 是因为游戏端口和查询端口各占 N 和 N+1 两个,只差 1 会重叠。
|
|
|
|
实例数上限 606 —— 四组端口各自从常规基址递增,最先相撞的是
|
|
RCON(21114 起)追上查询基址(27165):(27165-21114)/10 + 1 = 606。
|
|
超过这个数就必须重新规划基址,否则不同实例的不同服务会抢同一端口。
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
BASE = {"port": 7787, "query": 27165, "beacon": 15000, "rcon": 21114}
|
|
STEP = 10
|
|
# 相邻两组基址的最小间距决定上限:RCON(21114) 追上 query(27165)
|
|
MAX_INSTANCES = (BASE["query"] - BASE["rcon"]) // STEP + 1 # = 606
|
|
|
|
IMAGE = os.environ.get(
|
|
"SQUAD_IMAGE", "squaddocker.azurecr.cn/steam-game-runtime:latest")
|
|
DATA_ROOT = os.environ.get("SQUAD_DATA_ROOT", "/srv/squad")
|
|
MEM_MB = int(os.environ.get("SQUAD_MEM_MB", "6144"))
|
|
MAXPLAYERS = int(os.environ.get("SQUAD_MAXPLAYERS", "50"))
|
|
|
|
UPDATE_CMD = ('/home/steam/steamcmd/steamcmd.sh -net_ipv4 '
|
|
'+force_install_dir "/data" +login anonymous '
|
|
'"+app_update 403240 validate" +quit')
|
|
|
|
|
|
def ports_for(n):
|
|
off = (n - 1) * STEP
|
|
return {k: v + off for k, v in BASE.items()}
|
|
|
|
|
|
def package(n):
|
|
p = ports_for(n)
|
|
cwd = f"{DATA_ROOT}/inst{n}"
|
|
return {
|
|
"title": f"Squad 实例 {n:02d}",
|
|
"language": "zh_cn",
|
|
"platform": "Linux",
|
|
"description": (
|
|
f"游戏 {p['port']}/{p['port']+1} · 查询 {p['query']}/{p['query']+1} · "
|
|
f"Beacon {p['beacon']} · RCON {p['rcon']} · 目录 {cwd}。"
|
|
f"端口已预排,可与其他实例并存。镜像走私有仓库(本机访问不了 Docker Hub);"
|
|
f"网络 host(Squad 硬要求,bridge 下游戏内浏览器搜不到服务器)。"
|
|
f"创建后需 ① mkdir -p {cwd} && chown 1000:1000 "
|
|
f"② 首启下载约 14GB ③ 下载完在 ServerConfig/Rcon.cfg 设 Password,"
|
|
f"否则服务端不启动 RCON。"
|
|
),
|
|
"image": "http://mcsmanager.com/package-images/squad.jpg",
|
|
"gameType": "Squad",
|
|
# 分组:每 8 个一档,免得市场列表里一长条没法找
|
|
"category": f"实例 {((n - 1) // 8) * 8 + 1:02d}-{((n - 1) // 8 + 1) * 8:02d}",
|
|
"runtime": "Linux",
|
|
"hardware": "每实例 1 物理核 + 6GB 内存 + 20GB 磁盘",
|
|
"size": "约 14GB(游戏本体运行时下载)",
|
|
"targetLink": "",
|
|
"author": "自建 — 修正 MCSManager 官方 Squad 模板",
|
|
"tags": ["Docker", "host网络", f"实例{n:02d}"],
|
|
"setupInfo": {
|
|
# 官方模板这里是裸的 ./SquadGameServer.sh,不带端口参数,
|
|
# 多开时第二个实例会抢第一个的端口
|
|
"startCommand": (
|
|
f"./SquadGameServer.sh Port={p['port']} QueryPort={p['query']} "
|
|
f"RCONPORT={p['rcon']} beaconport={p['beacon']} "
|
|
f"FIXEDMAXPLAYERS={MAXPLAYERS} FIXEDMAXTICKRATE=50"),
|
|
"stopCommand": "^C", # Squad 无 stop 命令,只能靠信号
|
|
"updateCommand": UPDATE_CMD,
|
|
# Linux 上必须 utf8;官方模板在中文 Windows 环境下用 gbk,会乱码
|
|
"ie": "utf8", "oe": "utf8", "fileCode": "utf8",
|
|
"type": "steam/universal",
|
|
"tag": ["Squad"],
|
|
"processType": "docker",
|
|
"runAs": "steam",
|
|
"crlf": 1,
|
|
"cwd": cwd,
|
|
"enableRcon": True,
|
|
"rconPort": p["rcon"],
|
|
"rconIp": "127.0.0.1",
|
|
# 官方模板默认 25565 是 Minecraft 的,Squad 要指向 Steam 查询端口
|
|
"pingConfig": {"ip": "127.0.0.1", "port": p["query"], "type": 1},
|
|
"eventTask": {"autoStart": False, "autoRestart": True, "ignore": False},
|
|
"terminalOption": {"haveColor": False, "pty": True},
|
|
"docker": {
|
|
"image": IMAGE,
|
|
"updateCommandImage": IMAGE,
|
|
"networkMode": "host", # 官方模板是 bridge,Squad 不工作
|
|
"ports": [], # host 网络下端口映射无意义
|
|
"memory": MEM_MB, # 0=无上限,小内存机会被 OOM 拖垮整机
|
|
"changeWorkdir": True,
|
|
"workingDir": "/data",
|
|
"env": [],
|
|
"extraVolumes": [],
|
|
"networkAliases": [],
|
|
"cpusetCpus": "",
|
|
"cpuUsage": 0,
|
|
"maxSpace": None,
|
|
"network": None,
|
|
"io": None,
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
def main():
|
|
count = int(sys.argv[1]) if len(sys.argv) > 1 else 24
|
|
if not 1 <= count <= MAX_INSTANCES:
|
|
sys.exit(f"实例数须在 1..{MAX_INSTANCES} 之间。\n"
|
|
f"上限来自端口排布:RCON({BASE['rcon']}) 每实例 +{STEP},"
|
|
f"到第 {MAX_INSTANCES+1} 个就会撞上查询端口基址 {BASE['query']}。")
|
|
|
|
market = {
|
|
"languages": [{"label": "中文", "value": "zh_cn", "path": ""}],
|
|
"packages": [package(n) for n in range(1, count + 1)],
|
|
}
|
|
out = json.dumps(market, ensure_ascii=False, indent=2) + "\n"
|
|
|
|
# 默认写入仓库根的 mcsm-market.json;传 - 则输出到 stdout
|
|
if "-" in sys.argv[1:]:
|
|
sys.stdout.write(out)
|
|
else:
|
|
import pathlib
|
|
p = pathlib.Path(__file__).resolve().parent.parent / "mcsm-market.json"
|
|
p.write_text(out)
|
|
last = ports_for(count)
|
|
print(f"已写入 {p}")
|
|
print(f" 模板数 {count}(上限 {MAX_INSTANCES}) {p.stat().st_size} 字节")
|
|
print(f" 实例 1 游戏 {BASE['port']} 查询 {BASE['query']} "
|
|
f"Beacon {BASE['beacon']} RCON {BASE['rcon']}")
|
|
print(f" 实例 {count} 游戏 {last['port']} 查询 {last['query']} "
|
|
f"Beacon {last['beacon']} RCON {last['rcon']}")
|
|
print(f" 镜像 {IMAGE}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|