@yangrunchi/a_6 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,696 @@
1
+ // -- 作者: 杨润池
2
+ // -- 日期: 2025年10月25日
3
+ // -- 版本: 2.3
4
+ // -- 描述: 自动生成 TableMgr.lua 管理器(分别生成 csvcfg 和 game_config 的 require),只有内容变化时才提交,保留自定义代码区域
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const { execSync } = require('child_process');
9
+
10
+ // ==================== 配置 ====================
11
+
12
+ // 目录路径配置
13
+ const csvcfgDir = path.join(__dirname, '../../../dev/GUILayout/ssrgame/csvcfg'); // CSV配置目录
14
+ const mgrDir = path.join(__dirname, '../../../dev/GUILayout/ssrgame/mgr'); // 管理器目录
15
+ const gameConfigDir = path.join(__dirname, '../../../dev/scripts/game_config'); // 游戏配置目录
16
+ const tableMgrPath = path.join(mgrDir, 'TableMgr.lua'); // 输出文件路径
17
+
18
+ // 标记定义 - 用于识别文件中需要自动更新的区域
19
+ const MARKERS = {
20
+ csvcfgStart: '--------------------------------自动注册配置 csvcfg ↓↓↓ start---------------------------------------',
21
+ csvcfgEnd: '--------------------------------自动注册配置 csvcfg ↓↓↓ end---------------------------------------',
22
+ gameConfigStart: '--------------------------------自动注册配置 game_config ↓↓↓ start---------------------------------------',
23
+ gameConfigEnd: '--------------------------------自动注册配置 game_config ↓↓↓ end---------------------------------------',
24
+ customStart: '--------------------------------自定义代码 ↓↓↓ start--------------------------------------------',
25
+ customEnd: '--------------------------------自定义代码 ↓↓↓ end-----------------------------------------------'
26
+ };
27
+
28
+ // 打印路径调试信息
29
+ console.log('\n=== 路径调试信息 ===');
30
+ console.log('工作目录:', __dirname);
31
+ console.log('csvcfg 目录:', csvcfgDir, fs.existsSync(csvcfgDir) ? '✅' : '❌');
32
+ console.log('game_config 目录:', gameConfigDir, fs.existsSync(gameConfigDir) ? '✅' : '❌');
33
+ console.log('TableMgr.lua 路径:', tableMgrPath);
34
+
35
+ // 读取模板文件
36
+ const templatePath = path.join(__dirname, 'A6_template_tablemgr.txt');
37
+ let tableTemplate = '';
38
+
39
+ try {
40
+ if (fs.existsSync(templatePath)) {
41
+ tableTemplate = fs.readFileSync(templatePath, 'utf-8');
42
+ console.log('✅ 已加载模板文件: A6_template_tablemgr.txt');
43
+ } else {
44
+ console.error(`❌ 模板文件不存在: ${templatePath}`);
45
+ process.exit(1);
46
+ }
47
+ } catch (err) {
48
+ console.error(`❌ 读取模板失败: ${err.message}`);
49
+ process.exit(1);
50
+ }
51
+
52
+ // 读取项目配置(作者信息等)
53
+ let config = { author: "杨润池" };
54
+ try {
55
+ const configPath = path.join(__dirname, '../AutoCode/A1_996_config.json');
56
+ if (fs.existsSync(configPath)) {
57
+ config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
58
+ console.log('✅ 已加载配置, 作者:', config.author);
59
+ }
60
+ } catch (err) { }
61
+
62
+ // 需要排除的文件(不生成 require 语句)
63
+ const excludeFiles = new Set(['template', 'example', 'test']);
64
+ console.log('\n🚀 开始生成 TableMgr.lua...');
65
+
66
+ /**
67
+ * 获取当前日期时间字符串
68
+ * @returns {string} 格式化的日期时间,如 "2025-10-25 14:30:00"
69
+ */
70
+ function getDateTime() {
71
+ const d = new Date();
72
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`;
73
+ }
74
+
75
+ // ==================== SVN 辅助函数 ====================
76
+
77
+ /**
78
+ * 获取 SVN 用户名(当前登录的 SVN 用户)
79
+ */
80
+ function getSvnUsername() {
81
+ try {
82
+ const output = execSync(`svn info --show-item last-changed-author`, {
83
+ encoding: 'utf8',
84
+ stdio: ['pipe', 'pipe', 'ignore']
85
+ }).trim();
86
+ if (output) return output;
87
+ } catch (err) { }
88
+ return process.env.USERNAME || process.env.USER || config.author || 'unknown';
89
+ }
90
+
91
+ /**
92
+ * 执行 SVN 更新(版本2风格)
93
+ * @param {string} dirPath 目录路径
94
+ * @param {string} description 描述信息
95
+ * @returns {boolean} 是否成功
96
+ */
97
+ function svnUpdate(dirPath, description) {
98
+ if (!fs.existsSync(dirPath)) {
99
+ console.log(`⚠️ 目录不存在: ${path.basename(dirPath)}`);
100
+ return true;
101
+ }
102
+
103
+ try {
104
+ const dirName = path.basename(dirPath);
105
+ console.log(`🔄 ${description}: ${dirName}`);
106
+
107
+ // 静默执行更新
108
+ execSync(`svn update --quiet "${dirPath}"`, {
109
+ encoding: 'utf8',
110
+ stdio: 'ignore',
111
+ timeout: 60000
112
+ });
113
+
114
+ console.log(`✅ ${description}成功`);
115
+ return true;
116
+ } catch (err) {
117
+ // 忽略不在版本控制中的错误
118
+ if (err.message && err.message.includes('E155007')) {
119
+ console.log(`⚠️ ${path.basename(dirPath)} 不在版本控制中,跳过`);
120
+ return true;
121
+ }
122
+ console.log(`❌ ${description}失败: ${err.message}`);
123
+ return false;
124
+ }
125
+ }
126
+
127
+ // 提交 TableMgr.lua 到 SVN
128
+ function commitTableMgrToSvn() {
129
+ console.log('\n=== 提交 TableMgr.lua 到 SVN ===');
130
+
131
+ const mgrParentDir = path.dirname(tableMgrPath);
132
+
133
+ try {
134
+ // 1. 检查文件是否在 SVN 版本控制中
135
+ let isVersioned = false;
136
+ try {
137
+ execSync(`svn info "${tableMgrPath}"`, {
138
+ encoding: 'utf8',
139
+ stdio: ['pipe', 'pipe', 'ignore']
140
+ });
141
+ isVersioned = true;
142
+ console.log('✅ TableMgr.lua 已在 SVN 版本控制中');
143
+ } catch (err) {
144
+ isVersioned = false;
145
+ console.log('📝 TableMgr.lua 不在 SVN 版本控制中,需要添加');
146
+ }
147
+
148
+ // 2. 如果不在版本控制中,先添加
149
+ if (!isVersioned) {
150
+ try {
151
+ execSync(`svn add "${tableMgrPath}"`, {
152
+ encoding: 'utf8',
153
+ stdio: 'inherit'
154
+ });
155
+ console.log('✅ 文件已添加到 SVN');
156
+ } catch (addError) {
157
+ console.log(`❌ SVN 添加失败: ${addError.message}`);
158
+ return false;
159
+ }
160
+ }
161
+
162
+ // 3. 提交更改
163
+ try {
164
+ // 检查是否有待提交的更改
165
+ const statusOutput = execSync(`svn status "${tableMgrPath}"`, {
166
+ encoding: 'utf8',
167
+ cwd: mgrParentDir
168
+ });
169
+
170
+ const needCommit = statusOutput.trim().length > 0 &&
171
+ (statusOutput.trim().startsWith('M') ||
172
+ statusOutput.trim().startsWith('A'));
173
+
174
+ if (needCommit) {
175
+ const svnUser = getSvnUsername();
176
+ const isNewFile = !fs.existsSync(tableMgrPath) || statusOutput.trim().startsWith('A');
177
+ const commitMessage = isNewFile ?
178
+ `[Auto] ${svnUser} -首次创建 TableMgr.lua - ${getDateTime()}` :
179
+ `[Auto] ${svnUser} -配置文件变化,自动更新 TableMgr.lua - ${getDateTime()}`;
180
+
181
+ console.log(`📤 提交 TableMgr.lua 到 SVN...`);
182
+ console.log(` 提交信息: ${commitMessage}`);
183
+
184
+ execSync(`svn commit "${tableMgrPath}" -m "${commitMessage}"`, {
185
+ encoding: 'utf8',
186
+ stdio: 'inherit'
187
+ });
188
+ console.log('✅ TableMgr.lua 提交成功');
189
+ return true;
190
+ } else {
191
+ console.log('ℹ️ TableMgr.lua 没有需要提交的更改');
192
+ return true;
193
+ }
194
+ } catch (statusError) {
195
+ console.log(`⚠️ SVN 操作失败: ${statusError.message}`);
196
+ return false;
197
+ }
198
+ } catch (error) {
199
+ console.log(`❌ SVN 提交失败: ${error.message}`);
200
+ return false;
201
+ }
202
+ }
203
+
204
+ // ==================== 从 Lua 文件读取字段名 ====================
205
+
206
+ /**
207
+ * 分析表的值字符串,返回精确的类型字符串
208
+ * @param {string} valueStr 值的字符串表示
209
+ * @returns {string|null} 类型字符串,如 'table<number, number>' 或 null
210
+ */
211
+ function analyzeTableType(valueStr) {
212
+ if (!valueStr || typeof valueStr !== 'string') return null;
213
+ valueStr = valueStr.trim();
214
+ if (!valueStr.startsWith('{')) return null;
215
+
216
+ // 检测嵌套表结构: { [1] = { [1] = 200, [2] = 2000 } }
217
+ if (/^\{\s*\[\d+\]\s*=\s*\{\s*\[\d+\]\s*=\s*\d+/.test(valueStr)) {
218
+ const innerPairs = valueStr.match(/\[\d+\]\s*=\s*\d+/g);
219
+ if (innerPairs && innerPairs.length >= 2) return 'table<number, table<number, number>>';
220
+ return 'table<number, table>';
221
+ }
222
+ // 检测简单数组: { [1] = 100, [2] = 200 }
223
+ if (/^\{\s*\[\d+\]\s*=\s*\d+/.test(valueStr)) return 'table<number, number>';
224
+ // 检测字符串数组: { [1] = "a", [2] = "b" }
225
+ if (/^\{\s*\[\d+\]\s*=\s*["']/.test(valueStr)) return 'table<number, string>';
226
+ return 'table';
227
+ }
228
+
229
+ /**
230
+ * 从 Lua 配置文件中提取所有字段名
231
+ * @param {string} filePath Lua 文件路径
232
+ * @returns {Object} { fieldsSet: Set, fieldRawValues: Object }
233
+ */
234
+ function getFieldsFromLuaFile(filePath) {
235
+ const fieldsSet = new Set();
236
+ const fieldRawValues = {};
237
+
238
+ if (!fs.existsSync(filePath)) return { fieldsSet, fieldRawValues };
239
+
240
+ try {
241
+ const content = fs.readFileSync(filePath, 'utf-8');
242
+ let firstObject = null;
243
+ let inObject = false, braceDepth = 0, currentObject = '';
244
+
245
+ // 查找第一个配置对象(第一个 {...} 块)
246
+ for (let i = 0; i < content.length; i++) {
247
+ const ch = content[i];
248
+ if (ch === '{') {
249
+ if (!inObject) { inObject = true; currentObject = ''; }
250
+ braceDepth++;
251
+ currentObject += ch;
252
+ } else if (ch === '}') {
253
+ braceDepth--;
254
+ currentObject += ch;
255
+ if (braceDepth === 0 && inObject && !firstObject) {
256
+ firstObject = currentObject;
257
+ break;
258
+ }
259
+ } else if (inObject) {
260
+ currentObject += ch;
261
+ }
262
+ }
263
+
264
+ // 从第一个对象中提取字段名
265
+ if (firstObject) {
266
+ // 移除字符串内容,避免匹配到字符串内部的字段名
267
+ const cleaned = firstObject.replace(/["'][^"']*["']/g, '""');
268
+ const pattern = /(\w+)\s*=/g;
269
+ let match;
270
+ while ((match = pattern.exec(cleaned)) !== null) {
271
+ const key = match[1];
272
+ // 过滤掉数字和Lua关键字
273
+ if (!/^\d+$/.test(key) && !['FCOLOR', 'nil', 'true', 'false'].includes(key)) {
274
+ fieldsSet.add(key);
275
+ }
276
+ }
277
+
278
+ // 提取原始值用于类型推断
279
+ const rawValues = extractRawValuesFromObject(firstObject);
280
+ for (const [key, value] of Object.entries(rawValues)) {
281
+ fieldRawValues[key] = value;
282
+ }
283
+ }
284
+ } catch (err) { }
285
+ return { fieldsSet, fieldRawValues };
286
+ }
287
+
288
+ /**
289
+ * 从对象字符串中提取字段的原始值
290
+ * @param {string} objStr 对象字符串
291
+ * @returns {Object} 字段名到原始值的映射
292
+ */
293
+ function extractRawValuesFromObject(objStr) {
294
+ const rawValues = {};
295
+ let i = 0, len = objStr.length;
296
+
297
+ while (i < len) {
298
+ // 跳过空白
299
+ while (i < len && /\s/.test(objStr[i])) i++;
300
+ if (i >= len) break;
301
+
302
+ // 匹配字段名
303
+ const nameMatch = objStr.slice(i).match(/^(\w+)\s*=/);
304
+ if (!nameMatch) { i++; continue; }
305
+
306
+ const fieldName = nameMatch[1];
307
+ i += nameMatch[0].length;
308
+ while (i < len && /\s/.test(objStr[i])) i++;
309
+ if (i >= len) break;
310
+
311
+ // 提取值(支持嵌套表和字符串)
312
+ let value = '';
313
+ let braceDepth = 0, inString = false, stringChar = '';
314
+
315
+ while (i < len) {
316
+ const ch = objStr[i];
317
+ // 处理字符串开始/结束
318
+ if (!inString && (ch === '"' || ch === "'")) {
319
+ inString = true;
320
+ stringChar = ch;
321
+ value += ch;
322
+ i++;
323
+ continue;
324
+ }
325
+ // 字符串内
326
+ if (inString) {
327
+ value += ch;
328
+ if (ch === stringChar && objStr[i - 1] !== '\\') {
329
+ inString = false;
330
+ }
331
+ i++;
332
+ continue;
333
+ }
334
+ // 嵌套表
335
+ if (ch === '{') {
336
+ braceDepth++;
337
+ value += ch;
338
+ i++;
339
+ continue;
340
+ }
341
+ if (ch === '}') {
342
+ braceDepth--;
343
+ value += ch;
344
+ i++;
345
+ if (braceDepth === 0) {
346
+ while (i < len && (objStr[i] === ',' || /\s/.test(objStr[i]))) i++;
347
+ break;
348
+ }
349
+ continue;
350
+ }
351
+ // 顶层字段结束
352
+ if (braceDepth === 0 && (ch === ',' || ch === '\n')) {
353
+ i++;
354
+ break;
355
+ }
356
+ value += ch;
357
+ i++;
358
+ }
359
+
360
+ if (!['local', 'return', 'config'].includes(fieldName)) {
361
+ rawValues[fieldName] = value.trim();
362
+ }
363
+ }
364
+ return rawValues;
365
+ }
366
+
367
+ /**
368
+ * 推断字段类型(优先根据原始值,其次根据命名规范)
369
+ * @param {string} fieldName 字段名
370
+ * @param {string|null} rawValue 原始值
371
+ * @returns {string} 类型字符串
372
+ */
373
+ function inferFieldType(fieldName, rawValue = null) {
374
+
375
+ // 完全以值为准,不看字段名
376
+ if (rawValue && typeof rawValue === 'string') {
377
+ const trimmed = rawValue.trim();
378
+ if (/^-?\d+(?:\.\d+)?$/.test(trimmed)) return 'number';
379
+ if (trimmed.startsWith('{')) {
380
+ const tableType = analyzeTableType(trimmed);
381
+ return tableType || 'table';
382
+ }
383
+ return 'string';
384
+ }
385
+
386
+
387
+ const lowerName = fieldName.toLowerCase();
388
+ // 根据命名规范推断(fallback)
389
+ if (lowerName.includes('id') || lowerName.includes('lv') || lowerName === 'level') return 'number';
390
+ if (lowerName.includes('name') || lowerName.includes('desc') || lowerName.includes('title')) return 'string';
391
+ if (lowerName.includes('path') || lowerName.includes('tips')) return 'string';
392
+
393
+ return 'string';
394
+ }
395
+
396
+ /**
397
+ * 生成配置表的类型注解(用于IDE代码提示)
398
+ * @param {string} cfgName 配置表名称
399
+ * @param {Set} fieldsSet 字段集合
400
+ * @param {Object} fieldRawValues 字段原始值
401
+ * @returns {string} 类型注解字符串
402
+ */
403
+ function generateTypeAnnotation(cfgName, fieldsSet, fieldRawValues = {}) {
404
+ const indent = ' ';
405
+ if (!fieldsSet || fieldsSet.size === 0) return `${indent}---@class ${cfgName}`;
406
+
407
+ const lines = [`${indent}---@class ${cfgName}`];
408
+ for (const field of Array.from(fieldsSet).sort()) {
409
+ const rawValue = fieldRawValues[field];
410
+ const type = inferFieldType(field, rawValue);
411
+ if (type.includes('table<number, table<number, number>>')) {
412
+ lines.push(`${indent}---@field ${field} ${type} -- table类型: 索引→{属性ID, 属性值}`);
413
+ } else {
414
+ lines.push(`${indent}---@field ${field} ${type}`);
415
+ }
416
+ }
417
+ return lines.join('\n');
418
+ }
419
+
420
+ // ==================== 提取自定义代码 ====================
421
+
422
+ /**
423
+ * 从现有文件中提取自定义代码区域的内容
424
+ * @param {string} existingContent 现有文件内容
425
+ * @returns {string} 自定义代码内容(如果只有默认注释则返回空字符串)
426
+ */
427
+ function extractCustomCode(existingContent) {
428
+ if (!existingContent) return '';
429
+ const start = existingContent.indexOf(MARKERS.customStart);
430
+ const end = existingContent.indexOf(MARKERS.customEnd);
431
+ if (start !== -1 && end !== -1 && start < end) {
432
+ let code = existingContent.substring(start + MARKERS.customStart.length, end).trim();
433
+ // 如果只有默认注释,视为空
434
+ if (code && !['-- 在此处添加自定义代码,重新生成时会保留', ''].includes(code)) {
435
+ console.log(' 📝 检测到自定义代码,将保留');
436
+ return code;
437
+ }
438
+ }
439
+ return '';
440
+ }
441
+
442
+ // ==================== 生成 require 语句 ====================
443
+
444
+ /**
445
+ * 生成 csvcfg 目录下配置表的 require 语句
446
+ * @param {Array} files 文件列表
447
+ * @returns {string} require 语句字符串
448
+ */
449
+ function generateCsvcfgRequireLines(files) {
450
+ const lines = [];
451
+ for (const file of files) {
452
+ const name = file.name;
453
+ const { fieldsSet, fieldRawValues } = getFieldsFromLuaFile(file.fullPath);
454
+ const typeAnnotation = generateTypeAnnotation(name, fieldsSet, fieldRawValues);
455
+ if (typeAnnotation) lines.push(typeAnnotation);
456
+ lines.push(` ---@type table<number, ${name}>`);
457
+ if (name.includes('-')) {
458
+ lines.push(` ssrTableMgr["${name}"] = SL:Require("GUILayout/ssrgame/csvcfg/" .. "${name}")`);
459
+ } else {
460
+ lines.push(` self.${name} = SL:Require("GUILayout/ssrgame/csvcfg/" .. "${name}")`);
461
+ }
462
+ lines.push('');
463
+ }
464
+ return lines.join('\n');
465
+ }
466
+
467
+ /**
468
+ * 生成 game_config 目录下配置表的 require 语句
469
+ * @param {Array} files 文件列表
470
+ * @returns {string} require 语句字符串
471
+ */
472
+ function generateGameConfigRequireLines(files) {
473
+ const lines = [];
474
+ for (const file of files) {
475
+ const name = file.name;
476
+ const { fieldsSet, fieldRawValues } = getFieldsFromLuaFile(file.fullPath);
477
+ const typeAnnotation = generateTypeAnnotation(name, fieldsSet, fieldRawValues);
478
+ if (typeAnnotation) lines.push(typeAnnotation);
479
+ lines.push(` ---@type table<number, ${name}>`);
480
+ if (name.includes('-')) {
481
+ lines.push(` ssrTableMgr["${name}"] = SL:Require("scripts/game_config/" .. "${name}")`);
482
+ } else {
483
+ lines.push(` self.${name} = SL:Require("scripts/game_config/" .. "${name}")`);
484
+ }
485
+ lines.push('');
486
+ }
487
+ return lines.join('\n');
488
+ }
489
+
490
+ // ==================== 智能合并生成内容 ====================
491
+
492
+ /**
493
+ * 智能合并生成新内容(保留自定义代码区域)
494
+ * @param {string|null} existingContent 现有文件内容
495
+ * @param {Array} csvFiles csvcfg文件列表
496
+ * @param {Array} gameConfigFiles game_config文件列表
497
+ * @param {string} author 作者名
498
+ * @param {string} template 模板内容
499
+ * @returns {Object|null} 生成结果或null(无变化时)
500
+ */
501
+ function mergeAndGenerateContent(existingContent, csvFiles, gameConfigFiles, author, template) {
502
+ const dateTime = getDateTime();
503
+
504
+ // 生成新的 require 行
505
+ const newRequireLines1 = generateCsvcfgRequireLines(csvFiles);
506
+ const newRequireLines2 = generateGameConfigRequireLines(gameConfigFiles);
507
+
508
+ // 提取现有自定义代码(开发者手写的 GetCfg、GetCfgs 等方法)
509
+ let customCode = extractCustomCode(existingContent);
510
+ if (!customCode) customCode = '-- 在此处添加自定义代码,重新生成时会保留';
511
+
512
+ // 替换模板中的占位符
513
+ let result = template
514
+ .replace(/\{AUTHOR\}/g, author)
515
+ .replace(/\{DATETIME\}/g, dateTime)
516
+ .replace(/\{REQUIRE_LINES_1\}/g, newRequireLines1 || ' -- 暂无配置表')
517
+ .replace(/\{REQUIRE_LINES_2\}/g, newRequireLines2 || ' -- 暂无配置表');
518
+
519
+ // 替换自定义代码区域(保留开发者手写的内容)
520
+ const customStartIdx = result.indexOf(MARKERS.customStart);
521
+ const customEndIdx = result.indexOf(MARKERS.customEnd);
522
+
523
+ if (customStartIdx !== -1 && customEndIdx !== -1 && customStartIdx < customEndIdx) {
524
+ const beforeCustom = result.substring(0, customStartIdx + MARKERS.customStart.length);
525
+ const afterCustom = result.substring(customEndIdx);
526
+ result = beforeCustom + '\n' + customCode + '\n' + afterCustom;
527
+ }
528
+
529
+ // 检查内容是否有变化(忽略日期行)
530
+ if (existingContent) {
531
+ const normalize = (c) => c.split('\n').filter(l => !l.includes('-- 日期:')).join('\n').trim();
532
+ if (normalize(existingContent) === normalize(result)) {
533
+ console.log(' ⏭️ 内容无变化,跳过更新');
534
+ return null;
535
+ }
536
+ }
537
+
538
+ return { content: result, csvcfgCount: csvFiles.length, gameConfigCount: gameConfigFiles.length };
539
+ }
540
+
541
+ // ==================== 文件扫描 ====================
542
+
543
+ /**
544
+ * 递归获取目录下所有 Lua 文件
545
+ * @param {string} dir 目录路径
546
+ * @returns {Array} 文件列表 [{ name, fullPath }]
547
+ */
548
+ function getLuaFiles(dir) {
549
+ let results = [];
550
+ if (!fs.existsSync(dir)) return results;
551
+
552
+ for (const item of fs.readdirSync(dir)) {
553
+ const itemPath = path.join(dir, item);
554
+ try {
555
+ const stat = fs.statSync(itemPath);
556
+ if (stat.isDirectory()) {
557
+ results = results.concat(getLuaFiles(itemPath));
558
+ } else if (item.endsWith('.lua')) {
559
+ const fileName = item.replace('.lua', '');
560
+ let exclude = false;
561
+ for (const e of excludeFiles) {
562
+ if (fileName === e || fileName.startsWith(e)) { exclude = true; break; }
563
+ }
564
+ if (!exclude) results.push({ name: fileName, fullPath: itemPath });
565
+ }
566
+ } catch (err) { }
567
+ }
568
+ return results;
569
+ }
570
+
571
+ /**
572
+ * 获取 game_config 目录下的 Lua 文件(不递归,只有一层)
573
+ * @returns {Array} 文件列表 [{ name, fullPath }]
574
+ */
575
+ function getGameConfigFiles() {
576
+ const results = [];
577
+ if (!fs.existsSync(gameConfigDir)) return results;
578
+
579
+ for (const file of fs.readdirSync(gameConfigDir)) {
580
+ if (file.endsWith('.lua') && !file.startsWith('.')) {
581
+ const fileName = file.replace('.lua', '');
582
+ let exclude = false;
583
+ for (const e of excludeFiles) {
584
+ if (fileName === e || fileName.startsWith(e)) { exclude = true; break; }
585
+ }
586
+ if (!exclude) results.push({ name: fileName, fullPath: path.join(gameConfigDir, file) });
587
+ }
588
+ }
589
+ return results;
590
+ }
591
+
592
+ /**
593
+ * 备份文件
594
+ * @param {string} filePath 文件路径
595
+ * @returns {string|null} 备份文件路径
596
+ */
597
+ function backupFile(filePath) {
598
+ if (fs.existsSync(filePath)) {
599
+ const backupPath = filePath + '.backup_' + Date.now();
600
+ fs.writeFileSync(backupPath, fs.readFileSync(filePath, 'utf-8'));
601
+ console.log(` 📦 已备份: ${path.basename(backupPath)}`);
602
+ return backupPath;
603
+ }
604
+ return null;
605
+ }
606
+
607
+ /**
608
+ * 恢复备份
609
+ * @param {string} filePath 原文件路径
610
+ * @param {string} backupPath 备份文件路径
611
+ * @returns {boolean} 是否恢复成功
612
+ */
613
+ function restoreBackup(filePath, backupPath) {
614
+ if (backupPath && fs.existsSync(backupPath)) {
615
+ fs.writeFileSync(filePath, fs.readFileSync(backupPath, 'utf-8'));
616
+ fs.unlinkSync(backupPath);
617
+ console.log(' 🔄 已恢复备份');
618
+ return true;
619
+ }
620
+ return false;
621
+ }
622
+
623
+ // ==================== 主流程 ====================
624
+
625
+ /**
626
+ * 主函数:生成 TableMgr.lua
627
+ */
628
+ function main() {
629
+ // ========== 1. 更新配置目录 ==========
630
+ console.log('\n=== 更新配置目录 ===');
631
+
632
+ svnUpdate(csvcfgDir, '更新 csvcfg 目录');
633
+ svnUpdate(gameConfigDir, '更新 game_config 目录');
634
+ svnUpdate(mgrDir, '更新 mgr 目录');
635
+ // ========== 2. 扫描配置文件 ==========
636
+ console.log('\n=== 扫描配置文件 ===');
637
+
638
+ const csvFiles = getLuaFiles(csvcfgDir);
639
+ const gameConfigFiles = getGameConfigFiles();
640
+ console.log(`📋 csvcfg: ${csvFiles.length} 个, game_config: ${gameConfigFiles.length} 个`);
641
+
642
+ // 确保输出目录存在
643
+ if (!fs.existsSync(mgrDir)) fs.mkdirSync(mgrDir, { recursive: true });
644
+
645
+ // 读取现有文件内容(用于保留自定义代码)
646
+ const existingContent = fs.existsSync(tableMgrPath) ? fs.readFileSync(tableMgrPath, 'utf-8') : null;
647
+
648
+ // 智能合并生成新内容
649
+ const mergeResult = mergeAndGenerateContent(existingContent, csvFiles, gameConfigFiles, config.author, tableTemplate);
650
+
651
+ if (!mergeResult) {
652
+ console.log('\n✨ TableMgr.lua 内容无变化,跳过');
653
+ return;
654
+ }
655
+
656
+ // 备份原文件
657
+ const backupPath = backupFile(tableMgrPath);
658
+
659
+ try {
660
+ // 写入新文件
661
+ fs.writeFileSync(tableMgrPath, mergeResult.content, 'utf-8');
662
+ console.log(`\n✅ 生成管理器: ${tableMgrPath}`);
663
+ console.log(`📊 统计: csvcfg ${mergeResult.csvcfgCount} 个, game_config ${mergeResult.gameConfigCount} 个`);
664
+
665
+ // 删除备份(写入成功)
666
+ if (backupPath && fs.existsSync(backupPath)) fs.unlinkSync(backupPath);
667
+
668
+ // ========== 3. 提交到 SVN ==========
669
+ console.log('\n=== 提交到 SVN ===');
670
+ const commitSuccess = commitTableMgrToSvn()
671
+
672
+ if (commitSuccess) {
673
+ console.log('\n🎉 TableMgr.lua 已生成并提交到 SVN!');
674
+ } else {
675
+ console.log('\n⚠️ TableMgr.lua 已生成,但 SVN 提交失败,请手动处理');
676
+ }
677
+ } catch (err) {
678
+ console.error(`\n❌ 写入失败: ${err.message}`);
679
+ if (backupPath) restoreBackup(tableMgrPath, backupPath);
680
+ }
681
+ }
682
+
683
+ // 导出函数供其他模块调用
684
+ module.exports = {
685
+ getLuaFiles, // 获取 csvcfg 目录下所有 Lua 文件
686
+ getGameConfigFiles, // 获取 game_config 目录下所有 Lua 文件
687
+ getFieldsFromLuaFile, // 从 Lua 文件提取字段名
688
+ generateCsvcfgRequireLines, // 生成 csvcfg 的 require 语句
689
+ generateGameConfigRequireLines, // 生成 game_config 的 require 语句
690
+ mergeAndGenerateContent, // 智能合并生成内容
691
+ extractCustomCode, // 提取自定义代码区域
692
+ main // 主函数
693
+ };
694
+
695
+ // 直接执行时运行主函数
696
+ if (require.main === module) main();