mirror of
https://github.com/setube/ogame-vue-ts.git
synced 2026-05-11 23:45:11 +08:00
fix: 修复重复星球 ID 并优化 NPC 列表排序性能
修复玩家星球重复 ID 问题,通过构建映射关系更新相关引用(舰队任务、间谍报告等),避免数据指向错误目标。同时优化外交界面 NPC 列表计算,避免重复排序操作提升性能,并添加空列表检查防止除零错误。
This commit is contained in:
@@ -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 = {}
|
||||
|
||||
@@ -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
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
// 分页辅助函数
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user