294 lines
9.7 KiB
Go
294 lines
9.7 KiB
Go
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
|
|
})
|
|
}
|