package masterdata import ( "fmt" "sort" "lunar-tear/server/internal/utils" ) type BattleDropInfo struct { QuestSceneId int32 BattleDropCategoryId int32 } type QuestCatalog struct { SceneById map[int32]EntityMQuestScene MissionById map[int32]EntityMQuestMission QuestById map[int32]EntityMQuest MissionIdsByQuestId map[int32][]int32 RouteIdByQuestId map[int32]int32 SceneIdsByQuestId map[int32][]int32 OrderedQuestIds []int32 FirstClearRewardsByGroupId map[int32][]EntityMQuestFirstClearRewardGroup FirstClearRewardSwitchesByQuestId map[int32][]EntityMQuestFirstClearRewardSwitch MissionRewardsByMissionId map[int32][]EntityMQuestMissionReward WeaponIdsByReleaseConditionGroupId map[int32][]int32 ReleaseConditionsByGroupId map[int32][]EntityMWeaponStoryReleaseConditionGroup SceneGrantsBySceneId map[int32][]EntityMUserQuestSceneGrantPossession BattleDropRewardById map[int32]EntityMBattleDropReward PickupRewardIdsByGroupId map[int32][]int32 BattleDropsByQuestId map[int32][]BattleDropInfo ReplayFlowRewardsByGroupId map[int32][]EntityMQuestReplayFlowRewardGroup RentalQuestIds map[int32]bool TutorialUnlockConditions []EntityMTutorialUnlockCondition ChapterLastSceneByQuestId map[int32]int32 SeasonIdByRouteId map[int32]int32 QuestsWithDifficulty map[int32]bool // any questId referenced in m_quest_relation_main_flow BattleOnlyTargetSceneByQuestId map[int32]int32 UserExpThresholds []int32 CharacterExpThresholds []int32 CostumeExpByRarity map[int32][]int32 CostumeMaxLevelByRarity map[int32]NumericalFunc MaxStaminaByLevel map[int32]int32 CostumeById map[int32]EntityMCostume WeaponById map[int32]EntityMWeapon WeaponSkillSlots map[int32][]int32 WeaponAbilitySlots map[int32][]int32 *PartsCatalog } func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) { scenes, err := utils.ReadTable[EntityMQuestScene]("m_quest_scene") if err != nil { return nil, fmt.Errorf("load quest scene table: %w", err) } sort.Slice(scenes, func(i, j int) bool { if scenes[i].QuestId != scenes[j].QuestId { return scenes[i].QuestId < scenes[j].QuestId } if scenes[i].SortOrder != scenes[j].SortOrder { return scenes[i].SortOrder < scenes[j].SortOrder } return scenes[i].QuestSceneId < scenes[j].QuestSceneId }) missions, err := utils.ReadTable[EntityMQuestMission]("m_quest_mission") if err != nil { return nil, fmt.Errorf("load quest mission table: %w", err) } quests, err := utils.ReadTable[EntityMQuest]("m_quest") if err != nil { return nil, fmt.Errorf("load quest table: %w", err) } missionGroups, err := utils.ReadTable[EntityMQuestMissionGroup]("m_quest_mission_group") if err != nil { return nil, fmt.Errorf("load quest mission group table: %w", err) } sort.Slice(missionGroups, func(i, j int) bool { if missionGroups[i].QuestMissionGroupId != missionGroups[j].QuestMissionGroupId { return missionGroups[i].QuestMissionGroupId < missionGroups[j].QuestMissionGroupId } if missionGroups[i].SortOrder != missionGroups[j].SortOrder { return missionGroups[i].SortOrder < missionGroups[j].SortOrder } return missionGroups[i].QuestMissionId < missionGroups[j].QuestMissionId }) sequences, err := utils.ReadTable[EntityMMainQuestSequence]("m_main_quest_sequence") if err != nil { return nil, fmt.Errorf("load main quest sequence table: %w", err) } sort.Slice(sequences, func(i, j int) bool { if sequences[i].MainQuestSequenceId != sequences[j].MainQuestSequenceId { return sequences[i].MainQuestSequenceId < sequences[j].MainQuestSequenceId } if sequences[i].SortOrder != sequences[j].SortOrder { return sequences[i].SortOrder < sequences[j].SortOrder } return sequences[i].QuestId < sequences[j].QuestId }) chapters, err := utils.ReadTable[EntityMMainQuestChapter]("m_main_quest_chapter") if err != nil { return nil, fmt.Errorf("load main quest chapter table: %w", err) } routes, err := utils.ReadTable[EntityMMainQuestRoute]("m_main_quest_route") if err != nil { return nil, fmt.Errorf("load main quest route table: %w", err) } seasonIdByRouteId := make(map[int32]int32, len(routes)) for _, r := range routes { seasonIdByRouteId[r.MainQuestRouteId] = r.MainQuestSeasonId } firstClearSwitches, err := utils.ReadTable[EntityMQuestFirstClearRewardSwitch]("m_quest_first_clear_reward_switch") if err != nil { return nil, fmt.Errorf("load quest first clear reward switch table: %w", err) } firstClearRewards, err := utils.ReadTable[EntityMQuestFirstClearRewardGroup]("m_quest_first_clear_reward_group") if err != nil { return nil, fmt.Errorf("load quest first clear reward group table: %w", err) } sort.Slice(firstClearRewards, func(i, j int) bool { if firstClearRewards[i].QuestFirstClearRewardGroupId != firstClearRewards[j].QuestFirstClearRewardGroupId { return firstClearRewards[i].QuestFirstClearRewardGroupId < firstClearRewards[j].QuestFirstClearRewardGroupId } if firstClearRewards[i].SortOrder != firstClearRewards[j].SortOrder { return firstClearRewards[i].SortOrder < firstClearRewards[j].SortOrder } return firstClearRewards[i].QuestFirstClearRewardType < firstClearRewards[j].QuestFirstClearRewardType }) replayFlowRewards, err := utils.ReadTable[EntityMQuestReplayFlowRewardGroup]("m_quest_replay_flow_reward_group") if err != nil { return nil, fmt.Errorf("load quest replay flow reward group table: %w", err) } sort.Slice(replayFlowRewards, func(i, j int) bool { if replayFlowRewards[i].QuestReplayFlowRewardGroupId != replayFlowRewards[j].QuestReplayFlowRewardGroupId { return replayFlowRewards[i].QuestReplayFlowRewardGroupId < replayFlowRewards[j].QuestReplayFlowRewardGroupId } return replayFlowRewards[i].SortOrder < replayFlowRewards[j].SortOrder }) missionRewards, err := utils.ReadTable[EntityMQuestMissionReward]("m_quest_mission_reward") if err != nil { return nil, fmt.Errorf("load quest mission reward table: %w", err) } weapons, err := utils.ReadTable[EntityMWeapon]("m_weapon") if err != nil { return nil, fmt.Errorf("load weapon table: %w", err) } weaponSkillGroups, err := utils.ReadTable[EntityMWeaponSkillGroup]("m_weapon_skill_group") if err != nil { return nil, fmt.Errorf("load weapon skill group table: %w", err) } weaponAbilityGroups, err := utils.ReadTable[EntityMWeaponAbilityGroup]("m_weapon_ability_group") if err != nil { return nil, fmt.Errorf("load weapon ability group table: %w", err) } releaseConditions, err := utils.ReadTable[EntityMWeaponStoryReleaseConditionGroup]("m_weapon_story_release_condition_group") if err != nil { return nil, fmt.Errorf("load weapon story release condition table: %w", err) } costumeMasters, err := utils.ReadTable[EntityMCostume]("m_costume") if err != nil { return nil, fmt.Errorf("load costume table: %w", err) } costumeRarities, err := utils.ReadTable[EntityMCostumeRarity]("m_costume_rarity") if err != nil { return nil, fmt.Errorf("load costume rarity table: %w", err) } sceneGrants, err := utils.ReadTable[EntityMUserQuestSceneGrantPossession]("m_user_quest_scene_grant_possession") if err != nil { return nil, fmt.Errorf("load quest scene grant table: %w", err) } battleDropRewards, err := utils.ReadTable[EntityMBattleDropReward]("m_battle_drop_reward") if err != nil { return nil, fmt.Errorf("load battle drop reward table: %w", err) } pickupRewardGroups, err := utils.ReadTable[EntityMQuestPickupRewardGroup]("m_quest_pickup_reward_group") if err != nil { return nil, fmt.Errorf("load quest pickup reward group table: %w", err) } sort.Slice(pickupRewardGroups, func(i, j int) bool { if pickupRewardGroups[i].QuestPickupRewardGroupId != pickupRewardGroups[j].QuestPickupRewardGroupId { return pickupRewardGroups[i].QuestPickupRewardGroupId < pickupRewardGroups[j].QuestPickupRewardGroupId } return pickupRewardGroups[i].SortOrder < pickupRewardGroups[j].SortOrder }) sceneBattles, err := utils.ReadTable[EntityMQuestSceneBattle]("m_quest_scene_battle") if err != nil { return nil, fmt.Errorf("load quest scene battle table: %w", err) } battleGroups, err := utils.ReadTable[EntityMBattleGroup]("m_battle_group") if err != nil { return nil, fmt.Errorf("load battle group table: %w", err) } battles, err := utils.ReadTable[EntityMBattle]("m_battle") if err != nil { return nil, fmt.Errorf("load battle table: %w", err) } npcDecks, err := utils.ReadTable[EntityMBattleNpcDeck]("m_battle_npc_deck") if err != nil { return nil, fmt.Errorf("load battle npc deck table: %w", err) } npcDropCategories, err := utils.ReadTable[EntityMBattleNpcDeckCharacterDropCategory]("m_battle_npc_deck_character_drop_category") if err != nil { return nil, fmt.Errorf("load battle npc drop category table: %w", err) } rentalDecks, err := utils.ReadTable[EntityMBattleRentalDeck]("m_battle_rental_deck") if err != nil { return nil, fmt.Errorf("load battle rental deck table: %w", err) } tutorialUnlockConds, err := utils.ReadTable[EntityMTutorialUnlockCondition]("m_tutorial_unlock_condition") if err != nil { return nil, fmt.Errorf("load tutorial unlock condition table: %w", err) } relations, err := utils.ReadTable[EntityMQuestRelationMainFlow]("m_quest_relation_main_flow") if err != nil { return nil, fmt.Errorf("load quest relation main flow table: %w", err) } questsWithDifficulty := make(map[int32]bool, len(relations)*3) for _, r := range relations { questsWithDifficulty[r.MainFlowQuestId] = true if r.ReplayFlowQuestId != 0 { questsWithDifficulty[r.ReplayFlowQuestId] = true } if r.SubFlowQuestId != 0 { questsWithDifficulty[r.SubFlowQuestId] = true } } battleOnlyTargetSceneByQuestId := make(map[int32]int32) for _, scene := range scenes { if scene.IsBattleOnlyTarget { if _, exists := battleOnlyTargetSceneByQuestId[scene.QuestId]; !exists { battleOnlyTargetSceneByQuestId[scene.QuestId] = scene.QuestSceneId } } } paramMapRows, err := LoadParameterMap() if err != nil { return nil, err } userLevels, err := utils.ReadTable[EntityMUserLevel]("m_user_level") if err != nil { return nil, fmt.Errorf("load user level table: %w", err) } maxStaminaByLevel := make(map[int32]int32, len(userLevels)) for _, ul := range userLevels { maxStaminaByLevel[ul.UserLevel] = ul.MaxStamina } funcResolver, err := LoadFunctionResolver() if err != nil { return nil, fmt.Errorf("load function resolver: %w", err) } costumeExpByRarity := make(map[int32][]int32, len(costumeRarities)) costumeMaxLevelByRarity := make(map[int32]NumericalFunc, len(costumeRarities)) for _, r := range costumeRarities { if _, ok := costumeExpByRarity[r.RarityType]; !ok { costumeExpByRarity[r.RarityType] = BuildExpThresholds(paramMapRows, r.RequiredExpForLevelUpNumericalParameterMapId) } if _, ok := costumeMaxLevelByRarity[r.RarityType]; !ok { if f, found := funcResolver.Resolve(r.MaxLevelNumericalFunctionId); found { costumeMaxLevelByRarity[r.RarityType] = f } } } costumeById := make(map[int32]EntityMCostume, len(costumeMasters)) for _, cm := range costumeMasters { costumeById[cm.CostumeId] = cm } weaponById := make(map[int32]EntityMWeapon, len(weapons)) for _, w := range weapons { weaponById[w.WeaponId] = w } skillSlots := make(map[int32][]int32) for _, row := range weaponSkillGroups { skillSlots[row.WeaponSkillGroupId] = append(skillSlots[row.WeaponSkillGroupId], row.SlotNumber) } abilitySlots := make(map[int32][]int32) for _, row := range weaponAbilityGroups { abilitySlots[row.WeaponAbilityGroupId] = append(abilitySlots[row.WeaponAbilityGroupId], row.SlotNumber) } sceneById := make(map[int32]EntityMQuestScene, len(scenes)) sceneIdsByQuestId := make(map[int32][]int32) for _, scene := range scenes { sceneById[scene.QuestSceneId] = scene sceneIdsByQuestId[scene.QuestId] = append(sceneIdsByQuestId[scene.QuestId], scene.QuestSceneId) } missionById := make(map[int32]EntityMQuestMission, len(missions)) for _, mission := range missions { missionById[mission.QuestMissionId] = mission } questById := make(map[int32]EntityMQuest, len(quests)) for _, quest := range quests { questById[quest.QuestId] = quest } missionIdsByGroupId := make(map[int32][]int32, len(missionGroups)) for _, mg := range missionGroups { missionIdsByGroupId[mg.QuestMissionGroupId] = append( missionIdsByGroupId[mg.QuestMissionGroupId], mg.QuestMissionId) } missionIdsByQuestId := make(map[int32][]int32) for questId, quest := range questById { missionIds := missionIdsByGroupId[quest.QuestMissionGroupId] if len(missionIds) == 0 { continue } missionIdsByQuestId[questId] = append([]int32(nil), missionIds...) } chapterBySequenceId := make(map[int32]EntityMMainQuestChapter, len(chapters)) for _, chapter := range chapters { chapterBySequenceId[chapter.MainQuestSequenceGroupId] = chapter } routeIdByQuestId := make(map[int32]int32) for _, sequence := range sequences { if chapter, ok := chapterBySequenceId[sequence.MainQuestSequenceId]; ok { routeIdByQuestId[sequence.QuestId] = chapter.MainQuestRouteId } } sortedChapters := make([]EntityMMainQuestChapter, len(chapters)) copy(sortedChapters, chapters) sort.Slice(sortedChapters, func(i, j int) bool { return sortedChapters[i].SortOrder < sortedChapters[j].SortOrder }) sequencesByGroupId := make(map[int32][]EntityMMainQuestSequence) for _, seq := range sequences { sequencesByGroupId[seq.MainQuestSequenceId] = append(sequencesByGroupId[seq.MainQuestSequenceId], seq) } var orderedQuestIds []int32 for _, chapter := range sortedChapters { for _, seq := range sequencesByGroupId[chapter.MainQuestSequenceGroupId] { orderedQuestIds = append(orderedQuestIds, seq.QuestId) } } chapterLastSceneByQuestId := make(map[int32]int32) for _, chapter := range sortedChapters { seqs := sequencesByGroupId[chapter.MainQuestSequenceGroupId] var chapterLastScene int32 for i := len(seqs) - 1; i >= 0; i-- { if sids := sceneIdsByQuestId[seqs[i].QuestId]; len(sids) > 0 { chapterLastScene = sids[len(sids)-1] break } } if chapterLastScene != 0 { for _, seq := range seqs { chapterLastSceneByQuestId[seq.QuestId] = chapterLastScene } } } firstClearRewardsByGroupId := make(map[int32][]EntityMQuestFirstClearRewardGroup, len(firstClearRewards)) for _, reward := range firstClearRewards { firstClearRewardsByGroupId[reward.QuestFirstClearRewardGroupId] = append( firstClearRewardsByGroupId[reward.QuestFirstClearRewardGroupId], reward) } replayFlowRewardsByGroupId := make(map[int32][]EntityMQuestReplayFlowRewardGroup, len(replayFlowRewards)) for _, reward := range replayFlowRewards { replayFlowRewardsByGroupId[reward.QuestReplayFlowRewardGroupId] = append( replayFlowRewardsByGroupId[reward.QuestReplayFlowRewardGroupId], reward) } firstClearRewardSwitchesByQuestId := make(map[int32][]EntityMQuestFirstClearRewardSwitch, len(firstClearSwitches)) for _, switchRow := range firstClearSwitches { firstClearRewardSwitchesByQuestId[switchRow.QuestId] = append( firstClearRewardSwitchesByQuestId[switchRow.QuestId], switchRow) } missionRewardsByMissionId := make(map[int32][]EntityMQuestMissionReward, len(missionRewards)) for _, reward := range missionRewards { missionRewardsByMissionId[reward.QuestMissionRewardId] = append( missionRewardsByMissionId[reward.QuestMissionRewardId], reward) } weaponIdsByReleaseConditionGroupId := make(map[int32][]int32) for _, w := range weaponById { if w.WeaponStoryReleaseConditionGroupId != 0 { weaponIdsByReleaseConditionGroupId[w.WeaponStoryReleaseConditionGroupId] = append( weaponIdsByReleaseConditionGroupId[w.WeaponStoryReleaseConditionGroupId], w.WeaponId) } } releaseConditionsByGroupId := make(map[int32][]EntityMWeaponStoryReleaseConditionGroup) for _, c := range releaseConditions { releaseConditionsByGroupId[c.WeaponStoryReleaseConditionGroupId] = append( releaseConditionsByGroupId[c.WeaponStoryReleaseConditionGroupId], c) } sceneGrantsBySceneId := make(map[int32][]EntityMUserQuestSceneGrantPossession) for _, sg := range sceneGrants { sceneGrantsBySceneId[sg.QuestSceneId] = append(sceneGrantsBySceneId[sg.QuestSceneId], sg) } battleDropRewardById := make(map[int32]EntityMBattleDropReward, len(battleDropRewards)) for _, bdr := range battleDropRewards { battleDropRewardById[bdr.BattleDropRewardId] = bdr } pickupRewardIdsByGroupId := make(map[int32][]int32) for _, pg := range pickupRewardGroups { pickupRewardIdsByGroupId[pg.QuestPickupRewardGroupId] = append( pickupRewardIdsByGroupId[pg.QuestPickupRewardGroupId], pg.BattleDropRewardId) } battleGroupBySceneId := make(map[int32]int32, len(sceneBattles)) for _, sb := range sceneBattles { battleGroupBySceneId[sb.QuestSceneId] = sb.BattleGroupId } battleIdsByGroupId := make(map[int32][]int32) for _, bg := range battleGroups { battleIdsByGroupId[bg.BattleGroupId] = append(battleIdsByGroupId[bg.BattleGroupId], bg.BattleId) } type npcDeckKey struct { BattleNpcId int64 DeckType int32 BattleNpcDeckNumber int32 } npcDeckByKey := make(map[npcDeckKey]EntityMBattleNpcDeck, len(npcDecks)) for _, d := range npcDecks { npcDeckByKey[npcDeckKey{d.BattleNpcId, d.DeckType, d.BattleNpcDeckNumber}] = d } battleByIdMap := make(map[int32]EntityMBattle, len(battles)) for _, b := range battles { battleByIdMap[b.BattleId] = b } type dropCatKey struct { BattleNpcId int64 Uuid string } dropCategoryByKey := make(map[dropCatKey]int32, len(npcDropCategories)) for _, dc := range npcDropCategories { dropCategoryByKey[dropCatKey{dc.BattleNpcId, dc.BattleNpcDeckCharacterUuid}] = dc.BattleDropCategoryId } battleDropsByQuestId := make(map[int32][]BattleDropInfo) for questId := range questById { sids := sceneIdsByQuestId[questId] seen := make(map[BattleDropInfo]bool) var drops []BattleDropInfo for _, sceneId := range sids { groupId, ok := battleGroupBySceneId[sceneId] if !ok { continue } for _, battleId := range battleIdsByGroupId[groupId] { b, ok := battleByIdMap[battleId] if !ok { continue } dk := npcDeckKey{b.BattleNpcId, b.DeckType, b.BattleNpcDeckNumber} deck, ok := npcDeckByKey[dk] if !ok { continue } for _, uuid := range []string{deck.BattleNpcDeckCharacterUuid01, deck.BattleNpcDeckCharacterUuid02, deck.BattleNpcDeckCharacterUuid03} { if uuid == "" { continue } catId, ok := dropCategoryByKey[dropCatKey{b.BattleNpcId, uuid}] if !ok { continue } info := BattleDropInfo{QuestSceneId: sceneId, BattleDropCategoryId: catId} if !seen[info] { seen[info] = true drops = append(drops, info) } } } } if len(drops) > 0 { battleDropsByQuestId[questId] = drops } } rentalBattleGroups := make(map[int32]bool, len(rentalDecks)) for _, rd := range rentalDecks { rentalBattleGroups[rd.BattleGroupId] = true } rentalQuestIds := make(map[int32]bool) for questId := range questById { for _, sceneId := range sceneIdsByQuestId[questId] { if groupId, ok := battleGroupBySceneId[sceneId]; ok && rentalBattleGroups[groupId] { rentalQuestIds[questId] = true break } } } return &QuestCatalog{ SceneById: sceneById, MissionById: missionById, QuestById: questById, MissionIdsByQuestId: missionIdsByQuestId, RouteIdByQuestId: routeIdByQuestId, SceneIdsByQuestId: sceneIdsByQuestId, OrderedQuestIds: orderedQuestIds, FirstClearRewardsByGroupId: firstClearRewardsByGroupId, FirstClearRewardSwitchesByQuestId: firstClearRewardSwitchesByQuestId, MissionRewardsByMissionId: missionRewardsByMissionId, WeaponIdsByReleaseConditionGroupId: weaponIdsByReleaseConditionGroupId, ReleaseConditionsByGroupId: releaseConditionsByGroupId, SceneGrantsBySceneId: sceneGrantsBySceneId, BattleDropRewardById: battleDropRewardById, PickupRewardIdsByGroupId: pickupRewardIdsByGroupId, BattleDropsByQuestId: battleDropsByQuestId, ReplayFlowRewardsByGroupId: replayFlowRewardsByGroupId, RentalQuestIds: rentalQuestIds, TutorialUnlockConditions: tutorialUnlockConds, ChapterLastSceneByQuestId: chapterLastSceneByQuestId, SeasonIdByRouteId: seasonIdByRouteId, QuestsWithDifficulty: questsWithDifficulty, BattleOnlyTargetSceneByQuestId: battleOnlyTargetSceneByQuestId, UserExpThresholds: BuildExpThresholds(paramMapRows, 1), CharacterExpThresholds: BuildExpThresholds(paramMapRows, 31), CostumeExpByRarity: costumeExpByRarity, CostumeMaxLevelByRarity: costumeMaxLevelByRarity, MaxStaminaByLevel: maxStaminaByLevel, CostumeById: costumeById, WeaponById: weaponById, WeaponSkillSlots: skillSlots, WeaponAbilitySlots: abilitySlots, PartsCatalog: partsCatalog, }, nil } func (q *QuestCatalog) BattleOnlyTargetSceneIdFor(questId int32) (int32, bool) { v, ok := q.BattleOnlyTargetSceneByQuestId[questId] return v, ok } func (q *QuestCatalog) QuestHasDifficulty(questId int32) bool { return q.QuestsWithDifficulty[questId] }