package gameserver import ( "encoding/json" "fmt" "gac-hkrpg-go/internal/common" "sort" "sync" goproto "gac-hkrpg-go/protocol/proto" ) // ChallengeDataConfig mirrors challenge_data.json (Rust: ChallengeConfigData). type ChallengeDataConfig struct { TargetRules map[uint32]ChallengeTargetRule `json:"-"` Countdowns map[uint32]uint32 `json:"-"` StagesList []uint32 `json:"stages_list"` GroupsList []uint32 `json:"groups_list"` Challenges map[uint32]ChallengeStageData `json:"challenges"` Tierce map[uint32]ChallengeTierceStageData `json:"tierce"` Stages map[uint32]StageBattleData `json:"stages"` Peak map[string]PeakStageData `json:"peak"` } type ChallengeStageData struct { GroupID uint32 `json:"group_id"` Entrance uint32 `json:"entrance"` Entrance2 uint32 `json:"entrance2"` Group1 uint32 `json:"group1"` Group2 uint32 `json:"group2"` Monster1 uint32 `json:"monster1"` Monster2 uint32 `json:"monster2"` Event1 uint32 `json:"event1"` Event2 uint32 `json:"event2"` Buff uint32 `json:"buff"` } type ChallengeTargetRule struct { ID uint32 `json:"ID"` Type uint32 `json:"ChallengeTargetType"` Param uint32 `json:"ChallengeTargetParam1"` } type ChallengeTierceStageData struct { SpecialTarget uint32 `json:"-"` ClearScore uint32 `json:"-"` PreChallengeID uint32 `json:"-"` Entrance uint32 `json:"entrance"` Group uint32 `json:"group"` Monster uint32 `json:"monster"` Event uint32 `json:"event"` Targets []uint32 `json:"targets"` Score uint32 `json:"score"` } type StageBattleData struct { Level uint32 `json:"level"` Monsters [][]uint32 `json:"monsters"` } type PeakStageData struct { SceneResourceError string `json:"-"` Entrance uint32 `json:"-"` Group uint32 `json:"-"` GroupID uint32 `json:"-"` Buffs []uint32 `json:"-"` HardBuffs []uint32 `json:"-"` EventID uint32 `json:"event_id"` HardEventID uint32 `json:"hard_event_id"` MonsterID uint32 `json:"monster_id"` DefaultBuff uint32 `json:"default_buff"` IsBoss bool `json:"is_boss"` Targets []uint32 `json:"targets"` HardTarget uint32 `json:"hard_target"` } var ( challengeOnce sync.Once challengeData ChallengeDataConfig ) func getChallengeData() *ChallengeDataConfig { challengeOnce.Do(func() { raw, err := common.ReadResourceFile("challenge_data.json") if err != nil { Log("challenge_data.json not found!") return } if err := json.Unmarshal(raw, &challengeData); err != nil { Log("Failed to parse challenge_data.json: " + err.Error()) challengeData = ChallengeDataConfig{} } // The imported catalog stores Peak rows under peak.stages; the flat // resource overlays below add Client-specific placement/group data. // Do not turn the section names "stages" and "groups" into fake PeakIDs. var imported struct { Peak struct { Stages map[string]PeakStageData `json:"stages"` } `json:"peak"` } if json.Unmarshal(raw, &imported) == nil && len(imported.Peak.Stages) != 0 { challengeData.Peak = imported.Peak.Stages } challengeData.Countdowns = map[uint32]uint32{} challengeData.TargetRules = map[uint32]ChallengeTargetRule{} if raw, err := common.ReadResourceFile("resources/ChallengeTargetConfig.json"); err == nil { var table struct { Entries []ChallengeTargetRule `json:"challenge_targets"` } if json.Unmarshal(raw, &table) == nil { for _, entry := range table.Entries { challengeData.TargetRules[entry.ID] = entry } } } if challengeData.Challenges == nil { challengeData.Challenges = map[uint32]ChallengeStageData{} } if challengeData.Tierce == nil { challengeData.Tierce = map[uint32]ChallengeTierceStageData{} } if challengeData.Stages == nil { challengeData.Stages = map[uint32]StageBattleData{} } if raw, err := common.ReadResourceFile("resources/ChallengeMazeConfig.json"); err == nil { var table struct { Entries []struct { ID uint32 `json:"ID"` Countdown uint32 `json:"ChallengeCountDown"` GroupID uint32 `json:"GroupID"` Entrance uint32 `json:"MapEntranceID"` Entrance2 uint32 `json:"MapEntranceID2"` Group1 uint32 `json:"MazeGroupID1"` Group2 uint32 `json:"MazeGroupID2"` Monsters1 []uint32 `json:"NpcMonsterIDList1"` Monsters2 []uint32 `json:"NpcMonsterIDList2"` Events1 []uint32 `json:"EventIDList1"` Events2 []uint32 `json:"EventIDList2"` Buff uint32 `json:"MazeBuffID"` } `json:"challenge_config"` } if err := json.Unmarshal(raw, &table); err == nil { for _, entry := range table.Entries { challengeData.Countdowns[entry.ID] = entry.Countdown challengeData.Challenges[entry.ID] = ChallengeStageData{GroupID: entry.GroupID, Entrance: entry.Entrance, Entrance2: entry.Entrance2, Group1: entry.Group1, Group2: entry.Group2, Monster1: lastResourceID(entry.Monsters1), Monster2: lastResourceID(entry.Monsters2), Event1: lastResourceID(entry.Events1), Event2: lastResourceID(entry.Events2), Buff: entry.Buff} } } } if raw, err := common.ReadResourceFile("resources/ChallengeMazeTierceConfig.json"); err == nil { var table struct { Entries []struct { ID uint32 `json:"ID"` PreChallengeID uint32 `json:"PreChallengeMazeID"` Entrance uint32 `json:"MapEntranceID"` Group uint32 `json:"MazeGroupID"` Monsters []uint32 `json:"NpcMonsterIDList"` Events []uint32 `json:"EventIDList"` Targets []uint32 `json:"ChallengeTargetID"` SpecialTarget uint32 `json:"ChallengeTierceTargetID"` ClearScore uint32 `json:"IDBJENCBJHM"` } `json:"challenge_tierce_config"` } if err := json.Unmarshal(raw, &table); err == nil { for _, entry := range table.Entries { value := challengeData.Tierce[entry.ID] value.PreChallengeID, value.Entrance, value.Group = entry.PreChallengeID, entry.Entrance, entry.Group value.SpecialTarget, value.ClearScore = entry.SpecialTarget, entry.ClearScore value.Monster, value.Event, value.Targets = lastResourceID(entry.Monsters), lastResourceID(entry.Events), entry.Targets challengeData.Tierce[entry.ID] = value } } } if raw, err := common.ReadResourceFile("resources/StageConfig.json"); err == nil { var table struct { Entries []struct { ID uint32 `json:"StageID"` Level uint32 `json:"Level"` Monsters [][]uint32 `json:"MonsterList"` } `json:"stage_config"` } if err := json.Unmarshal(raw, &table); err == nil { for _, entry := range table.Entries { challengeData.Stages[entry.ID] = StageBattleData{Level: entry.Level, Monsters: entry.Monsters} } } } loadPeakResources(&challengeData) }) return &challengeData } func loadPeakResources(data *ChallengeDataConfig) { if data.Peak == nil { data.Peak = map[string]PeakStageData{} } if raw, err := common.ReadResourceFile("resources/ChallengePeakConfig.json"); err == nil { var table struct { Rows []struct { ID uint32 `json:"ID"` Entrance uint32 `json:"MapEntranceID"` Group uint32 `json:"MazeGroupID"` Events []uint32 `json:"EventIDList"` Monsters []uint32 `json:"NpcMonsterIDList"` Buffs []uint32 `json:"TagList"` Targets []uint32 `json:"NormalTargetList"` } `json:"challenge_peak_config"` } if err := json.Unmarshal(raw, &table); err == nil { for _, row := range table.Rows { value := data.Peak[itoa(int(row.ID))] value.Entrance, value.Group, value.EventID, value.MonsterID = row.Entrance, row.Group, lastResourceID(row.Events), lastResourceID(row.Monsters) value.Buffs, value.Targets = row.Buffs, row.Targets data.Peak[itoa(int(row.ID))] = value } } } if raw, err := common.ReadResourceFile("resources/ChallengePeakBossConfig.json"); err == nil { var table struct { Rows []struct { ID uint32 `json:"ID"` Events []uint32 `json:"HardEventIDList"` Buffs []uint32 `json:"HardTagList"` Target uint32 `json:"HardTarget"` } `json:"challenge_peak_boss_config"` } if err := json.Unmarshal(raw, &table); err == nil { for _, row := range table.Rows { value := data.Peak[itoa(int(row.ID))] value.IsBoss = true value.HardEventID, value.HardBuffs, value.HardTarget = lastResourceID(row.Events), row.Buffs, row.Target data.Peak[itoa(int(row.ID))] = value } } } if raw, err := common.ReadResourceFile("resources/ChallengePeakGroupConfig.json"); err == nil { var table struct { Rows []struct { ID uint32 `json:"ID"` Pre []uint32 `json:"PreLevelIDList"` Boss uint32 `json:"BossLevelID"` } `json:"challenge_peak_group_config"` } if err := json.Unmarshal(raw, &table); err == nil { for _, row := range table.Rows { for _, id := range append(row.Pre, row.Boss) { key := itoa(int(id)) value := data.Peak[key] value.GroupID = row.ID data.Peak[key] = value } } } } applyPeakSceneOverrides(data) } func lastResourceID(ids []uint32) uint32 { if len(ids) == 0 { return 0 } return ids[len(ids)-1] } func getChallengeStagesList() []uint32 { cd := getChallengeData() if len(cd.StagesList) > 0 { return cd.StagesList } var stages []uint32 for id := range cd.Challenges { stages = append(stages, id) } for id := range cd.Tierce { stages = append(stages, id) } sortU32Slice(stages) // dedup out := stages[:0] var last uint32 = ^uint32(0) for _, id := range stages { if id != last { out = append(out, id) last = id } } return out } func getChallengeGroups() []uint32 { cd := getChallengeData() if len(cd.GroupsList) > 0 { return cd.GroupsList } var groups []uint32 for _, c := range cd.Challenges { if c.GroupID != 0 { groups = append(groups, c.GroupID) } } sortU32Slice(groups) out := groups[:0] var last uint32 = ^uint32(0) for _, g := range groups { if g != last { out = append(out, g) last = g } } return out } func groupTotalStars(groupID uint32) uint64 { cd := getChallengeData() n := uint64(0) for _, c := range cd.Challenges { if c.GroupID == groupID { n++ } } return n * 3 } func challengeScore(id uint32) uint32 { switch { case id >= 20000 && id < 30000: return 80000 case id >= 30000 || (id >= 5000 && id < 6000): return 4000 } return 0 } func init() { handle("GetChallenge", func(s *Session, req *goproto.GetChallengeCsReq, rsp *goproto.GetChallengeScRsp) error { for _, groupID := range getChallengeGroups() { rsp.ChallengeGroupList = append(rsp.ChallengeGroupList, &goproto.ChallengeGroup{ GroupId: groupID, TakenStarsCountReward: groupTotalStars(groupID), }) } for _, id := range getChallengeStagesList() { rsp.ChallengeList = append(rsp.ChallengeList, &goproto.Challenge{ ChallengeId: id, Star: 7, TakenReward: 7, RecordId: 1, HGPKMHFPMBJ: false, ScoreTwo: challengeScore(id), ScoreId: challengeScore(id), }) } addMaxLevel := func(level, rType uint32, unlocked bool) { rsp.MaxLevelList = append(rsp.MaxLevelList, &goproto.ChallengeHistoryMaxLevel{ Level: level, HNHCFJJNJCE: unlocked, RewardDisplayType: rType, }) } addMaxLevel(15, 1, true) // Jarilo addMaxLevel(15, 1, false) addMaxLevel(6, 2, true) // Luofu addMaxLevel(6, 2, false) addMaxLevel(12, 3, true) // MoC addMaxLevel(12, 3, false) addMaxLevel(4, 4, true) // Pure Fiction addMaxLevel(4, 4, false) addMaxLevel(4, 5, true) // Apocalyptic Shadow addMaxLevel(4, 5, false) addMaxLevel(6, 6, true) // Anomaly Arbitration addMaxLevel(6, 6, false) for r := uint32(101001); r <= 101015; r++ { addMaxLevel(15, r, true) } for r := uint32(101016); r <= 101021; r++ { addMaxLevel(6, r, true) } for r := uint32(101201); r <= 101212; r++ { addMaxLevel(12, r, true) } for r := uint32(101401); r <= 101404; r++ { addMaxLevel(4, r, true) } for _, r := range []uint32{101713, 101913, 102113} { addMaxLevel(4, r, true) } return nil }) handle("GetCurChallenge", func(s *Session, req *goproto.GetCurChallengeCsReq, rsp *goproto.GetCurChallengeScRsp) error { rsp.Retcode = 0 if run := s.ChallengeRun; run != nil && !run.Tierce && !run.Peak { rsp.CurChallenge = run.Cur rsp.LineupList = []*goproto.LineupInfo{run.activeLineup(0)} } return nil }) handle("GetChallengePeakData", func(s *Session, req *goproto.GetChallengePeakDataCsReq, rsp *goproto.GetChallengePeakDataScRsp) error { rsp.Retcode = 0 cd := getChallengeData() type stageEntry struct { id uint32 stage PeakStageData } groups := map[uint32][]stageEntry{} for k, stage := range cd.Peak { var id uint32 if _, err := fmt.Sscanf(k, "%d", &id); err != nil { continue } g := stage.GroupID if g == 0 { continue } groups[g] = append(groups[g], stageEntry{id, stage}) } if len(groups) == 0 { // Never replace a missing catalog with a fabricated round: doing so // silently discards a valid Srtool choice and advertises wrong rooms. rsp.Retcode = uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST) PacketLog("PEAK_DATA rejected: no resource-backed AA groups") return nil } groupIDs := sortedU32Keys2(groups) if len(groupIDs) != 0 { rsp.CurrentPeakGroupId = groupIDs[0] } if selection, err := common.ReadAARoundSelection(); err != nil { SceneDebugLog("[PEAK] failed to read Srtool AA round selection: %v", err) } else if selection != nil { if _, exists := groups[selection.PeakGroupID]; exists { rsp.CurrentPeakGroupId = selection.PeakGroupID SceneDebugLog("[PEAK] Srtool selected AA round: group=%d", selection.PeakGroupID) } else { SceneDebugLog("[PEAK] Srtool AA round %d is missing from the loaded Client challenge data", selection.PeakGroupID) } } // Selection changes made in Srtool apply after leaving the existing // challenge. A data refresh during reconnect must not change the active // round underneath a running/settled Peak session. if run := s.ChallengeRun; run != nil && run.Peak { if stage, exists := cd.Peak[itoa(int(run.ID))]; exists && stage.GroupID != 0 { rsp.CurrentPeakGroupId = stage.GroupID } } PacketLog(fmt.Sprintf("PEAK_DATA current_group=%d groups=%d active_run=%t", rsp.CurrentPeakGroupId, len(groups), s.ChallengeRun != nil)) for _, groupID := range groupIDs { stages := groups[groupID] sort.Slice(stages, func(i, j int) bool { return stages[i].id < stages[j].id }) peakGroup := &goproto.ChallengePeakGroup{ PeakGroupId: groupID, CountOfPeaks: uint32(len(stages)), } for _, entry := range stages { stars := uint32(len(entry.stage.Targets)) peakGroup.ObtainedStars += stars for t := uint32(1); t <= stars; t++ { peakGroup.TakenStarRewards = append(peakGroup.TakenStarRewards, peakGroup.ObtainedStars-stars+t) } peakGroup.Peaks = append(peakGroup.Peaks, &goproto.ChallengePeak{ PeakId: entry.id, HasPassed: true, FinishedTargetList: entry.stage.Targets, }) } rsp.ChallengePeakGroups = append(rsp.ChallengePeakGroups, peakGroup) } return nil }) handle("GetCurChallengePeak", func(s *Session, req *goproto.GetCurChallengePeakCsReq, rsp *goproto.GetCurChallengePeakScRsp) error { rsp.Retcode = 0 if run := s.ChallengeRun; run != nil && run.Peak { rsp.PeakId, rsp.BossBuffId, rsp.HasPassed = run.ID, run.PeakBuff, run.Finished && run.Win } return nil }) handle("SetChallengePeakBossHardMode", func(s *Session, req *goproto.SetChallengePeakBossHardModeCsReq, rsp *goproto.SetChallengePeakBossHardModeScRsp) error { if s.PeakHardModes == nil { s.PeakHardModes = map[uint32]bool{} } s.PeakHardModes[req.PeakGroupId] = req.IsHardMode rsp.PeakGroupId, rsp.IsHardMode = req.PeakGroupId, req.IsHardMode return nil }) handle("ConfirmChallengePeakSettle", func(s *Session, req *goproto.ConfirmChallengePeakSettleCsReq, rsp *goproto.ConfirmChallengePeakSettleScRsp) error { rsp.PeakId, rsp.IOBCIMIPEOM = req.PeakId, req.IOBCIMIPEOM return nil }) handle("GetActivityScheduleConfig", func(s *Session, req *goproto.GetActivityScheduleConfigCsReq, rsp *goproto.GetActivityScheduleConfigScRsp) error { cfg, err := common.LoadRuntimeConfig() if err != nil { return err } cw, err := common.LoadCurrencyWarsCatalog() if err != nil { return err } rsp.Retcode = 0 add := func(activityID, panelID uint32) { rsp.ScheduleData = append(rsp.ScheduleData, &goproto.ActivityScheduleData{ ActivityId: activityID, PanelId: panelID, BeginTime: *cfg.ActivitySchedule.BeginTime, EndTime: *cfg.ActivitySchedule.EndTime, }) } // Advertise only the supported Endgame schedules. Exposing the full // ActivityConfig before implementing its activity-data handlers makes // the Client request unavailable state and can stall the events page. // Apocalyptic Shadow tab requires an active schedule with panel 21001 for actID := uint32(21001); actID <= 21030; actID++ { add(actID, 21001) } for i := uint32(1); i <= 8; i++ { add(2100000+i*100+1, 21001) } for _, schedule := range cw.ActivitySchedules() { add(schedule.ModuleID, schedule.ActivityID) } previews, err := common.LoadPreviewActivitySchedules() if err != nil { return err } for _, schedule := range previews { add(schedule.ModuleID, schedule.PanelID) } return nil }) }