From 2cfa275c7af67de357b91b74047ad6ece40520f0 Mon Sep 17 00:00:00 2001 From: wenyu Date: Wed, 18 Mar 2026 20:26:06 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E9=87=8D=E5=A4=8D?= =?UTF-8?q?=E6=98=9F=E7=90=83=20ID=20=E5=B9=B6=E4=BC=98=E5=8C=96=20NPC=20?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E6=8E=92=E5=BA=8F=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复玩家星球重复 ID 问题,通过构建映射关系更新相关引用(舰队任务、间谍报告等),避免数据指向错误目标。同时优化外交界面 NPC 列表计算,避免重复排序操作提升性能,并添加空列表检查防止除零错误。 --- src/utils/migration.ts | 447 +++++++++++++++++++++++++++++++----- src/views/DiplomacyView.vue | 20 +- src/views/GMView.vue | 2 + 3 files changed, 406 insertions(+), 63 deletions(-) diff --git a/src/utils/migration.ts b/src/utils/migration.ts index cc871b9..84af296 100644 --- a/src/utils/migration.ts +++ b/src/utils/migration.ts @@ -1,4 +1,18 @@ -import type { Planet, DebrisField, NPC } from '@/types/game' +import type { + AllyDefenseNotification, + DebrisField, + FleetMission, + IncomingFleetAlert, + JointAttackInvite, + MissionReport, + NPC, + NPCActivityNotification, + Planet, + Player, + Position, + SpiedNotification, + SpyReport +} from '@/types/game' import { decryptData, encryptData } from './crypto' import { generatePlanetTemperature } from '@/logic/planetLogic' import pkg from '../../package.json' @@ -8,6 +22,378 @@ import pkg from '../../package.json' * 用于从旧版本数据结构迁移到新版本 */ +type PlanetKind = 'planet' | 'moon' + +// oldPlanetId -> position -> planet/moon -> remapped target +type DuplicatePlanetIdMap = Map< + string, + Map> +> + +interface MigratablePlayer extends Player { + diplomaticRelations?: Record +} + +interface MigratableGameData { + currentPlanetId?: string + player?: MigratablePlayer + npcs?: NPC[] + universePlanets?: Record + debrisFields?: Record +} + +interface PlanetReferenceContext { + position?: Position + isMoon?: boolean + planetName?: string +} + +const getPlanetPositionKey = (position: Position): string => { + return `${position.galaxy}:${position.system}:${position.position}` +} + +const getPlanetKindKey = (isMoon?: boolean): PlanetKind => { + return isMoon ? 'moon' : 'planet' +} + +const buildDuplicatePlanetIdMap = (player: Player): DuplicatePlanetIdMap => { + const planetsByOriginalId = new Map() + + player.planets.forEach(planet => { + let group = planetsByOriginalId.get(planet.id) + if (!group) { + group = [] + planetsByOriginalId.set(planet.id, group) + } + group.push(planet) + }) + + const idMap: DuplicatePlanetIdMap = new Map() + + planetsByOriginalId.forEach((planets, originalId) => { + if (planets.length <= 1) return + + planets.forEach((planet, index) => { + if (index === 0) return + + const newId = `${originalId}_${Math.random().toString(36).substring(2, 9)}` + const positionKey = getPlanetPositionKey(planet.position) + + let byPosition = idMap.get(originalId) + if (!byPosition) { + byPosition = new Map() + idMap.set(originalId, byPosition) + } + + let byKind = byPosition.get(positionKey) + if (!byKind) { + byKind = new Map() + byPosition.set(positionKey, byKind) + } + + byKind.set(getPlanetKindKey(planet.isMoon), { + newId, + name: planet.name + }) + + planet.id = newId + }) + }) + + return idMap +} + +const resolveRemappedPlanetId = ( + planetId: string | undefined, + idMap: DuplicatePlanetIdMap, + context: PlanetReferenceContext = {} +): string | undefined => { + if (!planetId) return undefined + + const byPosition = idMap.get(planetId) + if (!byPosition) return undefined + + if (context.position) { + const byKind = byPosition.get(getPlanetPositionKey(context.position)) + if (!byKind) return undefined + + // 只有在位置或名称足够区分目标时才重写引用,避免把旧引用误指到错误星球 + if (context.isMoon !== undefined) { + return byKind.get(getPlanetKindKey(context.isMoon))?.newId + } + + if (context.planetName) { + const matchedByName = Array.from(byKind.values()).filter(entry => entry.name === context.planetName) + if (matchedByName.length === 1) { + const [matchedEntry] = matchedByName + if (matchedEntry) { + return matchedEntry.newId + } + } + } + + if (byKind.size === 1) { + return Array.from(byKind.values())[0]?.newId + } + + return undefined + } + + if (context.planetName) { + const matchedByName: Array<{ newId: string; name: string }> = [] + + byPosition.forEach(byKind => { + byKind.forEach(entry => { + if (entry.name === context.planetName) { + matchedByName.push(entry) + } + }) + }) + + if (matchedByName.length === 1) { + const [matchedEntry] = matchedByName + if (matchedEntry) { + return matchedEntry.newId + } + } + } + + return undefined +} + +const updatePlanetIdField = < + T extends Record, + K extends keyof T +>( + target: T, + key: K, + idMap: DuplicatePlanetIdMap, + context: PlanetReferenceContext = {} +): boolean => { + const currentValue = target[key] + if (typeof currentValue !== 'string') return false + + const remappedPlanetId = resolveRemappedPlanetId(currentValue, idMap, context) + if (!remappedPlanetId || remappedPlanetId === currentValue) return false + + target[key] = remappedPlanetId as T[K] + return true +} + +const updateMissionTargetPlanetId = (mission: FleetMission, idMap: DuplicatePlanetIdMap): boolean => { + return updatePlanetIdField(mission as unknown as Record, 'targetPlanetId', idMap, { + position: mission.targetPosition, + isMoon: mission.targetIsMoon + }) +} + +const updateSpyReportTargetPlanetId = (report: SpyReport, idMap: DuplicatePlanetIdMap): boolean => { + return updatePlanetIdField(report as unknown as Record, 'targetPlanetId', idMap, { + position: report.targetPosition, + planetName: report.targetPlanetName + }) +} + +const updateSpiedNotificationTargetPlanetId = ( + notification: SpiedNotification, + idMap: DuplicatePlanetIdMap +): boolean => { + return updatePlanetIdField(notification as unknown as Record, 'targetPlanetId', idMap, { + planetName: notification.targetPlanetName + }) +} + +const updateNPCActivityTargetPlanetId = ( + notification: NPCActivityNotification, + idMap: DuplicatePlanetIdMap +): boolean => { + return updatePlanetIdField(notification as unknown as Record, 'targetPlanetId', idMap, { + position: notification.targetPosition, + planetName: notification.targetPlanetName + }) +} + +const updateIncomingAlertTargetPlanetId = ( + alert: IncomingFleetAlert, + idMap: DuplicatePlanetIdMap +): boolean => { + return updatePlanetIdField(alert as unknown as Record, 'targetPlanetId', idMap, { + planetName: alert.targetPlanetName + }) +} + +const updateJointAttackTargetPlanetId = ( + invite: JointAttackInvite, + idMap: DuplicatePlanetIdMap +): boolean => { + return updatePlanetIdField(invite as unknown as Record, 'targetPlanetId', idMap, { + position: invite.targetPosition + }) +} + +const updateAllyDefenseTargetPlanetId = ( + notification: AllyDefenseNotification, + idMap: DuplicatePlanetIdMap +): boolean => { + return updatePlanetIdField(notification as unknown as Record, 'targetPlanetId', idMap, { + planetName: notification.targetPlanetName + }) +} + +const updateMissionReportPlanetIds = (report: MissionReport, idMap: DuplicatePlanetIdMap): boolean => { + let mutated = false + + if (updatePlanetIdField(report as unknown as Record, 'originPlanetId', idMap, { + planetName: report.originPlanetName + })) { + mutated = true + } + + if (updatePlanetIdField(report as unknown as Record, 'targetPlanetId', idMap, { + position: report.targetPosition, + planetName: report.targetPlanetName + })) { + mutated = true + } + + if (report.details?.newPlanetId) { + const remappedNewPlanetId = resolveRemappedPlanetId(report.details.newPlanetId, idMap, { + position: report.targetPosition, + planetName: report.details.newPlanetName || report.targetPlanetName + }) + + if (remappedNewPlanetId && remappedNewPlanetId !== report.details.newPlanetId) { + report.details.newPlanetId = remappedNewPlanetId + mutated = true + } + } + + return mutated +} + +/** + * 修复玩家星球的重复ID,并同步更新可被可靠识别的旧引用。 + * 缺少位置或名称上下文、无法安全判定归属的旧引用会保留原ID, + * 继续指向保留下来的首个星球,避免把数据误指到错误目标。 + */ +const fixDuplicatePlanetIds = (data: MigratableGameData): boolean => { + const player = data.player + if (!player || !Array.isArray(player.planets) || player.planets.length === 0) { + return false + } + + const idMap = buildDuplicatePlanetIdMap(player) + if (idMap.size === 0) { + return false + } + + let mutated = true + + player.planets.forEach(planet => { + if (planet.isMoon && updatePlanetIdField(planet as unknown as Record, 'parentPlanetId', idMap, { + position: planet.position, + isMoon: false + })) { + mutated = true + } + + // 等待队列里的 planetId 应始终与所属星球保持一致 + planet.waitingBuildQueue?.forEach(item => { + if (item.planetId && item.planetId !== planet.id) { + item.planetId = planet.id + mutated = true + } + }) + }) + + if (updatePlanetIdField(data as unknown as Record, 'currentPlanetId', idMap)) { + mutated = true + } + + player.fleetMissions?.forEach(mission => { + if (updateMissionTargetPlanetId(mission, idMap)) { + mutated = true + } + }) + + player.spyReports?.forEach(report => { + if (updateSpyReportTargetPlanetId(report, idMap)) { + mutated = true + } + }) + + player.spiedNotifications?.forEach(notification => { + if (updateSpiedNotificationTargetPlanetId(notification, idMap)) { + mutated = true + } + }) + + player.npcActivityNotifications?.forEach(notification => { + if (updateNPCActivityTargetPlanetId(notification, idMap)) { + mutated = true + } + }) + + player.missionReports?.forEach(report => { + if (updateMissionReportPlanetIds(report, idMap)) { + mutated = true + } + }) + + player.incomingFleetAlerts?.forEach(alert => { + if (updateIncomingAlertTargetPlanetId(alert, idMap)) { + mutated = true + } + }) + + player.jointAttackInvites?.forEach(invite => { + if (updateJointAttackTargetPlanetId(invite, idMap)) { + mutated = true + } + }) + + player.allyDefenseNotifications?.forEach(notification => { + if (updateAllyDefenseTargetPlanetId(notification, idMap)) { + mutated = true + } + }) + + data.npcs?.forEach(npc => { + if (npc.playerSpyReports) { + // playerSpyReports 的 key 就是玩家星球 ID,需要和报告内容一起迁移 + const remappedPlayerSpyReports: Record = {} + + Object.entries(npc.playerSpyReports).forEach(([planetId, report]) => { + if (updateSpyReportTargetPlanetId(report, idMap)) { + mutated = true + } + + const remappedPlanetId = resolveRemappedPlanetId(planetId, idMap, { + position: report.targetPosition, + planetName: report.targetPlanetName + }) + + if (remappedPlanetId && remappedPlanetId !== planetId) { + remappedPlayerSpyReports[remappedPlanetId] = report + mutated = true + } else { + remappedPlayerSpyReports[planetId] = report + } + }) + + npc.playerSpyReports = remappedPlayerSpyReports + } + + npc.fleetMissions?.forEach(mission => { + if (updateMissionTargetPlanetId(mission, idMap)) { + mutated = true + } + }) + }) + + return mutated +} + /** * 执行数据迁移 * 将旧版本的 universePlanets 和 debrisFields 从 gameStore 迁移到 universeStore @@ -22,13 +408,13 @@ export const migrateGameData = (): void => { if (!oldEncryptedData) return // 尝试解密(如果是加密格式) - let oldData: any + let oldData: MigratableGameData try { - oldData = decryptData(oldEncryptedData) + oldData = decryptData(oldEncryptedData) as MigratableGameData } catch { // 解密失败,可能是新格式(未加密),直接解析 try { - oldData = JSON.parse(oldEncryptedData) + oldData = JSON.parse(oldEncryptedData) as MigratableGameData } catch { return // 无法解析,放弃迁移 } @@ -101,54 +487,8 @@ export const migrateGameData = (): void => { } // 修复重复的星球ID - if (oldData.player?.planets && Array.isArray(oldData.player.planets)) { - const planetsByOriginalId = new Map() - - // 第一步:按ID分组 - oldData.player.planets.forEach((planet: Planet) => { - if (!planetsByOriginalId.has(planet.id)) { - planetsByOriginalId.set(planet.id, []) - } - planetsByOriginalId.get(planet.id)!.push(planet) - }) - - // 第二步:处理重复ID并建立映射 - const idMap = new Map() // Key: oldId_galaxy:system:position -> Value: newId - - planetsByOriginalId.forEach((planets, originalId) => { - if (planets.length > 1) { - // 对重复组中的星球,保留第一个,修改后续的 - planets.forEach((planet, index) => { - if (index > 0) { - const newId = `${originalId}_${Math.random().toString(36).substring(2, 9)}` - const posKey = `${planet.position.galaxy}:${planet.position.system}:${planet.position.position}` - // 记录映射:原始ID + 坐标 -> 新ID - idMap.set(`${originalId}_${posKey}`, newId) - - // 修改星球ID - planet.id = newId - needsSave = true - } - }) - } - }) - - // 第三步:更新月球的 parentPlanetId - if (idMap.size > 0) { - oldData.player.planets.forEach((planet: Planet) => { - if (planet.isMoon && planet.parentPlanetId) { - // 假设月球和母星坐标一致,通过月球坐标查找母星的新ID - const posKey = `${planet.position.galaxy}:${planet.position.system}:${planet.position.position}` - const mapKey = `${planet.parentPlanetId}_${posKey}` - - const newParentId = idMap.get(mapKey) - if (newParentId) { - planet.parentPlanetId = newParentId - needsSave = true - } - } - }) - } + if (fixDuplicatePlanetIds(oldData)) { + needsSave = true } // 迁移温度数据:为没有温度的星球生成温度 @@ -215,10 +555,11 @@ export const migrateGameData = (): void => { // 新版本统一使用 npc.relations[playerId] 存储NPC对玩家的关系 if (oldData.player?.diplomaticRelations && oldData.npcs && Array.isArray(oldData.npcs)) { const playerId = oldData.player.id + const npcs = oldData.npcs const playerRelations = oldData.player.diplomaticRelations as Record Object.entries(playerRelations).forEach(([npcId, relation]) => { - const npc = oldData.npcs.find((n: NPC) => n.id === npcId) + const npc = npcs.find((n: NPC) => n.id === npcId) if (npc) { if (!npc.relations) { npc.relations = {} diff --git a/src/views/DiplomacyView.vue b/src/views/DiplomacyView.vue index aa3d78a..2fa786a 100644 --- a/src/views/DiplomacyView.vue +++ b/src/views/DiplomacyView.vue @@ -708,30 +708,30 @@ } // 按关系状态分类NPC(同时应用搜索过滤) - const allNpcs = computed(() => sortNpcs(npcStore.npcs.filter(matchesSearch))) + // 先统一排序一次,避免不同标签页在同一批数据上重复排序 + const sortedNpcs = computed(() => sortNpcs(npcStore.npcs.filter(matchesSearch))) + + const allNpcs = computed(() => sortedNpcs.value) const friendlyNpcs = computed(() => { - return sortNpcs(npcStore.npcs.filter(npc => { - if (!matchesSearch(npc)) return false + return sortedNpcs.value.filter(npc => { const relation = getRelation(npc.id) return relation?.status === RelationStatus.Friendly - })) + }) }) const neutralNpcs = computed(() => { - return sortNpcs(npcStore.npcs.filter(npc => { - if (!matchesSearch(npc)) return false + return sortedNpcs.value.filter(npc => { const relation = getRelation(npc.id) return !relation || relation.status === RelationStatus.Neutral - })) + }) }) const hostileNpcs = computed(() => { - return sortNpcs(npcStore.npcs.filter(npc => { - if (!matchesSearch(npc)) return false + return sortedNpcs.value.filter(npc => { const relation = getRelation(npc.id) return relation?.status === RelationStatus.Hostile - })) + }) }) // 分页辅助函数 diff --git a/src/views/GMView.vue b/src/views/GMView.vue index fa5fd74..bb5a3d4 100644 --- a/src/views/GMView.vue +++ b/src/views/GMView.vue @@ -552,6 +552,8 @@ }) } else if (section.tabValue === 'ships') { if (!selectedPlanet.value) return + // 某些过滤场景下舰船列表可能为空,避免平均分配时除以 0 + if (!section.items.length) return // 重新计算最大舰队仓储,确保数据是最新的 const maxStorage = calculateMaxFleetStorage(selectedPlanet.value, gameStore.player.technologies)