206 lines
7.4 KiB
Python
206 lines
7.4 KiB
Python
from __future__ import annotations
|
||
|
||
import argparse
|
||
import io
|
||
import sys
|
||
import zipfile
|
||
from datetime import datetime
|
||
import hashlib
|
||
import re
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
from flask import Flask, Response, redirect, render_template, request, url_for
|
||
|
||
from pz_config.parsers import parse_sandboxvars_lua, parse_server_ini
|
||
from pz_config.writers import write_sandboxvars_lua, write_server_ini
|
||
|
||
|
||
def _parse_args(argv: list[str]) -> argparse.Namespace:
|
||
parser = argparse.ArgumentParser(description="Project Zomboid 配置 WebUI 编辑器")
|
||
parser.add_argument("--ini", default="server.ini", help="server.ini 路径(默认:./server.ini)")
|
||
parser.add_argument(
|
||
"--lua",
|
||
default="server_SandboxVars.lua",
|
||
help="server_SandboxVars.lua 路径(默认:./server_SandboxVars.lua)",
|
||
)
|
||
parser.add_argument(
|
||
"--translations",
|
||
default=str(Path("i18n") / "sandboxvars_zh.json"),
|
||
help="SandboxVars 中文说明 JSON(默认:./i18n/sandboxvars_zh.json)",
|
||
)
|
||
parser.add_argument("--host", default="127.0.0.1", help="监听地址(默认:127.0.0.1)")
|
||
parser.add_argument("--port", type=int, default=5050, help="监听端口(默认:5050)")
|
||
parser.add_argument("--debug", action="store_true", help="Flask debug 模式")
|
||
return parser.parse_args(argv)
|
||
|
||
|
||
def _coerce_value(value_type: str, raw: str) -> bool | int | float | str:
|
||
if value_type == "bool":
|
||
return raw.lower() == "true"
|
||
if value_type == "int":
|
||
return int(raw)
|
||
if value_type == "float":
|
||
return float(raw)
|
||
return raw
|
||
|
||
|
||
def create_app(ini_path: str, lua_path: str, translations_path: str) -> Flask:
|
||
app = Flask(__name__)
|
||
|
||
ini_path = str(Path(ini_path))
|
||
lua_path = str(Path(lua_path))
|
||
translations_path = str(Path(translations_path))
|
||
|
||
def _stable_group_id(name: str) -> str:
|
||
ascii_slug = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-") or "group"
|
||
digest = hashlib.md5(name.encode("utf-8")).hexdigest()[:8]
|
||
return f"{ascii_slug}-{digest}"
|
||
|
||
long_text_keys = {
|
||
"ServerWelcomeMessage",
|
||
"PublicDescription",
|
||
"Mods",
|
||
"Map",
|
||
"SpawnItems",
|
||
"ClientCommandFilter",
|
||
"ClientActionLogs",
|
||
"BadWordReplacement",
|
||
"BadWordListFile",
|
||
"GoodWordListFile",
|
||
"LootItemRemovalList",
|
||
"WorldItemRemovalList",
|
||
"Map.MapAllKnown",
|
||
}
|
||
|
||
@app.get("/")
|
||
def index() -> str:
|
||
translations = translations_path if Path(translations_path).exists() else None
|
||
ini_cfg = parse_server_ini(ini_path)
|
||
lua_cfg = parse_sandboxvars_lua(lua_path, translations_json=translations)
|
||
|
||
# Group Lua settings by their top-level table.
|
||
lua_groups: dict[str, list[Any]] = {}
|
||
for s in lua_cfg.settings:
|
||
lua_groups.setdefault(s.group, []).append(s)
|
||
|
||
for group_settings in lua_groups.values():
|
||
group_settings.sort(key=lambda s: s.line_index)
|
||
|
||
lua_groups_payload: list[dict[str, Any]] = []
|
||
for group_name, items in lua_groups.items():
|
||
first_line = min(s.line_index for s in items) if items else 0
|
||
lua_groups_payload.append(
|
||
{
|
||
"name": group_name,
|
||
"id": _stable_group_id(group_name),
|
||
"settings": items,
|
||
"count": len(items),
|
||
"order": first_line,
|
||
}
|
||
)
|
||
lua_groups_payload.sort(key=lambda g: g["order"])
|
||
|
||
lua_setting_count = sum(len(items) for items in lua_groups.values())
|
||
return render_template(
|
||
"index.html",
|
||
ini_settings=ini_cfg.settings,
|
||
lua_groups=lua_groups_payload,
|
||
lua_setting_count=lua_setting_count,
|
||
ini_path=ini_path,
|
||
lua_path=lua_path,
|
||
has_translations=Path(translations_path).exists(),
|
||
translations_path=translations_path,
|
||
long_text_keys=long_text_keys,
|
||
saved=request.args.get("saved") == "1",
|
||
)
|
||
|
||
def _read_updates() -> tuple[dict[str, object], dict[str, object], list[str]]:
|
||
translations = translations_path if Path(translations_path).exists() else None
|
||
ini_cfg = parse_server_ini(ini_path)
|
||
lua_cfg = parse_sandboxvars_lua(lua_path, translations_json=translations)
|
||
|
||
ini_updates: dict[str, object] = {}
|
||
lua_updates: dict[str, object] = {}
|
||
errors: list[str] = []
|
||
|
||
for s in ini_cfg.settings:
|
||
field = f"ini__{s.path}"
|
||
if s.value_type == "bool":
|
||
ini_updates[s.path] = field in request.form
|
||
continue
|
||
raw = request.form.get(field, "")
|
||
try:
|
||
ini_updates[s.path] = _coerce_value(s.value_type, raw)
|
||
except Exception:
|
||
errors.append(f"server.ini: {s.path} 值非法:{raw!r}")
|
||
|
||
for s in lua_cfg.settings:
|
||
field = f"lua__{s.path}"
|
||
if s.value_type == "bool":
|
||
lua_updates[s.path] = field in request.form
|
||
continue
|
||
raw = request.form.get(field, "")
|
||
try:
|
||
lua_updates[s.path] = _coerce_value(s.value_type, raw)
|
||
except Exception:
|
||
errors.append(f"server_SandboxVars.lua: {s.path} 值非法:{raw!r}")
|
||
|
||
return ini_updates, lua_updates, errors
|
||
|
||
@app.post("/download.zip")
|
||
def download_zip() -> Response:
|
||
translations = translations_path if Path(translations_path).exists() else None
|
||
ini_cfg = parse_server_ini(ini_path)
|
||
lua_cfg = parse_sandboxvars_lua(lua_path, translations_json=translations)
|
||
|
||
ini_updates, lua_updates, errors = _read_updates()
|
||
if errors:
|
||
return Response("\n".join(errors), status=400, mimetype="text/plain; charset=utf-8")
|
||
|
||
ini_out = write_server_ini(ini_cfg, ini_updates)
|
||
lua_out = write_sandboxvars_lua(lua_cfg, lua_updates)
|
||
|
||
buf = io.BytesIO()
|
||
with zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
||
zf.writestr(Path(ini_path).name, ini_out)
|
||
zf.writestr(Path(lua_path).name, lua_out)
|
||
buf.seek(0)
|
||
|
||
filename = f"pz-config-{datetime.now().strftime('%Y%m%d-%H%M%S')}.zip"
|
||
return Response(
|
||
buf.getvalue(),
|
||
mimetype="application/zip",
|
||
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
||
)
|
||
|
||
@app.post("/save")
|
||
def save_to_disk() -> Response:
|
||
translations = translations_path if Path(translations_path).exists() else None
|
||
ini_cfg = parse_server_ini(ini_path)
|
||
lua_cfg = parse_sandboxvars_lua(lua_path, translations_json=translations)
|
||
|
||
ini_updates, lua_updates, errors = _read_updates()
|
||
if errors:
|
||
return Response("\n".join(errors), status=400, mimetype="text/plain; charset=utf-8")
|
||
|
||
ini_out = write_server_ini(ini_cfg, ini_updates)
|
||
lua_out = write_sandboxvars_lua(lua_cfg, lua_updates)
|
||
|
||
Path(ini_path).write_text(ini_out, encoding="utf-8", newline="")
|
||
Path(lua_path).write_text(lua_out, encoding="utf-8", newline="")
|
||
return redirect(url_for("index", saved="1"))
|
||
|
||
return app
|
||
|
||
|
||
def main(argv: list[str]) -> int:
|
||
args = _parse_args(argv)
|
||
app = create_app(args.ini, args.lua, args.translations)
|
||
app.run(host=args.host, port=args.port, debug=args.debug)
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main(sys.argv[1:]))
|