update:优化个人训练图片
This commit is contained in:
@@ -0,0 +1,440 @@
|
||||
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 = "// <match-schema-generated>";
|
||||
const generatedEndMarker = "// </match-schema-generated>";
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user