feat:初版
This commit is contained in:
205
pz_webui.py
Normal file
205
pz_webui.py
Normal file
@@ -0,0 +1,205 @@
|
||||
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:]))
|
||||
Reference in New Issue
Block a user