import { createHash } from "node:crypto"; import { readFileSync, writeFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import vm from "node:vm"; const scriptDirectory = dirname(fileURLToPath(import.meta.url)); const projectRoot = resolve(scriptDirectory, ".."); const sourcePath = resolve(projectRoot, "src/utils/match.min.js"); const runtimePath = resolve(projectRoot, "src/utils/matchProtocol.js"); const generatedStartMarker = "// "; const generatedEndMarker = "// "; const supportedScalarKinds = new Set([ "int32", "int64", "float", "double", "bool", "string", "bytes", ]); const supportedMapKeyKinds = new Set(["int32", "int64", "bool", "string"]); // 新版描述文件未携带旧协议的 oneof 元数据,保留现有解码结果中的 payload 标识。 const compatibilityOneofs = { ServerMessage: { match_info: "payload", shoot_data: "payload", practice_info: "payload", }, }; function isRecord(value) { return value !== null && typeof value === "object" && !Array.isArray(value); } function extractRootCreateArgument(source) { const match = /(?:\$protobuf\.)?Root\.create\s*\(/.exec(source); if (!match) { throw new Error("match.min.js 中未找到 Root.create(...) 协议描述"); } const openIndex = source.indexOf("(", match.index); let depth = 0; let quote = ""; let escaped = false; let lineComment = false; let blockComment = false; for (let index = openIndex; index < source.length; index += 1) { const char = source[index]; const next = source[index + 1]; if (lineComment) { if (char === "\n") lineComment = false; continue; } if (blockComment) { if (char === "*" && next === "/") { blockComment = false; index += 1; } continue; } if (quote) { if (escaped) { escaped = false; } else if (char === "\\") { escaped = true; } else if (char === quote) { quote = ""; } continue; } if (char === '"' || char === "'" || char === "`") { quote = char; continue; } if (char === "/" && next === "/") { lineComment = true; index += 1; continue; } if (char === "/" && next === "*") { blockComment = true; index += 1; continue; } if (char === "(") { depth += 1; continue; } if (char === ")") { depth -= 1; if (depth === 0) { return source.slice(openIndex + 1, index).trim(); } } } throw new Error("match.min.js 中的 Root.create(...) 括号不完整"); } function stripQuotedText(source) { let output = ""; let quote = ""; let escaped = false; for (const char of source) { if (quote) { output += " "; if (escaped) { escaped = false; } else if (char === "\\") { escaped = true; } else if (char === quote) { quote = ""; } continue; } if (char === '"' || char === "'") { quote = char; output += " "; } else { output += char; } } return output; } function parseDescriptor(argumentSource) { if (!argumentSource.startsWith("{") || !argumentSource.endsWith("}")) { throw new Error("Root.create(...) 参数不是静态对象字面量"); } try { return JSON.parse(argumentSource); } catch { // 兼容旧版 pbjs 生成的未加引号对象键;只允许静态对象语法。 const syntaxOnly = stripQuotedText(argumentSource); if (/[();`=]/.test(syntaxOnly)) { throw new Error("旧版协议描述包含非静态表达式,已拒绝执行"); } const descriptor = vm.runInNewContext( `(${argumentSource})`, Object.create(null), { timeout: 1000 } ); return JSON.parse(JSON.stringify(descriptor)); } } function findProtocolNamespace(node, path = []) { if (!isRecord(node)) return null; const entries = isRecord(node.nested) ? node.nested : node; if (isRecord(entries.ServerMessage?.fields) && isRecord(entries.ClientMessage?.fields)) { return { entries, path }; } for (const [name, child] of Object.entries(entries)) { if (!isRecord(child) || isRecord(child.fields)) continue; const found = findProtocolNamespace(child, [...path, name]); if (found) return found; } return null; } function getEnumValues(definition) { if (!isRecord(definition)) return null; const candidate = isRecord(definition.values) ? definition.values : definition; const entries = Object.entries(candidate); if (entries.length === 0 || entries.some(([, value]) => !Number.isInteger(value))) { return null; } return Object.fromEntries(entries.sort((left, right) => left[1] - right[1])); } function normalizeTypeName(type) { return String(type || "") .replace(/^\./, "") .split(".") .pop(); } function getOneofByField(definition) { const result = new Map(); if (!isRecord(definition.oneofs)) return result; for (const [groupName, groupDefinition] of Object.entries(definition.oneofs)) { const fieldNames = Array.isArray(groupDefinition) ? groupDefinition : groupDefinition?.oneof; if (!Array.isArray(fieldNames)) continue; for (const fieldName of fieldNames) result.set(fieldName, groupName); } return result; } function buildSchema({ messageName, definition, messages, enumNames }) { const schema = {}; const usedIds = new Set(); const oneofByField = getOneofByField(definition); for (const [fieldKey, fieldDefinition] of Object.entries(definition.fields)) { const id = Number(fieldDefinition.id); if (!Number.isInteger(id) || id <= 0) { throw new Error(`${messageName}.${fieldKey} 的字段编号无效`); } if (usedIds.has(id)) { throw new Error(`${messageName} 存在重复字段编号 ${id}`); } usedIds.add(id); const rule = fieldDefinition.rule; if (rule && !["optional", "required", "repeated", "map"].includes(rule)) { throw new Error(`${messageName}.${fieldKey} 使用了不支持的规则 ${rule}`); } const fieldName = fieldDefinition.protoName || fieldKey; const typeName = normalizeTypeName(fieldDefinition.type); const keyTypeName = normalizeTypeName( fieldDefinition.keyType ?? fieldDefinition.keytype ); const isMap = rule === "map" || Boolean(keyTypeName); if (isMap) { if (!supportedMapKeyKinds.has(keyTypeName)) { throw new Error( `${messageName}.${fieldKey} 使用了不支持的 map key 类型 ${keyTypeName}` ); } const valueIsMessage = messages.has(typeName); const valueKind = enumNames.has(typeName) ? "int32" : typeName; if (!valueIsMessage && !supportedScalarKinds.has(valueKind)) { throw new Error( `${messageName}.${fieldKey} 使用了不支持的 map value 类型 ${typeName}` ); } schema[id] = valueIsMessage ? { name: fieldName, kind: "map", keyKind: keyTypeName, valueKind: "message", valueType: typeName, } : { name: fieldName, kind: "map", keyKind: keyTypeName, valueKind, }; continue; } const isMessage = messages.has(typeName); const kind = enumNames.has(typeName) ? "int32" : typeName; if (!isMessage && !supportedScalarKinds.has(kind)) { throw new Error(`${messageName}.${fieldKey} 使用了不支持的类型 ${typeName}`); } const repeated = rule === "repeated"; if (repeated && !isMessage && !["string", "bytes"].includes(kind)) { throw new Error( `${messageName}.${fieldKey} 是 packed scalar repeated,当前通用解码器尚不支持` ); } const field = isMessage ? { name: fieldName, kind: "message", type: typeName } : { name: fieldName, kind }; if (repeated) field.repeated = true; const oneof = fieldDefinition.oneof || oneofByField.get(fieldKey) || compatibilityOneofs[messageName]?.[fieldName]; if (oneof) field.oneof = oneof; schema[id] = field; } return schema; } function formatPropertyKey(key) { return /^(?:[A-Za-z_$][\w$]*|\d+)$/.test(key) ? key : JSON.stringify(key); } function formatJsValue(value, depth = 0) { if (!isRecord(value)) return JSON.stringify(value); const entries = Object.entries(value); if (entries.length === 0) return "{}"; const indent = " ".repeat(depth); const primitiveEntries = entries.every(([, child]) => !isRecord(child)); if (primitiveEntries && value.kind !== "map") { const singleLine = `{ ${entries .map(([key, child]) => `${formatPropertyKey(key)}: ${JSON.stringify(child)}`) .join(", ")} }`; if (indent.length + singleLine.length <= 100) return singleLine; } const childIndent = " ".repeat(depth + 1); const lines = entries.map( ([key, child]) => `${childIndent}${formatPropertyKey(key)}: ${formatJsValue(child, depth + 1)},` ); return `{\n${lines.join("\n")}\n${indent}}`; } function createGeneratedSource(source) { const descriptor = parseDescriptor(extractRootCreateArgument(source)); const namespace = findProtocolNamespace(descriptor); if (!namespace) { throw new Error("协议描述中未找到 ServerMessage 和 ClientMessage"); } const messages = new Map(); const enums = new Map(); for (const [name, definition] of Object.entries(namespace.entries)) { if (isRecord(definition?.fields)) { messages.set(name, definition); continue; } const values = getEnumValues(definition); if (values) enums.set(name, values); } const serverMessageType = enums.get("ServerMessageType"); const clientMessageType = enums.get("ClientMessageType"); if (!serverMessageType || !clientMessageType) { throw new Error("协议描述缺少 ServerMessageType 或 ClientMessageType"); } const enumNames = new Set(enums.keys()); const schemaByName = new Map(); for (const [messageName, definition] of messages) { schemaByName.set( messageName, buildSchema({ messageName, definition, messages, enumNames }) ); } const reachable = new Set(); function collectReachable(messageName) { if (reachable.has(messageName)) return; const schema = schemaByName.get(messageName); if (!schema) throw new Error(`找不到消息定义 ${messageName}`); reachable.add(messageName); for (const field of Object.values(schema)) { if (field.kind === "message") collectReachable(field.type); if (field.kind === "map" && field.valueKind === "message") { collectReachable(field.valueType); } } } collectReachable("ServerMessage"); const schemas = {}; for (const messageName of messages.keys()) { if (reachable.has(messageName)) schemas[messageName] = schemaByName.get(messageName); } const sourceHash = createHash("sha256").update(source).digest("hex").slice(0, 16); const namespaceName = namespace.path.join(".") || "root"; const fieldCount = Object.values(schemas).reduce( (total, schema) => total + Object.keys(schema).length, 0 ); const generatedBlock = [ generatedStartMarker, "// 此区块由 scripts/generate-match-schema.mjs 自动生成,请勿手动修改。", `// 来源:src/utils/match.min.js(sha256: ${sourceHash})`, `// 协议命名空间:${namespaceName};消息数:${Object.keys(schemas).length};字段数:${fieldCount}`, "", `export const ServerMessageType = ${formatJsValue(serverMessageType)};`, "", `export const ClientMessageType = ${formatJsValue(clientMessageType)};`, "", `const SCHEMAS = ${formatJsValue(schemas)};`, generatedEndMarker, ].join("\n"); return { generatedBlock, messageCount: Object.keys(schemas).length, fieldCount, }; } function main() { const args = process.argv.slice(2); const unknownArgs = args.filter((arg) => arg !== "--check"); if (unknownArgs.length > 0) { throw new Error(`未知参数:${unknownArgs.join(", ")}`); } const source = readFileSync(sourcePath, "utf8"); const { generatedBlock, messageCount, fieldCount } = createGeneratedSource(source); const runtimeSource = readFileSync(runtimePath, "utf8"); const startIndex = runtimeSource.indexOf(generatedStartMarker); const endMarkerIndex = runtimeSource.indexOf(generatedEndMarker); if (startIndex < 0 || endMarkerIndex < startIndex) { throw new Error("matchProtocol.js 缺少协议生成区块标记"); } const endIndex = endMarkerIndex + generatedEndMarker.length; const currentBlock = runtimeSource.slice(startIndex, endIndex); if (args.includes("--check")) { if (currentBlock !== generatedBlock) { throw new Error( "matchProtocol.js 的生成区块与 match.min.js 不同步,请运行 npm run proto:generate" ); } console.log(`[match-schema] 同步校验通过:${messageCount} 个消息,${fieldCount} 个字段`); return; } if (currentBlock === generatedBlock) { console.log(`[match-schema] 无需更新:${messageCount} 个消息,${fieldCount} 个字段`); return; } const nextRuntimeSource = runtimeSource.slice(0, startIndex) + generatedBlock + runtimeSource.slice(endIndex); writeFileSync(runtimePath, nextRuntimeSource, "utf8"); console.log(`[match-schema] 已生成:${messageCount} 个消息,${fieldCount} 个字段`); } try { main(); } catch (error) { console.error(`[match-schema] ${error.message}`); process.exitCode = 1; }