refactor(all): reorganize packages, establish player ownership and isolate accounts

This commit is contained in:
2026-10-07 22:42:16 +08:00
parent 8e8ddeddb6
commit 06a83ada28
850 changed files with 42335 additions and 79891 deletions
+26 -15
View File
@@ -8,10 +8,10 @@ by an explicitly configured local bd2server.
Example:
python tools/python/dev_mail_grant.py serve `
--game-data E:\\bd2\\dl\\GameData --game-data-version 20260923193640 `
--output data\\dev\\mail-grants-spool.json
--output data\\dev\\mail-grants-spool.json --account-id OWNER_ACCOUNT_ID
python tools/python/dev_mail_grant.py grant `
--output data\\dev\\currency-grants.json --identity test-grant-1 `
--output data\\dev\\currency-grants.json --account-id OWNER_ACCOUNT_ID --identity test-grant-1 `
--attachment 4:0:10000 --attachment 3:0:100
"""
@@ -486,8 +486,11 @@ def attachment(value: str) -> dict[str, int]:
def _validate_grant(value: Any) -> dict[str, Any]:
if not isinstance(value, dict) or set(value) != {"identity", "title", "body", "sent_at", "rewards"}:
raise ValueError("发放记录必须只包含 identity、title、body、sent_at、rewards")
if not isinstance(value, dict) or set(value) != {"account_id", "identity", "title", "body", "sent_at", "rewards"}:
raise ValueError("发放记录必须只包含 account_id、identity、title、body、sent_at、rewards")
account_id = value["account_id"]
if not isinstance(account_id,str) or not account_id.strip() or account_id != account_id.strip() or len(account_id)>500:
raise ValueError("account_id 必须明确指定一个有效收件账号")
identity, title, body = value["identity"], value["title"], value["body"]
if not isinstance(identity, str) or not identity.strip() or len(identity) > 500:
raise ValueError("identity 不能为空且不超过 500 字符")
@@ -504,22 +507,23 @@ def _validate_grant(value: Any) -> dict[str, Any]:
def load_grants(path: Path) -> dict[str, Any]:
if not path.exists():
return {"version": 1, "grants": []}
return {"version": 2, "grants": []}
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
raise ValueError(f"无法读取邮件发放文件 {path}: {exc}") from exc
if not isinstance(value, dict) or set(value) != {"version", "grants"} or type(value["version"]) is not int or value["version"] != 1 or not isinstance(value["grants"], list):
raise ValueError("邮件发放文件必须只包含 version=1 和 grants 数组")
if not isinstance(value, dict) or set(value) != {"version", "grants"} or type(value["version"]) is not int or value["version"] != 2 or not isinstance(value["grants"], list):
raise ValueError("邮件发放文件必须只包含 version=2 和 grants 数组")
grants = [_validate_grant(entry) for entry in value["grants"]]
identities = [entry["identity"] for entry in grants]
identities = [(entry["account_id"],entry["identity"]) for entry in grants]
if len(set(identities)) != len(identities):
raise ValueError("邮件发放文件包含重复 identity")
return {"version": 1, "grants": grants}
return {"version": 2, "grants": grants}
def grant(args: argparse.Namespace) -> int:
entry = _validate_grant({
"account_id": args.account_id,
"identity": args.identity.strip() if args.identity is not None else str(uuid.uuid4()),
"title": args.title.strip(),
"body": args.body.strip(),
@@ -529,7 +533,7 @@ def grant(args: argparse.Namespace) -> int:
output = args.output.resolve()
with grant_file_lock(output):
value = load_grants(output)
existing = next((item for item in value["grants"] if item["identity"] == entry["identity"]), None)
existing = next((item for item in value["grants"] if item["account_id"] == entry["account_id"] and item["identity"] == entry["identity"]), None)
if existing is not None:
if any(existing[key] != entry[key] for key in ("title", "body", "rewards")):
raise ValueError(f"identity {entry['identity']!r} 已存在且内容不同")
@@ -543,13 +547,16 @@ def grant(args: argparse.Namespace) -> int:
class MailGrantStore:
def __init__(self, output: Path, items: list[dict[str, Any]]):
def __init__(self, output: Path, items: list[dict[str, Any]], account_id: str):
if not isinstance(account_id,str) or not account_id.strip() or account_id!=account_id.strip() or len(account_id)>500:
raise ValueError("account_id 必须明确指定一个有效收件账号")
self.account_id = account_id
self.output = output.resolve()
self.items = items
self.item_keys = {(item["element_type"], item["id"]) for item in items}
self.lock = threading.Lock()
if not self.output.exists():
atomic_json(self.output, {"version": 1, "grants": []})
atomic_json(self.output, {"version": 2, "grants": []})
def grant(self, payload: Any) -> dict[str, Any]:
with self.lock:
@@ -575,6 +582,7 @@ class MailGrantStore:
now = int(time.time() * 1000)
entry = _validate_grant({
"account_id": self.account_id,
"identity": str(uuid.uuid4()),
"title": title,
"body": body,
@@ -706,13 +714,14 @@ class Handler(BaseHTTPRequestHandler):
def serve(args: argparse.Namespace) -> int:
items = load_items(args.game_data, args.game_data_version)
store = MailGrantStore(args.output, items)
store = MailGrantStore(args.output, items,args.account_id)
settings = DevelopmentSettingsStore(args.settings_output, load_inventory_limits(args.game_data, args.game_data_version))
Handler.store = store
Handler.settings = settings
server = ThreadingHTTPServer((args.listen_host, args.listen_port), Handler)
print(f"已读取 {len(items)} 个可由 ItemDBInfo 领取的 GameData 物品。")
print(f"浏览器打开:http://{args.listen_host}:{args.listen_port}/")
print(f"收件账号:{store.account_id}")
print(f"动态邮件队列:{store.output}")
print(f"开发工具配置:{settings.path}")
print("此服务不修改 data/state;bd2server 每次 /MailInfo 导入队列并唯一分配邮件 ID。")
@@ -731,13 +740,15 @@ def parser() -> argparse.ArgumentParser:
command = commands.add_parser("serve", help="start the loopback browser UI")
command.add_argument("--game-data", type=Path, required=True, help="GameData root")
command.add_argument("--game-data-version", required=True, help="validated GameData version")
command.add_argument("--output", type=Path, required=True, help="version=1 dynamic mail grant spool")
command.add_argument("--account-id", required=True, help="exact recipient account ID; no broadcast default")
command.add_argument("--output", type=Path, required=True, help="version=2 account-addressed dynamic mail grant spool")
command.add_argument("--settings-output", type=Path, default=Path("data/dev/dev-tools.json"), help="development settings JSON")
command.add_argument("--listen-host", default="127.0.0.1", help="loopback host (default: 127.0.0.1)")
command.add_argument("--listen-port", default=8765, type=int, help="loopback port (default: 8765)")
command.set_defaults(run=serve)
command = commands.add_parser("grant", help="append one durable currency or draw ticket mail grant (standard library only)")
command.add_argument("--output", type=Path, required=True, help="version=1 development mail grants JSON")
command.add_argument("--account-id", required=True, help="exact recipient account ID; no broadcast default")
command.add_argument("--output", type=Path, required=True, help="version=2 account-addressed mail grants JSON")
command.add_argument("--attachment", type=attachment, action="append", required=True, metavar="TYPE:ID:COUNT", help="supported currency or draw ticket reward; repeat to include multiple attachments in one mail")
command.add_argument("--identity", help="stable idempotency identity (default: a new UUID)")
command.add_argument("--title", default="开发测试物品", help="mail title (maximum 500 characters)")