feat: 市场模板改为脚本生成,预置 24 个实例
新增 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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
fe256c619f
commit
af87e416ce
+1467
-21
File diff suppressed because it is too large
Load Diff
Executable
+155
@@ -0,0 +1,155 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user