package gameserver import ( "encoding/json" "fmt" "path/filepath" "strings" "sync" "gac-hkrpg-go/internal/common" goproto "gac-hkrpg-go/protocol/proto" ) type prydwenEndgameSnapshot struct { Source struct { DatasetVersion string `json:"dataset_version"` ChallengeVersion string `json:"challenge_version"` ChallengeName string `json:"challenge_name"` } `json:"source"` AvatarAliases map[string]string `json:"avatar_aliases"` Mode string `json:"mode"` Floor uint32 `json:"floor"` Stages []struct { Stage uint32 `json:"stage"` Rank uint32 `json:"rank"` Characters []string `json:"characters"` } `json:"stages"` } type prydwenAvatarResource struct { ID uint32 `json:"AvatarID"` VOTag string `json:"AvatarVOTag"` ConfigPath string `json:"JsonPath"` } var ( prydwenOnce sync.Once prydwenLineups []*goproto.ChallengeLineupList 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 loadPrydwenLineups() ([]*goproto.ChallengeLineupList, error) { prydwenOnce.Do(func() { prydwenLineups, prydwenLoadErr = readPrydwenLineups() }) return prydwenLineups, prydwenLoadErr } func readPrydwenLineups() ([]*goproto.ChallengeLineupList, error) { raw, err := common.ReadResourceFile("resources/PrydwenEndgameTeams.json") if err != nil { return nil, fmt.Errorf("read Prydwen endgame team snapshot: %w", err) } var snapshot prydwenEndgameSnapshot if err := json.Unmarshal(raw, &snapshot); err != nil { return nil, fmt.Errorf("parse Prydwen endgame team snapshot: %w", err) } if snapshot.Mode != "moc" || snapshot.Floor == 0 || len(snapshot.Stages) == 0 { return nil, fmt.Errorf("Prydwen endgame team snapshot has no supported MoC floor") } 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 } } lineups := make([]*goproto.ChallengeLineupList, 0, len(snapshot.Stages)) seenStages := map[uint32]bool{} for _, stage := range snapshot.Stages { if stage.Stage == 0 || seenStages[stage.Stage] || len(stage.Characters) != 4 { return nil, fmt.Errorf("invalid Prydwen MoC stage %d team size/order", stage.Stage) } seenStages[stage.Stage] = true avatars := make([]*goproto.ChallengeAvatarInfo, 0, len(stage.Characters)) for index, slug := range stage.Characters { resourceTag := snapshot.AvatarAliases[slug] if resourceTag == "" { resourceTag = slug } id := avatarIDs[normalizedAvatarName(resourceTag)] if id == 0 { return nil, fmt.Errorf("Prydwen avatar %q has no ID in Client AvatarConfig", slug) } avatars = append(avatars, &goproto.ChallengeAvatarInfo{ AvatarType: goproto.AvatarType_AVATAR_FORMAL_TYPE, Id: id, Index: uint32(index), }) } lineups = append(lineups, &goproto.ChallengeLineupList{AvatarList: avatars}) } return lineups, nil } func buildPrydwenRecommendResponse(req *goproto.GetFriendRecommendLineupCsReq) (*goproto.GetFriendRecommendLineupScRsp, error) { lineups, err := loadPrydwenLineups() if err != nil { return nil, err } return &goproto.GetFriendRecommendLineupScRsp{ Key: req.Key, Type: req.Type, MGKCKHIIICL: []*goproto.FPDCINMDJCE{{ KJFEKHDEBIK: &goproto.FPDCINMDJCE_NHKCFOALAPD{ NHKCFOALAPD: &goproto.OJJJDPNOKHG{LineupList: lineups}, }, }}, }, nil } func init() { handle("GetFriendRecommendLineup", func(_ *Session, req *goproto.GetFriendRecommendLineupCsReq, rsp *goproto.GetFriendRecommendLineupScRsp) error { // Preserve request context. The game UI asks this endpoint for the // recommendation panel; an empty auto-response left the panel blank. response, err := buildPrydwenRecommendResponse(req) if err != nil { Log(fmt.Sprintf("[RECOMMEND] Prydwen data unavailable type=%s 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 Log(fmt.Sprintf("[RECOMMEND] served Prydwen MoC teams type=%s key=%d stages=%d", req.Type, req.Key, len(response.MGKCKHIIICL[0].GetNHKCFOALAPD().LineupList))) return nil }) }