fix: 修复重复星球 ID 并优化 NPC 列表排序性能

修复玩家星球重复 ID 问题,通过构建映射关系更新相关引用(舰队任务、间谍报告等),避免数据指向错误目标。同时优化外交界面 NPC 列表计算,避免重复排序操作提升性能,并添加空列表检查防止除零错误。
This commit is contained in:
wenyu
2026-03-18 20:26:06 +08:00
parent b1cf0acaae
commit 2cfa275c7a
3 changed files with 406 additions and 63 deletions

View File

@@ -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<string, Map<PlanetKind, { newId: string; name: string }>>
>
interface MigratablePlayer extends Player {
diplomaticRelations?: Record<string, unknown>
}
interface MigratableGameData {
currentPlanetId?: string
player?: MigratablePlayer
npcs?: NPC[]
universePlanets?: Record<string, Planet>
debrisFields?: Record<string, DebrisField>
}
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<string, Planet[]>()
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<string, unknown>,
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<string, unknown>, 'targetPlanetId', idMap, {
position: mission.targetPosition,
isMoon: mission.targetIsMoon
})
}
const updateSpyReportTargetPlanetId = (report: SpyReport, idMap: DuplicatePlanetIdMap): boolean => {
return updatePlanetIdField(report as unknown as Record<string, unknown>, 'targetPlanetId', idMap, {
position: report.targetPosition,
planetName: report.targetPlanetName
})
}
const updateSpiedNotificationTargetPlanetId = (
notification: SpiedNotification,
idMap: DuplicatePlanetIdMap
): boolean => {
return updatePlanetIdField(notification as unknown as Record<string, unknown>, 'targetPlanetId', idMap, {
planetName: notification.targetPlanetName
})
}
const updateNPCActivityTargetPlanetId = (
notification: NPCActivityNotification,
idMap: DuplicatePlanetIdMap
): boolean => {
return updatePlanetIdField(notification as unknown as Record<string, unknown>, 'targetPlanetId', idMap, {
position: notification.targetPosition,
planetName: notification.targetPlanetName
})
}
const updateIncomingAlertTargetPlanetId = (
alert: IncomingFleetAlert,
idMap: DuplicatePlanetIdMap
): boolean => {
return updatePlanetIdField(alert as unknown as Record<string, unknown>, 'targetPlanetId', idMap, {
planetName: alert.targetPlanetName
})
}
const updateJointAttackTargetPlanetId = (
invite: JointAttackInvite,
idMap: DuplicatePlanetIdMap
): boolean => {
return updatePlanetIdField(invite as unknown as Record<string, unknown>, 'targetPlanetId', idMap, {
position: invite.targetPosition
})
}
const updateAllyDefenseTargetPlanetId = (
notification: AllyDefenseNotification,
idMap: DuplicatePlanetIdMap
): boolean => {
return updatePlanetIdField(notification as unknown as Record<string, unknown>, 'targetPlanetId', idMap, {
planetName: notification.targetPlanetName
})
}
const updateMissionReportPlanetIds = (report: MissionReport, idMap: DuplicatePlanetIdMap): boolean => {
let mutated = false
if (updatePlanetIdField(report as unknown as Record<string, unknown>, 'originPlanetId', idMap, {
planetName: report.originPlanetName
})) {
mutated = true
}
if (updatePlanetIdField(report as unknown as Record<string, unknown>, '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<string, unknown>, '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<string, unknown>, '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<string, SpyReport> = {}
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<string, Planet[]>()
// 第一步按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<string, string>() // 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<string, any>
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 = {}

View File

@@ -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
}))
})
})
// 分页辅助函数

View File

@@ -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)