package gameserver import ( "fmt" "gac-hkrpg-go/internal/common" "sort" goproto "gac-hkrpg-go/protocol/proto" "google.golang.org/protobuf/proto" ) type challengeRoom struct { Entrance, Group, Monster, Event uint32 Avatars []uint32 Buffs []uint32 } // PearlSR 005d2c12 challenge_mgr.zig defines the three Tierce nodes 0..2. const tierceNodeCount = 3 func rememberTierceTeams(s *Session, id uint32, teams [][]uint32, buffs []uint32, single bool, node uint32) error { if s.TierceTeams == nil { s.TierceTeams = map[uint32][][]uint32{} } if s.TierceBuffs == nil { s.TierceBuffs = map[uint32][]uint32{} } p := s.JSON() if p == nil { return fmt.Errorf("player data is unavailable") } if p.TierceProgress == nil { p.TierceProgress = map[uint32]*common.TierceProgress{} } saved := p.TierceProgress[id] if saved == nil { saved = &common.TierceProgress{Nodes: map[uint32]common.TierceNodeProgress{}} p.TierceProgress[id] = saved } if s.TierceTeams[id] == nil { s.TierceTeams[id] = saved.Teams } if s.TierceBuffs[id] == nil { s.TierceBuffs[id] = saved.Buffs } if len(s.TierceTeams[id]) != tierceNodeCount { s.TierceTeams[id] = make([][]uint32, tierceNodeCount) } if len(s.TierceBuffs[id]) != tierceNodeCount { s.TierceBuffs[id] = make([]uint32, tierceNodeCount) } for index, team := range teams { target := index if single && len(teams) == 1 { target = int(node) } if target >= tierceNodeCount { continue } if len(team) > 0 { s.TierceTeams[id][target] = append([]uint32(nil), dedupeTruncate4(team)...) } if index < len(buffs) { s.TierceBuffs[id][target] = buffs[index] } } saved.Teams, saved.Buffs = s.TierceTeams[id], s.TierceBuffs[id] return p.SavePersistentChecked() } // A run owns its teams, score and room independently of Freesr and other // sessions. Rooms come from the installed ChallengeMaze/Tierce/Peak tables. type challengeRun struct { ID uint32 Tierce, Peak, Single, PendingNext, Finished bool Node uint32 Rooms []challengeRoom Scores []uint32 Rounds uint32 RoundCosts []uint32 Cur *goproto.CurChallenge TierceCur *goproto.CurTierceChallenge PeakBuff uint32 Results []*goproto.PMMOFOBCINL Win bool } func resourceRoom(stage ChallengeStageData, second bool, avatars []uint32, buff uint32) challengeRoom { r := challengeRoom{Entrance: stage.Entrance, Group: stage.Group1, Monster: stage.Monster1, Event: stage.Event1} if second { r.Entrance, r.Group, r.Monster, r.Event = stage.Entrance2, stage.Group2, stage.Monster2, stage.Event2 } r.Avatars = append([]uint32(nil), dedupeTruncate4(avatars)...) r.Buffs = []uint32{buff, stage.Buff} return r } func (run *challengeRun) lineupType() goproto.ExtraLineupType { // Each Tierce node has its own protocol team. Updating team 1 while the // client switches to team 3 leaves an empty TeamData and leader slot -1. if run.Tierce { switch run.Node { case 1: return goproto.ExtraLineupType_ExtraLineupType_LineupChallenge2 case 2: return goproto.ExtraLineupType_ExtraLineupType_LineupChallenge3 } } if !run.Tierce && !run.Peak && run.Node > 0 { return goproto.ExtraLineupType_ExtraLineupType_LineupChallenge2 } return goproto.ExtraLineupType_ExtraLineupType_LineupChallenge } func (run *challengeRun) activeLineup(plane uint32) *goproto.LineupInfo { return lineupInfoFromAvatars(run.Rooms[run.Node].Avatars, plane, run.lineupType(), 0) } func challengeCurBuffs(id uint32, buffs []uint32) *goproto.ChallengeCurBuffInfo { var valid []uint32 seen := map[uint32]bool{} for _, buff := range buffs { if buff != 0 && !seen[buff] { valid = append(valid, buff) seen[buff] = true } } switch { case id > 20000 && id < 30000: return &goproto.ChallengeCurBuffInfo{CurStoryBuffs: &goproto.ChallengeStoryBuffList{BuffList: valid}} case id > 30000: return &goproto.ChallengeCurBuffInfo{CurBossBuffs: &goproto.ChallengeBossBuffList{BuffList: valid, ChallengeBossConst: 1}} } return nil } func advanceChallengeRoom(s *Session) (*goproto.SceneInfo, error) { run := s.ChallengeRun if run == nil || !run.PendingNext || int(run.Node)+1 >= len(run.Rooms) { return nil, fmt.Errorf("challenge has no pending next room") } next := run.Node + 1 r := run.Rooms[next] if len(r.Avatars) == 0 { return nil, fmt.Errorf("challenge %d node %d has no saved team", run.ID, next) } previousConfig := s.ChallengeBattleConfig if err := setChallengeBattleConfig(s, r.Avatars, run.ID, r.Event, r.Buffs); err != nil { return nil, err } scene, _, _, err := loadChallengeScene(s, r.Entrance, r.Group, r.Monster, r.Event, r.Avatars) if err != nil { s.ChallengeBattleConfig = previousConfig return nil, err } run.Node, run.PendingNext = next, false if run.Cur != nil { run.Cur.ExtraLineupType = run.lineupType() run.Cur.StageInfo = challengeCurBuffs(run.ID, r.Buffs) } if run.TierceCur != nil { run.TierceCur.FINOCEGDBDI = next run.TierceCur.LineupList = []*goproto.LineupInfo{run.activeLineup(scene.PlaneId)} } SceneDebugLog("[CHALLENGE] advanced id=%d tierce=%t node=%d entry=%d stage=%d team=%v", run.ID, run.Tierce, next, r.Entrance, r.Event, r.Avatars) return scene, nil } func sendChallengeRoom(s *Session, scene *goproto.SceneInfo) error { run := s.ChallengeRun lineup := run.activeLineup(scene.PlaneId) if err := s.Send(&goproto.SyncLineupNotify{Lineup: lineup}); err != nil { return err } if !run.Tierce && !run.Peak { if err := s.Send(&goproto.ChallengeLineupNotify{ExtraLineupType: run.lineupType()}); err != nil { return err } } return s.Send(&goproto.EnterSceneByServerScNotify{Scene: scene, Lineup: lineup}) } func init() { handle("EnterChallengeNextPhase", func(s *Session, _ *goproto.EnterChallengeNextPhaseCsReq, rsp *goproto.EnterChallengeNextPhaseScRsp) error { if run := s.ChallengeRun; run == nil || run.Tierce || run.Peak { rsp.Retcode = uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST) return nil } scene, err := advanceChallengeRoom(s) if err != nil { rsp.Retcode = uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST) return nil } rsp.Scene = scene return s.Send(&goproto.SyncLineupNotify{Lineup: s.ChallengeRun.activeLineup(scene.PlaneId)}) }) // PearlSR 005d2c12 retains obfuscated command names. Client callback // metadata confirms b__25_0(LHAKNKHBLBI) and // b__21_0(JMPJCAPKNGI); resolve IDs by these table names. rawNamed("MHJNOBBOFOC", func(s *Session, _ []byte) error { if run := s.ChallengeRun; run == nil || !run.Tierce { return s.Send(&goproto.LHAKNKHBLBI{Retcode: uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST)}) } scene, err := advanceChallengeRoom(s) if err != nil { return s.Send(&goproto.LHAKNKHBLBI{Retcode: uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST)}) } if err := s.Send(&goproto.SyncLineupNotify{Lineup: s.ChallengeRun.activeLineup(scene.PlaneId)}); err != nil { return err } return s.Send(&goproto.LHAKNKHBLBI{Scene: scene, CurTierceChallenge: s.ChallengeRun.TierceCur}) }) rawNamed("PKKFNODINEM", func(s *Session, _ []byte) error { scene, err := restartChallengeRoom(s) if err != nil { return s.Send(&goproto.JMPJCAPKNGI{Retcode: uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST)}) } return s.Send(&goproto.JMPJCAPKNGI{Scene: scene}) }) rawNamed("MENLHHCJNCE", func(s *Session, _ []byte) error { return s.Send(&goproto.OKEDFNJJGHB{}) }) handle("RestartChallengePhase", func(s *Session, _ *goproto.RestartChallengePhaseCsReq, rsp *goproto.RestartChallengePhaseScRsp) error { scene, err := restartChallengeRoom(s) if err != nil { rsp.Retcode = uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST) } else { rsp.Scene = scene } return nil }) } func restartChallengeRoom(s *Session) (*goproto.SceneInfo, error) { run := s.ChallengeRun if run == nil { return nil, fmt.Errorf("no challenge to restart") } r := run.Rooms[run.Node] configure := setChallengeBattleConfig if run.Peak { configure = setPeakBattleConfig } if err := configure(s, r.Avatars, run.ID, r.Event, r.Buffs); err != nil { return nil, err } scene, _, _, err := loadChallengeScene(s, r.Entrance, r.Group, r.Monster, r.Event, r.Avatars) if err != nil { return nil, err } run.Finished, run.PendingNext = false, false run.Win = false run.Scores[run.Node] = 0 if len(run.RoundCosts) > int(run.Node) { run.RoundCosts[run.Node] = 0 } run.Rounds = 0 for _, rounds := range run.RoundCosts { run.Rounds += rounds } var results []*goproto.PMMOFOBCINL for _, result := range run.Results { if result.FINOCEGDBDI != run.Node { results = append(results, result) } } run.Results = results if run.Cur != nil { run.Cur.Status = goproto.ChallengeStatus_CHALLENGE_DOING run.Cur.RoundCount = run.Rounds run.Cur.ScoreId = run.Scores[0] if len(run.Scores) > 1 { run.Cur.ScoreTwo = run.Scores[1] } } CURRENT_BATTLE.Delete(s.Token) return scene, nil } func finishTierceNode(s *Session, req *goproto.PVEBattleResultCsReq) error { run := s.ChallengeRun lineup := &goproto.GLLFJFJBIBC{} for slot, id := range run.Rooms[run.Node].Avatars { avatar := s.JSON().Avatars[id] if avatar == nil { continue } info := &goproto.CLDOIEGCNLD{AvatarId: id, AvatarType: uint32(goproto.AvatarType_AVATAR_FORMAL_TYPE), Slot: uint32(slot), Level: avatar.Level, PAGPNOKLNOM: &goproto.ChallengeBossEquipmentInfo{}, OKCGHKDMFAM: &goproto.ChallengeBossAvatarRelicInfo{}} if cone := s.JSON().FindLightconeFor(id); cone != nil { info.PAGPNOKLNOM = &goproto.ChallengeBossEquipmentInfo{UniqueId: cone.InternalUID, Tid: cone.ItemID, Level: cone.Level, Promotion: cone.Promotion, Rank: cone.Rank} } lineup.MNABNJOEHLF = append(lineup.MNABNJOEHLF, info) } result := &goproto.PMMOFOBCINL{FINOCEGDBDI: run.Node, ScoreId: run.Scores[run.Node], EndStatus: req.EndStatus, Lineup: lineup} if req.Stt != nil { for _, targets := range req.Stt.BattleTargetInfo { result.BattleTargetList = append(result.BattleTargetList, targets.GetBattleTargetList()...) } } run.Results = append(run.Results, result) if err := saveTierceResult(s, result); err != nil { return err } var total uint32 for _, score := range run.Scores { total += score } allWon := len(run.Results) == tierceNodeCount for _, nodeResult := range run.Results { if nodeResult.EndStatus != goproto.BattleEndStatus_BATTLE_END_WIN { allWon = false } } var finishedTargets []uint32 if run.Finished && allWon { finishedTargets = tierceTargets(run.ID, total) } win := req.EndStatus == goproto.BattleEndStatus_BATTLE_END_WIN if run.Finished { win = win && allWon } if run.Finished && getChallengeData().Tierce[run.ID].ClearScore > 0 { win = win && total >= getChallengeData().Tierce[run.ID].ClearScore } run.Win = win // Verified Client events: this oneof is OnStageSettle; the field-8 // alternative is OnMedal and must not be used to switch stages. if err := s.Send(&goproto.DOPHNDHILPA{MMCGBGDJIPN: &goproto.DOPHNDHILPA_IANBCDJPDJF{IANBCDJPDJF: &goproto.JCKGBFJJFFC{PAIAEPJIDPE: result, IsWin: win, FinishedTargetList: finishedTargets}}}); err != nil { return err } if run.Finished { sort.Slice(run.Results, func(i, j int) bool { return run.Results[i].FINOCEGDBDI < run.Results[j].FINOCEGDBDI }) var score uint32 for _, value := range run.Scores { score += value } if err := s.Send(&goproto.DOPHNDHILPA{MMCGBGDJIPN: &goproto.DOPHNDHILPA_DFNNKENBOEN{DFNNKENBOEN: &goproto.GEKNAKDAMJF{ChallengeId: run.ID, IsWin: win, FinishedTargetList: finishedTargets, EGEHLOPFELK: &goproto.ChallengeTierceRecord{TotalScore: score}, GBMBAHHDONN: run.Results}}}); err != nil { return err } } return errResponseAlreadySent } // Scores are supplied by the Client; Tierce targets are evaluated against the // installed target table. A win alone does not grant score targets or rewards. func finishChallengeBattle(s *Session, req *goproto.PVEBattleResultCsReq, rsp *goproto.PVEBattleResultScRsp, active bool) error { run := s.ChallengeRun if !active || run == nil || run.Finished || run.PendingNext || req.StageId != run.Rooms[run.Node].Event { return nil } if req.EndStatus == goproto.BattleEndStatus_BATTLE_END_QUIT { return nil } if req.Stt != nil { run.Scores[run.Node] = req.Stt.ChallengeScore if len(run.RoundCosts) != len(run.Rooms) { run.RoundCosts = make([]uint32, len(run.Rooms)) } run.RoundCosts[run.Node] = req.Stt.RoundCnt run.Rounds = 0 for _, rounds := range run.RoundCosts { run.Rounds += rounds } } if run.Cur != nil { run.Cur.RoundCount = run.Rounds run.Cur.ScoreId = run.Scores[0] if len(run.Scores) > 1 { run.Cur.ScoreTwo = run.Scores[1] } } SceneDebugLog("[CHALLENGE] result id=%d node=%d status=%s stage=%d scores=%v rounds=%d", run.ID, run.Node, req.EndStatus, req.StageId, run.Scores, run.Rounds) if err := s.Send(rsp); err != nil { return err } win := req.EndStatus == goproto.BattleEndStatus_BATTLE_END_WIN run.Win = win if win && !run.Single && int(run.Node)+1 < len(run.Rooms) { run.PendingNext = true if run.Tierce { return finishTierceNode(s, req) } scene, err := advanceChallengeRoom(s) if err != nil { return err } if err := sendChallengeRoom(s, scene); err != nil { return err } return errResponseAlreadySent } run.Finished = true if run.Tierce { return finishTierceNode(s, req) } if run.Peak { if err := s.Send(&goproto.ChallengePeakSettleScNotify{PeakId: run.ID, IsWin: win, CyclesUsed: run.Rounds}); err != nil { return err } return errResponseAlreadySent } if run.Cur != nil { run.Cur.Status = goproto.ChallengeStatus_CHALLENGE_FAILED if win { run.Cur.Status = goproto.ChallengeStatus_CHALLENGE_FINISH } if err := s.Send(&goproto.ChallengeSettleNotify{ChallengeId: run.ID, IsWin: win, CurChallenge: proto.Clone(run.Cur).(*goproto.CurChallenge), Reward: &goproto.ItemList{}, ChallengeScore: run.Cur.ScoreId, ScoreTwo: run.Cur.ScoreTwo}); err != nil { return err } } return errResponseAlreadySent }