Files
GaCy-GO/internal/gameserver/challenge_run.go
T
2026-10-28 18:54:43 +07:00

411 lines
14 KiB
Go

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 <StartNext>b__25_0(LHAKNKHBLBI) and
// <Restart>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
}