Files
TankIC/TankiOffline/tools/GenerateProtocol.py
T
TermiNexus f3db51cf4b feat(protocol): 补齐容器命令与短命令别名机制
- Containers.kt 实现 lt/pt/zt/alias 四种布局,覆盖 ModelService 4 条容器命令
- 生成 ModelRegistry.ByModelId,供 pt 项按 modelId 展开
- 短命令:_setModelCommandAlias 解码后写入别名表,记录链可按别名继续解
- 下行注册补到 681 条;railgun-battle 400 帧复跑仍为 399/399 回编码一致
2026-10-21 00:13:12 +08:00

488 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# GenerateProtocol.py —— 从 3DTank/names 的协议表生成 Kotlin 数据类与 Codec
#
# 输入(全部由 3DTank/tools 的提取器产出,不要手改):
# names/codec_fields.json 655 个 codec 的字段(类型已解析成 bool/i32/opt<...>/list<...>/模型名)
# names/downlink_table.json 681 条下行方法(methodId + 有序字段)
# names/uplink_table.json 361 条上行方法(methodId + 负载字段)
# names/method_table.json methodId → modelId(用来给下行命令取语义类名)
#
# 输出(入库、可手改;重跑会覆盖):
# net/src/jsMain/kotlin/Net/Generated/Models/Module<id>.kt
# net/src/jsMain/kotlin/Net/Generated/Downlink/Module<id>.kt
# net/src/jsMain/kotlin/Net/Generated/Uplink/Module<id>.kt
# net/src/jsMain/kotlin/Net/Generated/Registry.kt
# net/src/jsMain/kotlin/Net/Generated/Metadata.kt
#
# 命名规则:全部 PascalCase(首字母大写,常见缩写大写),属性名只影响可读性,不影响线格式。
import json
import re
import shutil
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
NAMES = ROOT.parent / "3DTank" / "names"
OUT = ROOT / "net" / "src" / "jsMain" / "kotlin" / "Net" / "Generated"
# 这些 codec 的线形不能由字段表直译,改由 Net/Protocol 下手写(数据类仍由生成器产出)。
# module_85183::jq = TankState 的「字段表版」;真正上线的是 module_87072 注册的 T5(23 字节位打包)
OVERLAY_CODECS = {"module_85183::jq"}
OVERLAY_NAMES = {"module_85183::jq": "TankStateCodec"}
# ModelService 的 4 条容器命令:布局由 Net/Protocol/Containers.kt 手写,直接覆盖注册表
REGISTRY_OVERRIDES = {
"9038756763225879447": "LtCodec", # _unloadObjects
"7640916300855664666": "PtCodec", # _loadObjectsData
"3216143066888387731": "ZtCodec", # _loadDependencies
"8179162314198150518": "AliasCodec", # _setModelCommandAlias
}
PRIMS = {
"bool": ("Boolean", "R.ReadBool()", "W.WriteBool({})"),
"i8": ("Byte", "R.ReadByte()", "W.WriteByte({}.toInt())"),
"i16": ("Short", "R.ReadShort()", "W.WriteShort({}.toInt())"),
"i32": ("Int", "R.ReadInt()", "W.WriteInt({})"),
"i64": ("Long", "R.ReadLong()", "W.WriteLong({})"),
"f32": ("Float", "R.ReadFloat()", "W.WriteFloat({})"),
"f64": ("Double", "R.ReadDouble()", "W.WriteDouble({})"),
"string": ("String", "R.ReadString()", "W.WriteString({})"),
"bytes": ("ByteArray", "R.ReadBytes()", "W.WriteBytes({})"),
"array": ("ByteArray", "R.ReadBytes()", "W.WriteBytes({})"),
"vector3": ("Vector3", "R.ReadVector3()", "W.WriteVector3({})"),
}
ACRONYMS = {"id", "uid", "sid", "cid", "cc", "ccu", "url", "uri", "ip", "api", "gpu", "fps", "hud", "db", "hp", "xp"}
KOTLIN_KEYWORDS = {
"as", "break", "class", "continue", "do", "else", "false", "for", "fun", "if", "in",
"interface", "is", "null", "object", "package", "return", "super", "this", "throw",
"true", "try", "typealias", "typeof", "val", "var", "when", "while", "by", "catch",
"constructor", "delegate", "dynamic", "field", "file", "finally", "get", "import",
"init", "param", "property", "receiver", "set", "setparam", "where", "it",
}
def ToPascal(Name: str) -> str:
"""camelCase / snake_case / 缩写 → PascalCase(缩写大写)。"""
if not Name:
return "Field"
Parts = [P for P in re.split(r"[^0-9A-Za-z]+", Name) if P]
if not Parts:
return "Field"
Out = []
for Part in Parts:
Low = Part.lower()
if Low in ACRONYMS:
Out.append(Low.upper())
elif Part.isupper() and len(Part) > 1:
Out.append(Part)
else:
Out.append(Part[0].upper() + Part[1:])
Result = "".join(Out)
if Result and Result[0].isdigit():
Result = "F" + Result
return Result
def KotlinName(Name: str) -> str:
return f"`{Name}`" if Name in KOTLIN_KEYWORDS else Name
def Txt(Node) -> str:
Kind = Node[0]
if Kind == "model":
return "#" + Node[1]
if Kind in ("opt", "list", "enum"):
return f"{Kind}<{Txt(Node[1])}>"
if Kind == "map":
return f"map<{Txt(Node[1])},{Txt(Node[2])}>"
return Kind
def SplitTopLevel(Text: str):
Depth = 0
for I, C in enumerate(Text):
if C == "<":
Depth += 1
elif C == ">":
Depth -= 1
elif C == "," and Depth == 0:
return Text[:I].strip(), Text[I + 1:].strip()
raise ValueError(f"map 参数不合法:{Text!r}")
class Resolver:
"""把协议表里的类型串解析成类型节点;TypeCodec 是裸类名跨模块时的唯一可靠指针。"""
def __init__(self, CodecKeyToName):
self.CodecKeyToName = CodecKeyToName
self.NameSet = set(CodecKeyToName.values())
def Parse(self, Text: str, TypeCodec: str = ""):
S = (Text or "").strip()
if S.startswith("opt<") and S.endswith(">"):
return ("opt", self.Parse(S[4:-1], TypeCodec))
if S.startswith("list<") and S.endswith(">"):
return ("list", self.Parse(S[5:-1], TypeCodec))
if S.startswith("enum<") and S.endswith(">"):
return ("enum", self.Parse(S[5:-1], TypeCodec))
if S.startswith("map<") and S.endswith(">"):
K, V = SplitTopLevel(S[4:-1])
return ("map", self.Parse(K, TypeCodec), self.Parse(V, TypeCodec))
if S in PRIMS:
return (S,)
if TypeCodec and TypeCodec in self.CodecKeyToName:
return ("model", self.CodecKeyToName[TypeCodec])
if S in self.NameSet:
return ("model", S)
raise ValueError(f"未知类型 {S!r}(TypeCodec={TypeCodec!r})")
def KotlinType(Node) -> str:
Kind = Node[0]
if Kind == "opt":
return KotlinType(Node[1]) + "?"
if Kind == "list":
return "List<" + KotlinType(Node[1]) + ">"
if Kind == "map":
return f"Map<{KotlinType(Node[1])}, {KotlinType(Node[2])}>"
if Kind == "enum":
return "Int"
if Kind == "model":
return Node[1]
return PRIMS[Kind][0]
def DecodeExpr(Node) -> str:
Kind = Node[0]
if Kind == "opt":
return "Wire.ReadOpt(R, N) { " + DecodeExpr(Node[1]) + " }"
if Kind == "list":
return "Wire.ReadList(R) { " + DecodeExpr(Node[1]) + " }"
if Kind == "map":
return "Wire.ReadMap(R, { " + DecodeExpr(Node[1]) + " }, { " + DecodeExpr(Node[2]) + " })"
if Kind == "enum":
return "R.ReadInt()"
if Kind == "model":
return Node[1] + "Codec.Decode(R, N)"
return PRIMS[Kind][1]
def EncodeExpr(Node, Value: str) -> str:
Kind = Node[0]
if Kind == "opt":
return "Wire.WriteOpt(N, " + Value + ") { " + EncodeExpr(Node[1], "it") + " }"
if Kind == "list":
return "Wire.WriteList(W, " + Value + ") { " + EncodeExpr(Node[1], "it") + " }"
if Kind == "map":
return ("Wire.WriteMap(W, " + Value + ", { " + EncodeExpr(Node[1], "it") + " }, { " +
EncodeExpr(Node[2], "it") + " })")
if Kind == "enum":
return "W.WriteInt(" + Value + ")"
if Kind == "model":
return Node[1] + "Codec.Encode(W, N, " + Value + ")"
return PRIMS[Kind][2].replace("{}", Value)
def FieldList(Items):
Out = []
for I, F in enumerate(Items, 1):
Out.append({
"Order": F.get("Order") or I,
"Name": F.get("Name") or "",
"Type": F.get("Type") or "?",
"TypeCodec": F.get("TypeCodec") or "",
})
return Out
def UniqueFieldNames(Fields):
Used = {}
Out = []
for F in Fields:
Base = ToPascal(F["Name"]) if F["Name"] and F["Name"] not in ("?", "(匿名)") else f"Field{F['Order']}"
Base = KotlinName(Base)
if Base in Used:
Used[Base] += 1
Base = f"{Base}{Used[Base]}"
else:
Used[Base] = 1
Out.append(Base)
return Out
class ClassDef:
def __init__(self, Name, Fields, Module="", Comment="", CodecKey=""):
self.Name = Name
self.Fields = Fields
self.Module = Module
self.Comment = Comment
self.CodecKey = CodecKey
def Main():
CodecFields = json.loads((NAMES / "codec_fields.json").read_text("utf-8"))
Downlink = json.loads((NAMES / "downlink_table.json").read_text("utf-8"))
Uplink = json.loads((NAMES / "uplink_table.json").read_text("utf-8"))
MethodTable = json.loads((NAMES / "method_table.json").read_text("utf-8"))
Codecs = CodecFields.get("Codecs", {})
# ---------- 1. 给每个 codec 分配唯一类名 ----------
CodecKeyToName = {}
NameOwner = {}
ModelDefs = {}
for Key, C in Codecs.items():
Base = ToPascal(C.get("ModelName") or f"{C.get('ModuleId','0')}_{C.get('Codec','X')}")
Name = Base
if Name in NameOwner and NameOwner[Name] != Key:
Name = f"{Base}M{C.get('ModuleId','0')}{ToPascal(C.get('Codec','X'))}"
NameOwner[Name] = Key
CodecKeyToName[Key] = Name
ModelDefs[Key] = ClassDef(Name, FieldList(C.get("Fields") or []), C.get("Module", ""),
C.get("ModelName") or "codec " + Key, Key)
# ---------- 2. 解析模型字段(TypeCodec 已覆盖跨模块裸类名;解析不了的如实列出) ----------
Resolved = {} # codecKey → (ClassDef, Nodes)
Failed = {} # codecKey → 原因
R = Resolver(CodecKeyToName)
for Key, Def in ModelDefs.items():
try:
Nodes = [R.Parse(F["Type"], F["TypeCodec"]) for F in Def.Fields]
except ValueError as E:
Failed[Key] = str(E)
continue
Resolved[Key] = (Def, Nodes)
# 依赖级联:引用了「解析失败的模型」的模型也一并跳过,保证生成物一定能编译
def ModelRefs(Node):
Kind = Node[0]
if Kind == "model":
return {Node[1]}
if Kind in ("opt", "list", "enum"):
return ModelRefs(Node[1])
if Kind == "map":
return ModelRefs(Node[1]) | ModelRefs(Node[2])
return set()
while True:
FailedNames = {CodecKeyToName[K] for K in Failed}
Cascaded = []
for Key, (Def, Nodes) in Resolved.items():
Refs = set()
for N in Nodes:
Refs |= ModelRefs(N)
if Refs & FailedNames:
Cascaded.append((Key, "引用了解析失败的模型 " + ", ".join(sorted(Refs & FailedNames))))
if not Cascaded:
break
for Key, Why in Cascaded:
Failed[Key] = Why
Resolved.pop(Key, None)
Active = {K: V for K, V in CodecKeyToName.items() if K in Resolved}
# ---------- 3. 下行/上行命令 ----------
ModelIdToName = {}
for Key, C in Codecs.items():
if Key not in Resolved:
continue
for Mid in C.get("ModelIds") or []:
if C.get("ModelName"):
ModelIdToName[str(Mid)] = CodecKeyToName[Key]
MethodToModelId = {str(Row["MethodId"]): str(Row["ModelId"]) for Row in MethodTable.get("Rows", [])}
R = Resolver(Active)
SigToName = {}
for _, (Def, Nodes) in Resolved.items():
Sig = "|".join(Txt(N) for N in Nodes)
SigToName.setdefault(Sig, Def.Name)
NameUsed = set(CodecKeyToName.values())
def AllocName(Base, Sig, Suffix):
if Sig in SigToName:
return SigToName[Sig], False
Name = Base
if Name in NameUsed:
Name = f"{Base}{Suffix}"
NameUsed.add(Name)
SigToName[Sig] = Name
return Name, True
DownDefs = []
DownRegistry = [] # (MethodId, className)
for Cmd in Downlink.get("Commands", []):
Fields = FieldList(Cmd.get("Fields") or [])
Mid = str(Cmd["MethodId"])
try:
Nodes = [R.Parse(F["Type"], F["TypeCodec"]) for F in Fields]
except ValueError as E:
print(f"[跳过] 下行 {Mid} {Cmd.get('OwnerClass')}:{E}", file=sys.stderr)
continue
Sig = "|".join(Txt(N) for N in Nodes)
Base = ModelIdToName.get(MethodToModelId.get(Mid, ""), "")
if not Base:
Base = f"Down{Cmd.get('Module','').replace('module_','')}{ToPascal(Cmd.get('OwnerClass') or 'Cmd')}{Mid[-4:]}"
Name, IsNew = AllocName(Base, Sig, Mid[-4:])
if IsNew:
DownDefs.append(ClassDef(Name, Fields, Cmd.get("Module", ""), "下行 " + Mid))
DownRegistry.append((Mid, Name))
UpDefs = []
UpRegistry = []
for Cmd in Uplink.get("Commands", []):
Fields = FieldList(Cmd.get("Fields") or [])
Mid = str(Cmd["MethodId"])
try:
Nodes = [R.Parse(F["Type"], F["TypeCodec"]) for F in Fields]
except ValueError as E:
print(f"[跳过] 上行 {Mid} {Cmd.get('OwnerClass')}:{E}", file=sys.stderr)
continue
Sig = "|".join(Txt(N) for N in Nodes)
Base = f"Up{Cmd.get('Module','').replace('module_','')}{ToPascal(Cmd.get('OwnerClass') or 'Cmd')}"
Name, IsNew = AllocName(Base, Sig, Mid[-4:])
if IsNew:
UpDefs.append(ClassDef(Name, Fields, Cmd.get("Module", ""), "上行 " + Mid))
UpRegistry.append((Mid, Name))
# ---------- 4. 写文件 ----------
OUT.mkdir(parents=True, exist_ok=True)
for Sub in ("Models", "Downlink", "Uplink"):
Target = OUT / Sub
if Target.exists():
shutil.rmtree(Target)
Target.mkdir(parents=True, exist_ok=True)
Header = ("// 本文件由 TankiOffline/tools/GenerateProtocol.py 生成。\n"
"// 允许手改;重跑生成器会覆盖。类型口径见 3DTank/docs/KtRebuild_Plan.md。\n"
"package Net.Generated\n\n"
"import Core.ByteReader\nimport Core.ByteWriter\nimport Core.NullmapCursor\n"
"import Core.NullmapWriter\nimport Core.Vector3\n"
"import Net.Wire.ModelCodec\nimport Net.Wire.Wire\nimport Net.Wire.ErasedCodec\n"
"import Net.Protocol.TankStateCodec\nimport Net.Protocol.LtCodec\n"
"import Net.Protocol.PtCodec\nimport Net.Protocol.ZtCodec\nimport Net.Protocol.AliasCodec\n\n")
def EmitClass(Def: ClassDef, Nodes):
Names = UniqueFieldNames(Def.Fields)
Lines = [f"// {Def.Comment}|{Def.Module}"]
if Nodes:
Lines.append(f"data class {Def.Name}(")
for N, P in zip(Nodes, Names):
Lines.append(f" val {P}: {KotlinType(N)},")
Lines.append(")")
else:
Lines.append(f"class {Def.Name}")
Lines.append("")
if Def.CodecKey in OVERLAY_CODECS:
Lines.append("// 该模型的 codec 由 Net/Protocol 下手写(线形不是字段直译),此处不生成。")
Lines.append("")
return Lines
Lines.append(f"object {Def.Name}Codec : ModelCodec<{Def.Name}> {{")
Lines.append(f" override fun Decode(R: ByteReader, N: NullmapCursor): {Def.Name} {{")
for N, P in zip(Nodes, Names):
Lines.append(f" val {P} = {DecodeExpr(N)}")
if Nodes:
Lines.append(f" return {Def.Name}({', '.join(Names)})")
else:
Lines.append(f" return {Def.Name}()")
Lines.append(" }")
Lines.append("")
Lines.append(f" override fun Encode(W: ByteWriter, N: NullmapWriter, V: {Def.Name}) {{")
for N, P in zip(Nodes, Names):
Lines.append(f" {EncodeExpr(N, 'V.' + P)}")
Lines.append(" }")
Lines.append("}")
Lines.append("")
return Lines
def GroupByModule(Items):
Out = {}
for Def, Nodes in Items:
Out.setdefault(Def.Module or "Misc", []).append((Def, Nodes))
return Out
for Group, Items in GroupByModule(Resolved.values()).items():
Path_ = OUT / "Models" / f"{ToPascal(Group.replace('module_', 'Module'))}.kt"
Path_.write_text(Header + "\n".join(sum((EmitClass(D, N) for D, N in Items), [])), "utf-8")
for Group, Items in GroupByModule([(D, None) for D in DownDefs]).items():
Path_ = OUT / "Downlink" / f"{ToPascal(Group.replace('module_', 'Module'))}.kt"
Path_.write_text(Header + "\n".join(sum((EmitClass(D, [R.Parse(F['Type'], F['TypeCodec']) for F in D.Fields])
for D, _ in Items), [])), "utf-8")
for Group, Items in GroupByModule([(D, None) for D in UpDefs]).items():
Path_ = OUT / "Uplink" / f"{ToPascal(Group.replace('module_', 'Module'))}.kt"
Path_.write_text(Header + "\n".join(sum((EmitClass(D, [R.Parse(F['Type'], F['TypeCodec']) for F in D.Fields])
for D, _ in Items), [])), "utf-8")
# ---------- 5. 注册表 ----------
Emitted = {D.Name for D, _ in Resolved.values()} | {D.Name for D in DownDefs} | {D.Name for D in UpDefs}
def Registry(Entries, Title):
Lines = [f"object {Title} {{", " val ByMethodId: Map<Long, ErasedCodec> = mapOf("]
Seen = set()
for Mid, Name in Entries:
if Name not in Emitted or Mid in Seen or Mid in REGISTRY_OVERRIDES:
continue
Seen.add(Mid)
Lines.append(f" {Mid}L to {Name}Codec,")
if Title == "DownlinkRegistry":
for Mid, Expr in REGISTRY_OVERRIDES.items():
Lines.append(f" {Mid}L to {Expr},")
Lines.append(" )")
Lines.append("}")
return Lines
def RegistryWithModelIds(ResolvedMap, NameMap):
Lines = ["object ModelRegistry {", " val ByModelId: Map<Long, ErasedCodec> = mapOf("]
Seen = set()
for Key, (Def, Nodes) in ResolvedMap.items():
C = Codecs[Key]
Expr = OVERLAY_NAMES.get(Key, NameMap[Key] + "Codec")
Ids = []
if C.get("ModelId"):
Ids.append(str(C["ModelId"]))
Ids += [str(M) for M in (C.get("ModelIds") or [])]
for Mid in Ids:
if Mid in Seen:
continue
Seen.add(Mid)
Lines.append(f" {Mid}L to {Expr},")
Lines.append(" )")
Lines.append("}")
return Lines
(OUT / "Registry.kt").write_text(
Header + "\n".join(Registry(DownRegistry, "DownlinkRegistry")) + "\n\n" +
"\n".join(Registry(UpRegistry, "UplinkRegistry")) + "\n\n" +
"\n".join(RegistryWithModelIds(Resolved, CodecKeyToName)) + "\n", "utf-8")
# ---------- 6. 元数据 ----------
def MetaEntry(Def: ClassDef, Nodes):
Mid = Def.Comment.split()[-1]
FS = ", ".join(f'ProtocolFieldMeta({F["Order"]}, "{F["Name"]}", "{Txt(N)}")'
for F, N in zip(Def.Fields, Nodes))
return f' {Mid}L to ProtocolCommandMeta({Mid}L, "{Def.Name}", "{Def.Module}", listOf({FS})),'
def NodesOf(Def):
return [R.Parse(F["Type"], F["TypeCodec"]) for F in Def.Fields]
Meta = ["package Net.Generated", "",
"data class ProtocolFieldMeta(val Order: Int, val Name: String, val WireType: String)", "",
"data class ProtocolCommandMeta(val MethodId: Long, val Name: String, val Module: String, val Fields: List<ProtocolFieldMeta>)", "",
"object ProtocolMetadata {", " val Downlink: Map<Long, ProtocolCommandMeta> = mapOf("]
Meta += [MetaEntry(D, NodesOf(D)) for D in DownDefs]
Meta += [" )", " val Uplink: Map<Long, ProtocolCommandMeta> = mapOf("]
Meta += [MetaEntry(D, NodesOf(D)) for D in UpDefs]
Meta += [" )", "}"]
(OUT / "Metadata.kt").write_text("\n".join(Meta) + "\n", "utf-8")
print(f"模型类:{len(Resolved)} / {len(Codecs)}(跳过 {len(Failed)})")
print(f"下行命令:{len(DownDefs)} 个新类,注册 {len({M for M, N in DownRegistry if N in Emitted})} 条")
print(f"上行命令:{len(UpDefs)} 个新类,注册 {len({M for M, N in UpRegistry if N in Emitted})} 条")
if Failed:
print("跳过的 codec:")
for Key, Why in Failed.items():
print(f" · {Key}: {Why}")
if __name__ == "__main__":
Main()