工具链(3DTank/tools/):extract(模块提取)、mine_evidence/apply_names(命名证据与别名回填)、extract_codec_fields(字段表 + 全库槽位类型 + PrimClassWire/ResourceClasses)、mine_downlink(下行字段表)、extract_space_init/rebase_space_init/audit_space_init(空间模板与记录切分审计)、extract_space_session(把真实会话切成按上行触发的回放模板)、pair_flow(请求→应答配对)、probe_containers、ingest/decrypt_frames(抓包入库与解密)、serve/fetch_assets(本地站点镜像)、import_db(抓包对象仓库导入 MySQL)。 服务器(3DTank/tools/server/):link/entry/codec/container/dispatch/world/space/semantic(协议层与 lt/pt/zt 三个容器布局)、session(会话回放,顺序推进)、db + schema.sql(MySQL 连接层与对象仓库表结构)、store/responder(查库组帧与定向应答;接管范围默认只开 ba.lcr)。 同时修 pair_flow 的时间轴偏移:timeline_c2s 与 timeline_s2c 的 0 点相差 144~4453ms,改前配对整体偏约 2 秒(实测 space-100073070 偏 2084ms),改后 22.4% 的触发者被纠正。 src/、src_named/、packets/、3DTank/local/(含数据库口令)不进库,可用上述工具重新生成。
126 lines
4.4 KiB
JavaScript
126 lines
4.4 KiB
JavaScript
// db.js - MySQL 连接层(mysql2 连接池)+ schema 装载
|
||
//
|
||
// 配置来源优先级(高 → 低):
|
||
// 1. 调用方显式传入的配置对象
|
||
// 2. 环境变量 TANK_DB_HOST / TANK_DB_USER / TANK_DB_PASSWORD / TANK_DB_PORT / TANK_DB_NAME
|
||
// 3. local/db.json(未跟踪目录,凭据不进 git)
|
||
// 4. 内置默认(127.0.0.1:3306 / root / 库名 tanki)
|
||
//
|
||
// 关键取数口径:协议里的 id 是 i64,且**确实存在负数**(-2147481788 一族合成对象 id),
|
||
// 所以这里必须 supportBigNumbers + bigNumberStrings —— 大整数一律以**十进制字符串**返回,
|
||
// 与 tools/ 里既有代码「id 全部当字符串比较」的口径一致(改用 Number 会在大 id 上静默丢精度)。
|
||
'use strict';
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
const PROJECT = path.resolve(__dirname, '..', '..');
|
||
const CONFIG_PATH = path.join(PROJECT, 'local', 'db.json');
|
||
const SCHEMA_PATH = path.join(__dirname, 'schema.sql');
|
||
|
||
const DEFAULTS = {
|
||
Host: '127.0.0.1',
|
||
User: 'root',
|
||
Password: '',
|
||
Port: 3306,
|
||
Database: 'tanki',
|
||
};
|
||
|
||
function ReadConfigFile(File) {
|
||
const P = File || CONFIG_PATH;
|
||
if (!fs.existsSync(P)) return {};
|
||
try {
|
||
return JSON.parse(fs.readFileSync(P, 'utf8'));
|
||
} catch (Err) {
|
||
throw new Error('读不到数据库配置 ' + P + ':' + Err.message);
|
||
}
|
||
}
|
||
|
||
// 合出最终配置;缺项一律落到 DEFAULTS,不做隐式猜测
|
||
function LoadConfig(Explicit) {
|
||
const FromFile = ReadConfigFile(Explicit && Explicit.ConfigFile);
|
||
const Env = {
|
||
Host: process.env.TANK_DB_HOST,
|
||
User: process.env.TANK_DB_USER,
|
||
Password: process.env.TANK_DB_PASSWORD,
|
||
Port: process.env.TANK_DB_PORT ? Number(process.env.TANK_DB_PORT) : undefined,
|
||
Database: process.env.TANK_DB_NAME,
|
||
};
|
||
const Pick = (Key) => {
|
||
const Sources = [Explicit && Explicit[Key], Env[Key], FromFile[Key], DEFAULTS[Key]];
|
||
for (const V of Sources) if (V !== undefined && V !== null && V !== '') return V;
|
||
return DEFAULTS[Key];
|
||
};
|
||
return {
|
||
Host: String(Pick('Host')),
|
||
User: String(Pick('User')),
|
||
Password: String(Pick('Password')),
|
||
Port: Number(Pick('Port')) || DEFAULTS.Port,
|
||
Database: String(Pick('Database')),
|
||
};
|
||
}
|
||
|
||
// 建连接池。mysql2 是可选依赖:没装时给出可操作的报错,而不是一句 module not found
|
||
function Connect(Config) {
|
||
let mysql;
|
||
try {
|
||
mysql = require('mysql2/promise');
|
||
} catch (Err) {
|
||
throw new Error('没装 mysql2(在 tools/ 下执行 npm install mysql2)');
|
||
}
|
||
const C = Config || LoadConfig();
|
||
return mysql.createPool({
|
||
host: C.Host,
|
||
user: C.User,
|
||
password: C.Password,
|
||
port: C.Port,
|
||
database: C.Database,
|
||
waitForConnections: true,
|
||
connectionLimit: 8,
|
||
charset: 'utf8mb4',
|
||
supportBigNumbers: true,
|
||
bigNumberStrings: true,
|
||
multipleStatements: true, // EnsureSchema 要整份 schema.sql 一次执行
|
||
});
|
||
}
|
||
|
||
// 把 schema.sql 跑一遍(幂等:全部 CREATE TABLE IF NOT EXISTS)
|
||
async function EnsureSchema(Pool, SchemaFile) {
|
||
const Sql = fs.readFileSync(SchemaFile || SCHEMA_PATH, 'utf8');
|
||
await Pool.query(Sql);
|
||
const [Rows] = await Pool.query(
|
||
'SELECT TABLE_NAME AS Name FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() ORDER BY TABLE_NAME',
|
||
);
|
||
return Rows.map((R) => R.Name);
|
||
}
|
||
|
||
// 取一行(没有则 null)
|
||
async function One(Pool, Sql, Params) {
|
||
const [Rows] = await Pool.query(Sql, Params || []);
|
||
return Rows.length ? Rows[0] : null;
|
||
}
|
||
|
||
// 取多行
|
||
async function Many(Pool, Sql, Params) {
|
||
const [Rows] = await Pool.query(Sql, Params || []);
|
||
return Rows;
|
||
}
|
||
|
||
// 批量插入(Values 是数组的数组),自动按列切片拼多条 VALUES
|
||
async function InsertMany(Pool, Table, Columns, Values, ChunkSize) {
|
||
if (!Values.length) return 0;
|
||
const Size = ChunkSize || 500;
|
||
const Cols = Columns.map((C) => '`' + C + '`').join(', ');
|
||
let Done = 0;
|
||
for (let I = 0; I < Values.length; I += Size) {
|
||
const Chunk = Values.slice(I, I + Size);
|
||
const Placeholders = Chunk.map(() => '(' + Columns.map(() => '?').join(', ') + ')').join(', ');
|
||
const Flat = [];
|
||
for (const Row of Chunk) for (const V of Row) Flat.push(V);
|
||
await Pool.query('INSERT INTO `' + Table + '` (' + Cols + ') VALUES ' + Placeholders, Flat);
|
||
Done += Chunk.length;
|
||
}
|
||
return Done;
|
||
}
|
||
|
||
module.exports = { LoadConfig, Connect, EnsureSchema, One, Many, InsertMany, CONFIG_PATH, SCHEMA_PATH, DEFAULTS };
|