import { membershipAdd, membershipClear, membershipHas, createMembershipSet, type MembershipSet } from '../../mx/tools/MembershipSet'; /** 读表(与 ConfigTableData 方法对齐,避免循环引用) */ export interface TableRowSource { /** 表全部行 */ getTableList(tableName: string): T[]; /** 按 id 取一行 */ getTableData(tableName: string, id: number): T | null; } /** 索敌已实现的 atk_type 头 */ const ATK_LOGIC: readonly number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 20]; /** Skill.skill 额外效果已落地的头 */ const EXTRA_SKILL: readonly number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; /** Weapon.type(1刀 2枪 3棍 4弓 5燃烧 6增幅) */ const WEAPON_TYPE: readonly number[] = [1, 2, 3, 4, 5, 6]; /** * 配表与实现契约对不上时的统一告警。 * 只打印,不改数值、不补默认、不假装成功。 */ export class TableAnomaly { /** 策划检索前缀 */ static readonly PREFIX = '【配表异常】'; private static _seen: MembershipSet = createMembershipSet(); /** * 打印配表异常(同 key 只打一次)。 * @param table 表名 * @param id 行 id * @param field 字段 * @param reason 原因 * @param extra 现场值 */ static warn(table: string, id: number | string, field: string, reason: string, extra?: unknown): void { const key = `${table}\t${id}\t${field}\t${reason}`; if (membershipHas(this._seen, key)) return; membershipAdd(this._seen, key); if (extra !== undefined) { console.warn(TableAnomaly.PREFIX, table, 'id=', id, field, reason, extra); return; } console.warn(TableAnomaly.PREFIX, table, 'id=', id, field, reason); } /** 清空去重(热更后重新扫) */ static reset(): void { membershipClear(this._seen); } /** * 扫描已加载表:引用断裂、类型不在实现集、主动 cd≤0 等。 * @param src 配表源 */ static scan(src: TableRowSource): void { this.reset(); this._scanSkill(src); this._scanHero(src); this._scanWeapon(src); } private static _scanSkill(src: TableRowSource): void { const rows = src.getTableList('Skill'); for (let i = 0; i < rows.length; i++) { const row = rows[i]; if (!row) continue; const id = Number(row.id) || 0; const typ = Math.floor(Number(row.type) || 0); if (typ !== 1 && typ !== 2) { this.warn('Skill', id, 'type', '不是主动1/被动2', row.type); } if (typ === 1 && !(Number(row.cd) > 0)) { this.warn('Skill', id, 'cd', '主动技能 cd≤0,实现不会编造间隔', row.cd); } const atkHead = this._csvHead(row.atk_type); const atkRaw = String(row.atk_type ?? '').trim(); if (atkRaw && !this._in(ATK_LOGIC, atkHead)) { this.warn('Skill', id, 'atk_type', '头类型无索敌实现', atkRaw); } const extraHead = this._csvHead(row.skill); if (String(row.skill ?? '').trim() && extraHead > 0 && !this._in(EXTRA_SKILL, extraHead)) { this.warn('Skill', id, 'skill', '额外效果头未实现', row.skill); } if (typ === 1 && !atkRaw && extraHead <= 0) { this.warn('Skill', id, 'atk_type', '主动技能无 atk_type 且无 skill 额外效果'); } } } private static _scanHero(src: TableRowSource): void { const rows = src.getTableList('Hero'); for (let i = 0; i < rows.length; i++) { const row = rows[i]; if (!row) continue; const id = Number(row.id) || 0; const ht = Math.floor(Number(row.herotype) || 0); if (ht !== 1 && ht !== 2 && ht !== 3) { this.warn('Hero', id, 'herotype', '不是基础字1/合成武将2/功能字3', row.herotype); } this._checkSkillIds(src, 'Hero', id, 'atk_id', row.atk_id); this._checkSkillIds(src, 'Hero', id, 'act_skill', row.act_skill); this._checkWeaponRef(src, 'Hero', id, 'startweapon', Number(row.startweapon) || 0); this._checkWeaponRef(src, 'Hero', id, 'personalweapon', Number(row.personalweapon) || 0); const startId = Number(row.startweapon) || 0; if (ht === 2 && startId > 0) { const w = src.getTableData('Weapon', startId); const want = Math.floor(Number(row.weapon) || 0); if (w && want > 0 && want !== 5 && Math.floor(Number(w.type) || 0) !== want) { this.warn('Hero', id, 'startweapon', '默认武器 Weapon.type 与 Hero.weapon 不一致', `${startId} type=${w.type} weapon=${want}`); } } const limit = Math.floor(Number(row.level_limit) || 0); const atkLen = row.atk_id?.length ?? 0; if (limit > 0 && atkLen > 0 && atkLen < limit) { this.warn('Hero', id, 'atk_id', '长度小于 level_limit', `len=${atkLen} limit=${limit}`); } } } private static _scanWeapon(src: TableRowSource): void { const rows = src.getTableList('Weapon'); for (let i = 0; i < rows.length; i++) { const row = rows[i]; if (!row) continue; const id = Number(row.id) || 0; const typ = Math.floor(Number(row.type) || 0); if (!this._in(WEAPON_TYPE, typ)) { this.warn('Weapon', id, 'type', '不是刀1/枪2/棍3/弓4/燃烧5/增幅6', row.type); } const skillId = Number(row.act_skill) || 0; if (skillId > 0 && !src.getTableData('Skill', skillId)) { this.warn('Weapon', id, 'act_skill', 'Skill 行不存在', skillId); } const heroId = Number(row.heroid) || 0; if (heroId > 0) { const hero = src.getTableData('Hero', heroId); if (!hero) { this.warn('Weapon', id, 'heroid', 'Hero 行不存在', heroId); } else if (Math.floor(Number(hero.personalweapon) || 0) !== id) { this.warn('Weapon', id, 'heroid', 'Hero.personalweapon 不是本武器', `hero=${heroId} personalweapon=${hero.personalweapon}`); } } if (this._in(WEAPON_TYPE, typ) && !String(row.skin ?? '').trim()) { this.warn('Weapon', id, 'skin', '战斗武器皮肤为空'); } } } private static _checkSkillIds(src: TableRowSource, table: string, id: number, field: string, ids: readonly number[] | undefined): void { if (!ids) return; for (let i = 0; i < ids.length; i++) { const sid = Number(ids[i]) || 0; if (sid <= 0) continue; if (!src.getTableData('Skill', sid)) { this.warn(table, id, field, `第${i}项 Skill 行不存在`, sid); } } } private static _checkWeaponRef(src: TableRowSource, table: string, id: number, field: string, weaponId: number): void { if (weaponId <= 0) return; if (!src.getTableData('Weapon', weaponId)) { this.warn(table, id, field, 'Weapon 行不存在', weaponId); } } private static _csvHead(raw: unknown): number { const s = String(raw ?? '').trim(); if (!s) return 0; return Math.floor(Number(s.split(',')[0]) || 0); } private static _in(list: readonly number[], n: number): boolean { for (let i = 0; i < list.length; i++) { if (list[i] === n) return true; } return false; } }