package gameserver import ( "fmt" "sort" "gac-hkrpg-go/internal/common" pb "gac-hkrpg-go/protocol/proto" ) // currencyWarsAbandonRun removes only the active run from the saved CW // response. Season progress and all unrelated player data remain intact. func currencyWarsAbandonRun(data *pb.GridFightGetDataScRsp) (uint32, bool) { if data == nil || data.DHFOFLFCLAH == nil { return 0, false } runID := data.DHFOFLFCLAH.UniqueId data.DHFOFLFCLAH = nil return runID, true } func currencyWarsQuitSettlement(catalog *common.CurrencyWarsCatalog, data *pb.GridFightGetDataScRsp, run *pb.MPIMAEMCEFH) (*pb.GridFightSyncKeepWinCntNotify, *pb.GridFightSettleNotify, error) { return currencyWarsTerminalSettlement(catalog, data, run, pb.PEIJNEFOCHI_PEIJNEFOCHI_NOCIJDINKPJ, false, nil) } // currencyWarsTerminalSettlement builds the ordered server notifications expected // when a run ends (quit, defeat, or victory). func currencyWarsTerminalSettlement(catalog *common.CurrencyWarsCatalog, data *pb.GridFightGetDataScRsp, run *pb.MPIMAEMCEFH, reason pb.PEIJNEFOCHI, isWon bool, damageList []*pb.PLFKMENCDMM) (*pb.GridFightSyncKeepWinCntNotify, *pb.GridFightSettleNotify, error) { if catalog == nil || run == nil || run.UniqueId == 0 { return nil, nil, fmt.Errorf("CW settlement is missing catalog or active run") } player, team, _ := cwComponents(run) if player == nil || team == nil { return nil, nil, fmt.Errorf("CW settlement is missing player/team components") } var stage *pb.BEEFLEJAOCN for _, component := range run.NAGDJGMMCBE { if component == nil { continue } if current := component.GetKLCPAAHDPJF(); current != nil { if stage != nil { return nil, nil, fmt.Errorf("CW settlement has duplicate stage components") } stage = current } } if stage == nil { return nil, nil, fmt.Errorf("CW settlement is missing the current stage component") } avatarIDs := make([]uint32, 0, len(team.GridGameRoleList)) seenAvatars := make(map[uint32]struct{}, len(team.GridGameRoleList)) for _, role := range team.GridGameRoleList { if role == nil || role.Id == 0 { continue } if _, exists := seenAvatars[role.Id]; exists { continue } seenAvatars[role.Id] = struct{}{} avatarIDs = append(avatarIDs, role.Id) } sort.Slice(avatarIDs, func(i, j int) bool { return avatarIDs[i] < avatarIDs[j] }) roles := make([]*pb.CDEAABODKCP, 0, len(team.GridGameRoleList)) for _, role := range team.GridGameRoleList { if role != nil { roles = append(roles, role) } } sort.SliceStable(roles, func(i, j int) bool { if roles[i].Pos != roles[j].Pos { return roles[i].Pos < roles[j].Pos } return roles[i].UniqueId < roles[j].UniqueId }) // Rank calculation based on remaining HP percentage (GridFight_SettleRankHpConfig: [1, 40, 70]) // Rank C = 0, Rank B = 1, Rank A = 2, Rank S = 3, Rank SS = 4 var rankType uint32 var divisionUp uint32 if isWon && player.PPKKHHNEOHB > 0 && player.GridFightLineupHp > 0 { hpPercent := float64(player.GridFightLineupHp) / float64(player.PPKKHHNEOHB) * 100.0 if hpPercent >= 100.0 || player.GridFightLineupHp >= player.PPKKHHNEOHB { rankType = 5 // Rank SSS (flawless / 100% HP) divisionUp = 3 } else if hpPercent >= 90.0 { rankType = 4 // Rank SS divisionUp = 3 } else if hpPercent >= 70.0 { rankType = 3 // Rank S divisionUp = 3 } else if hpPercent >= 40.0 { rankType = 2 // Rank A divisionUp = 2 } else { rankType = 1 // Rank B divisionUp = 1 } } else { rankType = 0 // Rank C (defeat / 0 HP) divisionUp = 0 } seasonDivisions := catalog.SeasonDivisions(catalog.ActiveSeasonID) curDivisionID := run.DivisionId newDivisionID := curDivisionID isPromotion := false if isWon && divisionUp > 0 && len(seasonDivisions) > 0 { curIdx := -1 for i, d := range seasonDivisions { if d.ID == curDivisionID { curIdx = i break } } if curIdx >= 0 { targetIdx := curIdx + int(divisionUp) if targetIdx >= len(seasonDivisions) { targetIdx = len(seasonDivisions) - 1 } newDivisionID = seasonDivisions[targetIdx].ID if seasonDivisions[targetIdx].DivisionLevel > seasonDivisions[curIdx].DivisionLevel { isPromotion = true } } } var equipmentList []*pb.IJDLENJJBHJ if bag := cwEquipmentBag(run); bag != nil { equipmentList = bag.GridFightEquipmentList } var augments []*pb.PPGEBEBPMJC for _, component := range run.NAGDJGMMCBE { if component == nil { continue } if owned := component.GetLLDBOIANGDN(); owned != nil { augments = owned.SyncAugmentInfo break } } var teamValue uint32 for _, r := range team.GridGameRoleList { if r != nil { teamValue += uint32(r.RoleStar) * 2 } } finish := &pb.OEDHMAHDECC{ Reason: reason, IsOverlock: run.IsOverlock, JLDDDLDNFEF: run.JLDDDLDNFEF, LLJJCOOKANH: rankType, MBMJNEDIFFC: divisionUp, DBODGPBOIHC: isPromotion, EEGPILJDLBG: damageList, GridFightEquipmentList: equipmentList, SyncAugmentInfo: augments, AKEGLJLCFPH: teamValue, NDLIKNEKDLL: &pb.GOCIGPCDKDJ{ LKCJAPIGEND: player.PPKKHHNEOHB, PAGPKOKMDBK: player.GridFightLineupHp, DCAHJELNJNP: stage.DCAHJELNJNP, EMOHLJHPOPC: stage.EMOHLJHPOPC, AACGBEBGDPE: run.DivisionId, SectionId: stage.SectionId, FDGKBOIEHBD: player.NDAINFLOLPJ, FMEOHKHGMJM: []uint32{catalog.ActiveSeasonModule().ActivityModuleID}, }, PPJFPNMIFHL: &pb.GLPCEKMKLGG{GridFightAvatarList: avatarIDs}, GridGameRoleList: roles, GridGameNpcList: team.GridGameNpcList, BossInfo: stage.BossInfo, CFFCNEDGCJJ: stage.CFFCNEDGCJJ, OAIGAGABCMK: stage.GridFightPortalBuffList, GridFightTraitInfo: cwCalculateBoardTraits(catalog, run), } var weeklyScore, expBase, expExtra, totalExp uint32 if scoreCatalog, err := common.LoadCurrencyWarsScore(catalog); err == nil { weeklyScore, expBase, expExtra = scoreCatalog.CalculateRunScore(run.DivisionId, run.IsOverlock, stage.DCAHJELNJNP, stage.SectionId, isWon) totalExp = expBase + expExtra } var curSeasonExp, curTalentCoin uint32 if data != nil && data.LFLCDNGALME != nil && data.LFLCDNGALME.HFPEJIDDDJG != nil { if data.LFLCDNGALME.HFPEJIDDDJG.Exp != nil { curSeasonExp = data.LFLCDNGALME.HFPEJIDDDJG.Exp.KEJGCDOGEDD } curTalentCoin = data.LFLCDNGALME.HFPEJIDDDJG.GEBGGMAGDJB } newSeasonExp := curSeasonExp + totalExp seasonLevel := (newSeasonExp / 800) + 1 maxExp := catalog.ActiveSeasonModule().MaxRewardExp maxLevel := maxExp / 800 if maxLevel == 0 { maxLevel = 60 } if seasonLevel > maxLevel { seasonLevel = maxLevel } if data != nil && data.LFLCDNGALME != nil && data.LFLCDNGALME.HFPEJIDDDJG != nil { if data.LFLCDNGALME.HFPEJIDDDJG.Exp == nil { data.LFLCDNGALME.HFPEJIDDDJG.Exp = &pb.GLIFOLCEMIN{} } data.LFLCDNGALME.HFPEJIDDDJG.Exp.KEJGCDOGEDD = newSeasonExp if newDivisionID > data.LFLCDNGALME.HFPEJIDDDJG.DivisionId { data.LFLCDNGALME.HFPEJIDDDJG.DivisionId = newDivisionID } data.LFLCDNGALME.HFPEJIDDDJG.GDAKCOICIDA = player.GDAKCOICIDA } settleNotify := &pb.GridFightSettleNotify{ FLHOPMMEGAI: run.DivisionId, CDANKDNACJP: newDivisionID, NMCKJPEMACM: seasonLevel, EDHKHJAPMKA: weeklyScore, CGKNDIJMOCJ: expBase, MNNKKEPCPPL: expExtra, KEJGCDOGEDD: newSeasonExp, GEBGGMAGDJB: curTalentCoin, BFLLJJBHLDF: finish, } if data == nil { settleNotify.NMCKJPEMACM = catalog.ActiveSeasonModule().MaxRewardExp settleNotify.KEJGCDOGEDD = totalExp } return &pb.GridFightSyncKeepWinCntNotify{EHBLJGIENPI: player.GDAKCOICIDA}, settleNotify, nil } func init() { // The current protocol's QuitSettle request has an empty body and a typed // Retcode response. A successful request abandons the active CW run; it // does not award a victory or mutate Adventure inventory. cwHandle("GridFightQuitSettle", func(s *Session, _ *pb.GridFightQuitSettleCsReq, rsp *pb.GridFightQuitSettleScRsp) error { rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_NOT_IN_GAMEPLAY) catalog, err := common.LoadCurrencyWarsCatalog() if err != nil { PacketLog(fmt.Sprintf("CW_QUIT_SETTLE rejected reason=catalog_error error=%v", err)) return nil } data, revision, err := readCurrencyWarsData(catalog) if err != nil { PacketLog(fmt.Sprintf("CW_QUIT_SETTLE rejected reason=snapshot_error error=%v", err)) return nil } if revision != s.currencyWarsRevision { rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_PARAM_NOT_MATCH) PacketLog(fmt.Sprintf("CW_QUIT_SETTLE rejected reason=session_revision_mismatch saved=%d session=%d", revision, s.currencyWarsRevision)) return nil } if data == nil { PacketLog(fmt.Sprintf("CW_QUIT_SETTLE rejected reason=no_saved_data revision=%d", revision)) return nil } run := data.DHFOFLFCLAH if run == nil { PacketLog(fmt.Sprintf("CW_QUIT_SETTLE rejected reason=no_active_run revision=%d", revision)) return nil } runID := run.UniqueId keepWinCount, settlement, err := currencyWarsQuitSettlement(catalog, data, run) if err != nil { PacketLog(fmt.Sprintf("CW_QUIT_SETTLE rejected run=%d reason=settlement_build_error error=%v", runID, err)) return nil } _, active := currencyWarsAbandonRun(data) if !active { return nil } if err = s.saveCurrencyWarsTerminal(data, catalog, settlement.BFLLJJBHLDF); err != nil { rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_CONF_MISS) PacketLog(fmt.Sprintf("CW_QUIT_SETTLE rejected run=%d reason=save_error error=%v", runID, err)) return nil } if err = cwSend(s, keepWinCount); err != nil { return err } if err = cwSend(s, settlement); err != nil { return err } CURRENT_BATTLE.Delete(s.Token) s.currencyWarsOpen = false rsp.Retcode = 0 PacketLog(fmt.Sprintf("CW_QUIT_SETTLE completed run=%d revision=%d result=abandoned", runID, s.currencyWarsRevision)) return nil }) }