package gameserver import ( "encoding/json" "fmt" "os" "sort" "sync" "gac-hkrpg-go/internal/common" goproto "gac-hkrpg-go/protocol/proto" ) // MissionUnlocks mirrors mission_unlocks.json (loaded once). type MissionUnlocks struct { FuncUnlockMissions []uint32 `json:"funcUnlockMissions"` AllMainMissions []uint32 `json:"allMainMissions"` TeleportIDs []uint32 `json:"teleportIds"` EntranceIDs []uint32 `json:"entranceIds"` RaidIDs []uint32 `json:"raidIds"` ChallengeRaidIDs []uint32 `json:"challengeRaidIds"` NpcIDs []uint32 `json:"npcIds"` SubMissionIDs []uint32 `json:"subMissionIds"` QuestIDs []uint32 `json:"questIds"` } var ( missionUnlocksOnce sync.Once missionUnlocks MissionUnlocks missionCompatOnce sync.Once missionCompatIDs map[uint32]struct{} missionCompatErr error ) type missionCompatibility struct { UnsupportedSubMissions map[string][]uint32 `json:"unsupported_sub_mission_ids_by_client_version"` } func excludedSubMissionsForVersion(raw []byte, clientVersion string) (map[uint32]struct{}, error) { var compatibility missionCompatibility if err := json.Unmarshal(raw, &compatibility); err != nil { return nil, fmt.Errorf("parse MissionCompatibility.json: %w", err) } excluded := make(map[uint32]struct{}) for _, id := range compatibility.UnsupportedSubMissions[clientVersion] { if id == 0 { return nil, fmt.Errorf("MissionCompatibility.json contains zero sub-mission ID for Client %s", clientVersion) } if _, exists := excluded[id]; exists { return nil, fmt.Errorf("MissionCompatibility.json contains duplicate sub-mission ID %d for Client %s", id, clientVersion) } excluded[id] = struct{}{} } return excluded, nil } func excludedSubMissionsForConfiguredClient() (map[uint32]struct{}, error) { missionCompatOnce.Do(func() { cfg, err := common.LoadRuntimeConfig() if err != nil { missionCompatErr = err return } raw, err := common.ReadResourceFile("resources/MissionCompatibility.json", "MissionCompatibility.json") if err != nil { missionCompatErr = err return } missionCompatIDs, missionCompatErr = excludedSubMissionsForVersion(raw, cfg.ClientVersion) }) return missionCompatIDs, missionCompatErr } func getMissionUnlocks() *MissionUnlocks { missionUnlocksOnce.Do(func() { raw, err := os.ReadFile("mission_unlocks.json") if err != nil { Log("mission_unlocks.json not found: " + err.Error()) return } if err := json.Unmarshal(raw, &missionUnlocks); err != nil { Log("Failed to parse mission_unlocks.json: " + err.Error()) missionUnlocks = MissionUnlocks{} return } Log("Loaded mission_unlocks.json successfully") }) return &missionUnlocks } func init() { handle("GetMissionStatus", func(s *Session, req *goproto.GetMissionStatusCsReq, rsp *goproto.GetMissionStatusScRsp) error { rsp.Retcode = 0 unlocks := getMissionUnlocks() // main missions already finished: res.json + funcUnlockMissions + client-requested. // Without these the client Lua locks the HUD, teleports and Endgame. seen := map[uint32]bool{} var finished []uint32 add := func(ids []uint32) { for _, id := range ids { if id != 0 && !seen[id] { seen[id] = true finished = append(finished, id) } } } if res, err := common.GameRes(); err == nil { for _, m := range res.LevelOutputConfigs { for _, sc := range m { for _, grp := range sc.Scenes { add(grp.FinishedMainMissions) } } } } add(unlocks.FuncUnlockMissions) add(req.MainMissionIdList) sort.Slice(finished, func(i, j int) bool { return finished[i] < finished[j] }) rsp.FinishedMainMissionIdList = finished // beta client re-asks when this field is missing (tag 13) rsp.CurversionFinishedMainMissionIdList = finished for _, id := range req.SubMissionIdList { rsp.SubMissionStatusList = append(rsp.SubMissionStatusList, &goproto.Mission{ Id: id, Progress: 1, Status: goproto.MissionStatus_MISSION_FINISH, }) } SceneDebugLog("GET_MISSION_STATUS main_req=%d sub_req=%d finished_sent=%d", len(req.MainMissionIdList), len(req.SubMissionIdList), len(rsp.FinishedMainMissionIdList)) return nil }) handle("GetMissionData", func(s *Session, req *goproto.GetMissionDataCsReq, rsp *goproto.GetMissionDataScRsp) error { rsp.Retcode = 0 unlocks := getMissionUnlocks() seen := map[uint32]bool{} var finished []uint32 for _, id := range unlocks.FuncUnlockMissions { if id != 0 && !seen[id] { seen[id] = true finished = append(finished, id) } } sort.Slice(finished, func(i, j int) bool { return finished[i] < finished[j] }) for _, id := range finished { rsp.MainMissionList = append(rsp.MainMissionList, &goproto.MainMission{ Id: id, Status: goproto.MissionStatus_MISSION_FINISH, }) } rsp.FinishedMainMissionIdList = finished // The Client's train unlock-data callback reads sub-mission rows from // GetMissionData. Sending only MainMissionList leaves those lookups // absent and can throw in TrainPartyModule while the train scene loads. excludedSubMissions, err := excludedSubMissionsForConfiguredClient() if err != nil { return err } seenSubMissions := make(map[uint32]struct{}, len(unlocks.SubMissionIDs)) excludedCount := 0 for _, id := range unlocks.SubMissionIDs { if id == 0 { continue } if _, excluded := excludedSubMissions[id]; excluded { excludedCount++ continue } if _, exists := seenSubMissions[id]; exists { continue } seenSubMissions[id] = struct{}{} rsp.MissionList = append(rsp.MissionList, &goproto.Mission{ Id: id, Progress: 1, Status: goproto.MissionStatus_MISSION_FINISH, }) } SceneDebugLog("GET_MISSION_DATA finished_main_sent=%d sub_missions_sent=%d sub_missions_excluded=%d", len(rsp.FinishedMainMissionIdList), len(rsp.MissionList), excludedCount) return nil }) handle("GetQuestData", func(s *Session, req *goproto.GetQuestDataCsReq, rsp *goproto.GetQuestDataScRsp) error { rsp.Retcode = 0 unlocks := getMissionUnlocks() for _, id := range unlocks.QuestIDs { rsp.QuestList = append(rsp.QuestList, &goproto.Quest{ Id: id, Status: goproto.QuestStatus_QuestStatus_QuestFinish, Progress: 1, }) } SceneDebugLog("GET_QUEST_DATA quests_sent=%d", len(rsp.QuestList)) return nil }) // client Lua (_GetActiveGroupInfoByMissionCustomValue) reads this response; // dropping it blocks input. handle("GetMainMissionCustomValue", func(s *Session, req *goproto.GetMainMissionCustomValueCsReq, rsp *goproto.GetMainMissionCustomValueScRsp) error { rsp.Retcode = 0 for _, id := range req.SubMissionIdList { rsp.MainMissionList = append(rsp.MainMissionList, &goproto.MainMission{ Id: id, Status: goproto.MissionStatus_MISSION_FINISH, }) } SceneDebugLog("GET_MAIN_MISSION_CUSTOM_VALUE ids=%d", len(req.SubMissionIdList)) return nil }) handle("UpdateTrackMainMission", func(s *Session, req *goproto.UpdateTrackMainMissionCsReq, rsp *goproto.UpdateTrackMainMissionScRsp) error { rsp.Retcode = 0 rsp.TrackMissionId = req.TrackMissionId return nil }) handle("GetNpcMessageGroup", func(s *Session, req *goproto.GetNpcMessageGroupCsReq, rsp *goproto.GetNpcMessageGroupScRsp) error { rsp.Retcode = 0 return nil }) }