package gameserver import ( "encoding/json" "fmt" "path/filepath" "sort" "strconv" "strings" "sync" "gac-hkrpg-go/internal/common" goproto "gac-hkrpg-go/protocol/proto" ) type prydwenEndgameSnapshot struct { Source struct { DatasetVersion string `json:"dataset_version"` CollectDate string `json:"collect_date"` } `json:"source"` Protocol struct { RequestTypeRoutes map[string]string `json:"request_type_routes"` } `json:"protocol"` AvatarAliases map[string]string `json:"avatar_aliases"` Modes map[string]prydwenModeSnapshot `json:"modes"` } type prydwenModeSnapshot struct { ChallengeVersion string `json:"challenge_version"` ChallengeName string `json:"challenge_name"` Floor uint32 `json:"floor"` Stages []prydwenStageSnapshot `json:"stages"` } type prydwenStageSnapshot struct { Stage string `json:"stage"` Rank uint32 `json:"rank"` Characters []string `json:"characters"` } type prydwenAvatarResource struct { ID uint32 `json:"AvatarID"` VOTag string `json:"AvatarVOTag"` ConfigPath string `json:"JsonPath"` } type challengePeakGroupResource struct { ID uint32 `json:"ID"` RecommendID uint32 `json:"RecommendID"` PreLevelIDList []uint32 `json:"PreLevelIDList"` BossLevelID uint32 `json:"BossLevelID"` } var ( prydwenOnce sync.Once prydwenSnapshot prydwenEndgameSnapshot prydwenLoadErr error ) func normalizedAvatarName(name string) string { name = strings.ToLower(name) return strings.Map(func(r rune) rune { if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') { return r } return -1 }, name) } func loadPrydwenSnapshot() (*prydwenEndgameSnapshot, error) { prydwenOnce.Do(func() { prydwenSnapshot, prydwenLoadErr = readPrydwenSnapshot() }) if prydwenLoadErr != nil { return nil, prydwenLoadErr } return &prydwenSnapshot, nil } func readPrydwenSnapshot() (prydwenEndgameSnapshot, error) { var snapshot prydwenEndgameSnapshot raw, err := common.ReadResourceFile("resources/PrydwenEndgameTeams.json") if err != nil { return snapshot, fmt.Errorf("read Prydwen endgame team snapshot: %w", err) } if err := json.Unmarshal(raw, &snapshot); err != nil { return snapshot, fmt.Errorf("parse Prydwen endgame team snapshot: %w", err) } for _, mode := range []string{"moc", "pf", "as", "aa"} { entry, ok := snapshot.Modes[mode] if !ok || len(entry.Stages) == 0 { return snapshot, fmt.Errorf("Prydwen snapshot has no %s teams", mode) } } if len(snapshot.Protocol.RequestTypeRoutes) == 0 { return snapshot, fmt.Errorf("Prydwen snapshot has no request type routing table") } return snapshot, nil } func readPrydwenAvatarIDs(aliases map[string]string) (map[string]uint32, error) { avatarRaw, err := common.ReadResourceFile("resources/AvatarConfig.json") if err != nil { return nil, fmt.Errorf("read Client AvatarConfig resource: %w", err) } var avatarTable struct { Entries []prydwenAvatarResource `json:"avatar_config"` } if err := json.Unmarshal(avatarRaw, &avatarTable); err != nil { return nil, fmt.Errorf("parse Client AvatarConfig resource: %w", err) } avatarIDs := make(map[string]uint32, len(avatarTable.Entries)*2) for _, avatar := range avatarTable.Entries { if avatar.ID == 0 { continue } if tag := normalizedAvatarName(avatar.VOTag); tag != "" { avatarIDs[tag] = avatar.ID } pathName := strings.TrimSuffix(filepath.Base(avatar.ConfigPath), filepath.Ext(avatar.ConfigPath)) pathName = strings.TrimSuffix(strings.TrimPrefix(pathName, "Avatar_"), "_Config") if pathName = normalizedAvatarName(pathName); pathName != "" { avatarIDs[pathName] = avatar.ID } } for slug, clientTag := range aliases { if id := avatarIDs[normalizedAvatarName(clientTag)]; id != 0 { avatarIDs[normalizedAvatarName(slug)] = id } } return avatarIDs, nil } func prydwenStageLineup(stage prydwenStageSnapshot, avatarIDs map[string]uint32) (*goproto.ChallengeLineupList, error) { if stage.Stage == "" || len(stage.Characters) != 4 { return nil, fmt.Errorf("invalid Prydwen stage %q team size/order", stage.Stage) } avatars := make([]*goproto.ChallengeAvatarInfo, 0, len(stage.Characters)) seen := make(map[uint32]bool, len(stage.Characters)) for index, slug := range stage.Characters { id := avatarIDs[normalizedAvatarName(slug)] if id == 0 { return nil, fmt.Errorf("Prydwen avatar %q in stage %s has no ID in Client AvatarConfig", slug, stage.Stage) } if seen[id] { return nil, fmt.Errorf("Prydwen stage %s repeats AvatarID %d", stage.Stage, id) } seen[id] = true avatars = append(avatars, &goproto.ChallengeAvatarInfo{ AvatarType: goproto.AvatarType_AVATAR_FORMAL_TYPE, Id: id, Index: uint32(index), }) } return &goproto.ChallengeLineupList{AvatarList: avatars}, nil } func buildPrydwenLineups(mode prydwenModeSnapshot, avatarIDs map[string]uint32) ([]*goproto.ChallengeLineupList, error) { lineups := make([]*goproto.ChallengeLineupList, 0, len(mode.Stages)) stages := append([]prydwenStageSnapshot(nil), mode.Stages...) sort.SliceStable(stages, func(i, j int) bool { return stages[i].Stage < stages[j].Stage }) seen := map[string]bool{} for _, stage := range stages { if seen[stage.Stage] { return nil, fmt.Errorf("Prydwen snapshot repeats stage %s", stage.Stage) } seen[stage.Stage] = true lineup, err := prydwenStageLineup(stage, avatarIDs) if err != nil { return nil, err } lineups = append(lineups, lineup) } return lineups, nil } func readPrydwenLineups() ([]*goproto.ChallengeLineupList, error) { snapshot, err := loadPrydwenSnapshot() if err != nil { return nil, err } avatarIDs, err := readPrydwenAvatarIDs(snapshot.AvatarAliases) if err != nil { return nil, err } return buildPrydwenLineups(snapshot.Modes["moc"], avatarIDs) } func buildPrydwenPeakLineups(mode prydwenModeSnapshot, aliases map[string]string, requestKey uint32) (*goproto.MJMGDLJAJCM, error) { groupRaw, err := common.ReadResourceFile("resources/ChallengePeakGroupConfig.json") if err != nil { return nil, fmt.Errorf("read Client ChallengePeakGroupConfig resource: %w", err) } var groups struct { Entries []challengePeakGroupResource `json:"challenge_peak_group_config"` } if err := json.Unmarshal(groupRaw, &groups); err != nil { return nil, fmt.Errorf("parse Client ChallengePeakGroupConfig resource: %w", err) } if len(groups.Entries) == 0 { return nil, fmt.Errorf("Client ChallengePeakGroupConfig contains no groups") } // The key semantics are not established in the obfuscated schema. Use an // exact Client resource match if present; otherwise pair the bundled // current-cycle snapshot with the newest group in this Client's resources. selected := groups.Entries[0] for _, group := range groups.Entries { if group.ID > selected.ID { selected = group } } matched := false for _, group := range groups.Entries { if requestKey != 0 && (requestKey == group.ID || requestKey == group.RecommendID || containsPeakLevel(group, requestKey)) { selected = group matched = true break } } if requestKey != 0 && !matched { Log(fmt.Sprintf("[RECOMMEND] AA key=%d did not match a Client Peak resource; using latest configured group=%d", requestKey, selected.ID)) } levelIDs := append([]uint32(nil), selected.PreLevelIDList...) if selected.BossLevelID != 0 { levelIDs = append(levelIDs, selected.BossLevelID) } if len(levelIDs) != len(mode.Stages) { return nil, fmt.Errorf("Prydwen AA has %d teams but Client Peak group %d has %d levels", len(mode.Stages), selected.ID, len(levelIDs)) } avatarIDs, err := readPrydwenAvatarIDs(aliases) if err != nil { return nil, err } stages := append([]prydwenStageSnapshot(nil), mode.Stages...) sort.SliceStable(stages, func(i, j int) bool { return stages[i].Stage < stages[j].Stage }) peakLineups := make([]*goproto.OKFKPPNGCOD, 0, len(stages)) for i, stage := range stages { if stage.Stage == "" || len(stage.Characters) != 4 { return nil, fmt.Errorf("invalid Prydwen AA stage %q team size/order", stage.Stage) } avatars := make([]*goproto.ONKDAPOKGAC, 0, len(stage.Characters)) seen := map[uint32]bool{} for index, slug := range stage.Characters { id := avatarIDs[normalizedAvatarName(slug)] if id == 0 { return nil, fmt.Errorf("Prydwen avatar %q in AA stage %s has no ID in Client AvatarConfig", slug, stage.Stage) } if seen[id] { return nil, fmt.Errorf("Prydwen AA stage %s repeats AvatarID %d", stage.Stage, id) } seen[id] = true avatars = append(avatars, &goproto.ONKDAPOKGAC{ AvatarType: goproto.AvatarType_AVATAR_FORMAL_TYPE, Id: id, Index: uint32(index), }) } peakLineups = append(peakLineups, &goproto.OKFKPPNGCOD{PeakId: levelIDs[i], AvatarList: avatars}) } return &goproto.MJMGDLJAJCM{DNIGKDKIPPJ: peakLineups}, nil } func containsPeakLevel(group challengePeakGroupResource, key uint32) bool { if key == group.BossLevelID { return key != 0 } for _, levelID := range group.PreLevelIDList { if key == levelID { return true } } return false } func recommendationMode(snapshot *prydwenEndgameSnapshot, req *goproto.GetFriendRecommendLineupCsReq) (string, error) { typeKey := strconv.FormatInt(int64(req.GetType()), 10) mode := snapshot.Protocol.RequestTypeRoutes[typeKey] if mode == "" { return "", fmt.Errorf("unmapped GetFriendRecommendLineup type=%s", typeKey) } if _, ok := snapshot.Modes[mode]; !ok { return "", fmt.Errorf("GetFriendRecommendLineup type=%s routes to missing mode %q", typeKey, mode) } return mode, nil } func buildPrydwenRecommendResponse(req *goproto.GetFriendRecommendLineupCsReq) (*goproto.GetFriendRecommendLineupScRsp, error) { snapshot, err := loadPrydwenSnapshot() if err != nil { return nil, err } modeName, err := recommendationMode(snapshot, req) if err != nil { return nil, err } response := &goproto.GetFriendRecommendLineupScRsp{Key: req.Key, Type: req.Type} if modeName == "aa" { peakLineups, err := buildPrydwenPeakLineups(snapshot.Modes[modeName], snapshot.AvatarAliases, req.GetKey()) if err != nil { return nil, err } response.MGKCKHIIICL = []*goproto.FPDCINMDJCE{{ KJFEKHDEBIK: &goproto.FPDCINMDJCE_LMHIOKFMDNE{LMHIOKFMDNE: peakLineups}, }} return response, nil } avatarIDs, err := readPrydwenAvatarIDs(snapshot.AvatarAliases) if err != nil { return nil, err } lineups, err := buildPrydwenLineups(snapshot.Modes[modeName], avatarIDs) if err != nil { return nil, err } response.MGKCKHIIICL = []*goproto.FPDCINMDJCE{{ KJFEKHDEBIK: &goproto.FPDCINMDJCE_NHKCFOALAPD{ NHKCFOALAPD: &goproto.OJJJDPNOKHG{LineupList: lineups}, }, }} return response, nil } func init() { handle("GetFriendRecommendLineup", func(_ *Session, req *goproto.GetFriendRecommendLineupCsReq, rsp *goproto.GetFriendRecommendLineupScRsp) error { response, err := buildPrydwenRecommendResponse(req) if err != nil { // Still answer with request context so the UI does not wait forever. rsp.Key = req.Key rsp.Type = req.Type Log(fmt.Sprintf("[RECOMMEND] Prydwen data unavailable type=%d key=%d: %v", req.Type, req.Key, err)) return nil } rsp.Key = response.Key rsp.Type = response.Type rsp.MGKCKHIIICL = response.MGKCKHIIICL rsp.Retcode = response.Retcode rsp.ALDBIIIGLOM = response.ALDBIIIGLOM mode, _ := recommendationMode(&prydwenSnapshot, req) Log(fmt.Sprintf("[RECOMMEND] served Prydwen %s teams type=%d key=%d groups=%d", strings.ToUpper(mode), req.Type, req.Key, len(response.MGKCKHIIICL))) return nil }) }