383 lines
13 KiB
Go
383 lines
13 KiB
Go
package gameserver
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"slices"
|
|
"sort"
|
|
|
|
"gac-hkrpg-go/internal/common"
|
|
pb "gac-hkrpg-go/protocol/proto"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
// The current named command table, FFGO's extracted descriptor, and the native
|
|
// GridFightOrbServerData.GetOrb sender agree on this request and its wire tags.
|
|
// Do not introduce a numeric alias for the reference's descriptive name.
|
|
func init() {
|
|
rawNamed("IKMOCJBMGOJ", func(s *Session, payload []byte) error {
|
|
rsp := &pb.PNHDGCBLPGD{Retcode: uint32(pb.Retcode_RET_GRID_FIGHT_PARAM_NOT_MATCH)}
|
|
var req pb.IKMOCJBMGOJ
|
|
if err := proto.Unmarshal(payload, &req); err != nil {
|
|
return cwSend(s, rsp)
|
|
}
|
|
if err := cwUseOrbs(s, &req); err != nil {
|
|
var sendError *cwPacketSendError
|
|
if errors.As(err, &sendError) {
|
|
return err
|
|
}
|
|
rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_CONF_MISS)
|
|
PacketLog("CW_USE_ORB rejected without consuming: " + err.Error())
|
|
return cwSend(s, rsp)
|
|
}
|
|
rsp.Retcode = 0
|
|
return cwSend(s, rsp)
|
|
})
|
|
}
|
|
|
|
type cwOrbPayout struct {
|
|
UID uint32
|
|
Drop *pb.GridFightDropInfo
|
|
Updates []*pb.KFNCAIHIHCM
|
|
}
|
|
|
|
func cwUseOrbs(s *Session, req *pb.IKMOCJBMGOJ) error {
|
|
c, data, err := cwPreparingData(s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
run := data.DHFOFLFCLAH
|
|
var bag *pb.DBONCGFHHDN
|
|
for _, component := range run.NAGDJGMMCBE {
|
|
if v := component.GetDBAGKJNPDOL(); v != nil {
|
|
bag = v
|
|
}
|
|
}
|
|
if bag == nil {
|
|
return fmt.Errorf("CW Orb component missing")
|
|
}
|
|
owned := make(map[uint32]*pb.IBPDPCOGDEP, len(bag.MDKGCIKOMLI))
|
|
for _, orb := range bag.MDKGCIKOMLI {
|
|
if orb == nil || orb.UniqueId == 0 || owned[orb.UniqueId] != nil {
|
|
return fmt.Errorf("CW Orb inventory identity invalid")
|
|
}
|
|
owned[orb.UniqueId] = orb
|
|
}
|
|
uids := slices.Clone(req.CBMGDBJHLHO)
|
|
if req.IsGetAll {
|
|
uids = nil
|
|
for _, orb := range bag.MDKGCIKOMLI {
|
|
uids = append(uids, orb.UniqueId)
|
|
}
|
|
}
|
|
if len(uids) == 0 {
|
|
return fmt.Errorf("no owned Orbs selected")
|
|
}
|
|
recipes, err := common.LoadCurrencyWarsOrbRecipes()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
captured, err := common.LoadCurrencyWarsRewards(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
stage, _ := cwStage(run)
|
|
if stage == nil {
|
|
return fmt.Errorf("CW stage missing")
|
|
}
|
|
used := map[uint32]bool{}
|
|
payouts := make([]cwOrbPayout, 0, len(uids))
|
|
for _, uid := range uids {
|
|
orb := owned[uid]
|
|
if orb == nil || used[uid] {
|
|
return fmt.Errorf("Orb UID %d missing or duplicated", uid)
|
|
}
|
|
used[uid] = true
|
|
row, ok := captured.Orbs[orb.LDBNOMPCOKK]
|
|
if !ok {
|
|
return fmt.Errorf("Orb %d not present in captured Client table", orb.LDBNOMPCOKK)
|
|
}
|
|
match := false
|
|
for _, reference := range recipes.Orbs {
|
|
if reference.ID == row.ID && reference.Bonus == row.Bonus {
|
|
match = true
|
|
break
|
|
}
|
|
}
|
|
if !match {
|
|
return fmt.Errorf("Orb %d reference bonus differs from captured Client table", row.ID)
|
|
}
|
|
choices, err := recipes.Choices(row.ID, stage.DCAHJELNJNP)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Reject the whole recipe before rolling if a branch cannot execute. This
|
|
// avoids silently conditioning the reward distribution on supported kinds.
|
|
for _, choice := range choices {
|
|
if len(choice) == 0 {
|
|
return fmt.Errorf("Orb has an empty reward choice")
|
|
}
|
|
for _, reward := range choice {
|
|
if !cwOrbRewardSupported(reward.Reward) {
|
|
return fmt.Errorf("Orb %d reward %s needs its effect implementation", row.ID, reward.Reward)
|
|
}
|
|
}
|
|
}
|
|
pick, err := rand.Int(rand.Reader, big.NewInt(int64(len(choices))))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
payout := cwOrbPayout{UID: uid, Drop: &pb.GridFightDropInfo{}}
|
|
for _, reward := range choices[pick.Int64()] {
|
|
updates, drops, err := cwApplyOrbReward(c, run, recipes, reward)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
payout.Updates = append(payout.Updates, updates...)
|
|
payout.Drop.NMGIABHIJJB = append(payout.Drop.NMGIABHIJJB, drops...)
|
|
}
|
|
payout.Updates = append(payout.Updates, &pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_GOHHJLAPMBB{GOHHJLAPMBB: uid}})
|
|
payouts = append(payouts, payout)
|
|
}
|
|
bag.MDKGCIKOMLI = slices.DeleteFunc(bag.MDKGCIKOMLI, func(v *pb.IBPDPCOGDEP) bool { return used[v.UniqueId] })
|
|
// The detached snapshot plus CAS commits inventory and all rewards together.
|
|
// Neither failed validation nor a stale external revision consumes an Orb.
|
|
if err = s.saveCurrencyWarsData(data, c); err != nil {
|
|
return err
|
|
}
|
|
for _, payout := range payouts {
|
|
// Native FFGO sends the drop presentation before the grouped sync. Only
|
|
// scalar drop fields 1/2/3 plus the exact role-star oneof (tag101).
|
|
if err = cwSend(s, &pb.GDHFMHKFEAL{FKNNKCKLFMD: payout.UID, NMGIABHIJJB: payout.Drop}); err != nil {
|
|
return err
|
|
}
|
|
// Native OrbSummarizer requires source1 and first context uint=OrbUID.
|
|
if err = cwSend(s, &pb.GridFightSyncUpdateResultScNotify{GCPNIHHBGGD: []*pb.ABPLALCHNFA{{GridUpdateSrc: pb.JJDFGMMMCMI_JJDFGMMMCMI_DBIEKOMKBIN, OOEOCGLLHFO: []uint32{payout.UID}, JKPJFKCBIEM: payout.Updates}}}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
PacketLog(fmt.Sprintf("CW_USE_ORB saved count=%d revision=%d", len(payouts), s.currencyWarsRevision))
|
|
return nil
|
|
}
|
|
|
|
func cwOrbRewardSupported(kind string) bool {
|
|
switch kind {
|
|
case "Gold", "Exp", "Refresh", "GlobalHp", "Item", "RandomEquip", "RandomRole":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func cwApplyOrbReward(c *common.CurrencyWarsCatalog, run *pb.MPIMAEMCEFH, recipes *common.CurrencyWarsOrbRecipes, reward common.CurrencyWarsOrbReward) ([]*pb.KFNCAIHIHCM, []*pb.BCCONKJHDJL, error) {
|
|
player, _, shop := cwComponents(run)
|
|
if player == nil || shop == nil {
|
|
return nil, nil, fmt.Errorf("CW Orb player/shop components missing")
|
|
}
|
|
var updates []*pb.KFNCAIHIHCM
|
|
drop := &pb.BCCONKJHDJL{}
|
|
add := func(current, delta uint32) (uint32, error) {
|
|
if uint64(current)+uint64(delta) > uint64(^uint32(0)) {
|
|
return 0, fmt.Errorf("CW Orb reward overflow")
|
|
}
|
|
return current + delta, nil
|
|
}
|
|
var err error
|
|
switch reward.Reward {
|
|
case "Gold":
|
|
if reward.Amount == 0 {
|
|
return nil, nil, fmt.Errorf("CW Orb gold amount is missing")
|
|
}
|
|
player.Gold, err = add(player.Gold, reward.Amount)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
updates = append(updates, cwGoldSync(player.Gold))
|
|
drop.FKKGLFOHGGA = pb.MJJIFGINJAJ_MJJIFGINJAJ_item_value
|
|
drop.Num = reward.Amount
|
|
case "Exp":
|
|
if reward.Amount == 0 {
|
|
return nil, nil, fmt.Errorf("CW Orb EXP amount is missing")
|
|
}
|
|
if err = cwApplyBaseExperience(c, player, shop, reward.Amount); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
updates = append(updates, cwCapacitySync(player), cwShopSync(shop), &pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_PLPHEIBLPDL{PLPHEIBLPDL: &pb.PIHKLPJGEPM{Level: player.EHNJDIDABHD, Exp: player.GridFightCurLevelExp, HFMKLAPEKFA: player.NDAINFLOLPJ}}})
|
|
drop.FKKGLFOHGGA = pb.MJJIFGINJAJ_MJJIFGINJAJ_exp
|
|
drop.Num = reward.Amount
|
|
case "Refresh":
|
|
if reward.Amount == 0 {
|
|
return nil, nil, fmt.Errorf("CW Orb refresh amount is missing")
|
|
}
|
|
shop.OPDCMCFOFAA, err = add(shop.OPDCMCFOFAA, reward.Amount)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
updates = append(updates, cwShopSync(shop))
|
|
drop.FKKGLFOHGGA = pb.MJJIFGINJAJ_MJJIFGINJAJ_LJBCDJNNBJH
|
|
drop.Num = reward.Amount
|
|
case "GlobalHp":
|
|
if reward.Amount == 0 || player.GridFightLineupHp > player.PPKKHHNEOHB {
|
|
return nil, nil, fmt.Errorf("CW Orb HP amount/cap invalid")
|
|
}
|
|
gain := min(reward.Amount, player.PPKKHHNEOHB-player.GridFightLineupHp)
|
|
player.GridFightLineupHp += gain
|
|
updates = append(updates, &pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_GridFightLineupHp{GridFightLineupHp: &pb.FMJDDIHBELC{GridFightLineupHp: player.GridFightLineupHp, PPKKHHNEOHB: player.PPKKHHNEOHB}}})
|
|
drop.FKKGLFOHGGA = pb.MJJIFGINJAJ_MJJIFGINJAJ_grid_fight_lineup_hp
|
|
drop.Num = gain
|
|
case "RandomEquip":
|
|
if reward.Count == 0 || reward.Category == "" {
|
|
return nil, nil, fmt.Errorf("CW Orb equipment count/category missing")
|
|
}
|
|
gear, err := common.LoadCurrencyWarsEquipment(c)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
var candidates []uint32
|
|
for _, reference := range recipes.Equipment {
|
|
if reference.Category == reward.Category {
|
|
if _, ok := gear[reference.ID]; !ok {
|
|
return nil, nil, fmt.Errorf("CW Orb equipment %d in category %s is uncaptured", reference.ID, reward.Category)
|
|
}
|
|
candidates = append(candidates, reference.ID)
|
|
}
|
|
}
|
|
sort.Slice(candidates, func(i, j int) bool { return candidates[i] < candidates[j] })
|
|
if len(candidates) == 0 {
|
|
return nil, nil, fmt.Errorf("CW Orb equipment category %s is uncaptured", reward.Category)
|
|
}
|
|
var drops []*pb.BCCONKJHDJL
|
|
for i := uint32(0); i < reward.Count; i++ {
|
|
pick, err := rand.Int(rand.Reader, big.NewInt(int64(len(candidates))))
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
id := candidates[pick.Int64()]
|
|
n, err := cwApplyCommand(c, run, common.CurrencyWarsCommand{Field: "equip", Value: id, Quantity: 1})
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
for _, group := range n.GCPNIHHBGGD {
|
|
updates = append(updates, group.JKPJFKCBIEM...)
|
|
}
|
|
drops = append(drops, &pb.BCCONKJHDJL{FKKGLFOHGGA: pb.MJJIFGINJAJ_MJJIFGINJAJ_NLFCABFPJHD, FNLMKJLKLFH: id, Num: 1})
|
|
}
|
|
return updates, drops, nil
|
|
case "RandomRole":
|
|
if reward.Count == 0 || reward.Rarity == 0 {
|
|
return nil, nil, fmt.Errorf("CW Orb role count/rarity missing")
|
|
}
|
|
_, team, shop := cwComponents(run)
|
|
if team == nil || shop == nil {
|
|
return nil, nil, fmt.Errorf("CW Orb role/shop components missing")
|
|
}
|
|
var drops []*pb.BCCONKJHDJL
|
|
for i := uint32(0); i < reward.Count; i++ {
|
|
pool, err := c.NewCapturedCardPool()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
for _, role := range team.GridGameRoleList {
|
|
if err = pool.ReserveOwned(c, role.Id, role.RoleStar); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
for _, offer := range shop.OGDKJPDFFAI {
|
|
role := offer.GetBDPIMPJOJBK()
|
|
if role == nil || offer.LHGIFDNPIDE {
|
|
continue
|
|
}
|
|
if err = pool.ReserveOwned(c, role.RoleId, role.RoleStar); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
var candidates []uint32
|
|
stars := map[uint32]uint32{}
|
|
for _, role := range c.CapturedRoles {
|
|
if role.Config.SeasonID != c.ActiveSeasonID || !role.Config.IsInPool || role.Config.Rarity != reward.Rarity {
|
|
continue
|
|
}
|
|
if stock, ok := pool.Roles[role.Config.ID]; !ok || stock.Disabled || stock.Remaining == 0 {
|
|
continue
|
|
}
|
|
star := reward.Star
|
|
if star == 0 {
|
|
star = role.DefaultStar
|
|
}
|
|
if _, err = c.CapturedRoleVariant(role.Config.ID, star); err != nil {
|
|
continue
|
|
}
|
|
// ReserveOwned uses captured star rows to count copies; no guessed
|
|
// power-of-three relationship is introduced for higher-star rewards.
|
|
candidatePool, err := c.NewCapturedCardPool()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
for id, stock := range pool.Roles {
|
|
candidatePool.Roles[id] = stock
|
|
}
|
|
if err = candidatePool.ReserveOwned(c, role.Config.ID, star); err != nil {
|
|
continue
|
|
}
|
|
candidates = append(candidates, role.Config.ID)
|
|
stars[role.Config.ID] = star
|
|
}
|
|
sort.Slice(candidates, func(i, j int) bool { return candidates[i] < candidates[j] })
|
|
if len(candidates) == 0 {
|
|
return nil, nil, fmt.Errorf("CW Orb rarity %d/star %d has no available captured role", reward.Rarity, reward.Star)
|
|
}
|
|
pick, err := rand.Int(rand.Reader, big.NewInt(int64(len(candidates))))
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
id := candidates[pick.Int64()]
|
|
n, err := cwApplyCommand(c, run, common.CurrencyWarsCommand{Field: "role", Value: id, Star: stars[id], Quantity: 1})
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
for _, group := range n.GCPNIHHBGGD {
|
|
updates = append(updates, group.JKPJFKCBIEM...)
|
|
}
|
|
drops = append(drops, &pb.BCCONKJHDJL{FKKGLFOHGGA: pb.MJJIFGINJAJ_MJJIFGINJAJ_BDPIMPJOJBK, FNLMKJLKLFH: id, Num: 1, DisplayValue: &pb.AOHKKFHDALO{NEBNJDMFELO: &pb.AOHKKFHDALO_RoleStar{RoleStar: stars[id]}}})
|
|
}
|
|
return updates, drops, nil
|
|
case "Item":
|
|
if reward.ItemID == 0 || reward.Count == 0 {
|
|
return nil, nil, fmt.Errorf("CW Orb item identity/count missing")
|
|
}
|
|
gear, err := common.LoadCurrencyWarsEquipment(c)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
rewards, err := common.LoadCurrencyWarsRewards(c)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
field := ""
|
|
if _, ok := gear[reward.ItemID]; ok {
|
|
field = "equip"
|
|
} else if _, ok := rewards.Consumables[reward.ItemID]; ok {
|
|
field = "consumable"
|
|
} else if _, ok := rewards.Forges[reward.ItemID]; ok {
|
|
field = "forge"
|
|
}
|
|
if field == "" {
|
|
return nil, nil, fmt.Errorf("CW Orb item %d has no captured acquisition path", reward.ItemID)
|
|
}
|
|
n, err := cwApplyCommand(c, run, common.CurrencyWarsCommand{Field: field, Value: reward.ItemID, Quantity: reward.Count})
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
for _, group := range n.GCPNIHHBGGD {
|
|
updates = append(updates, group.JKPJFKCBIEM...)
|
|
}
|
|
drop.FKKGLFOHGGA = pb.MJJIFGINJAJ_MJJIFGINJAJ_NLFCABFPJHD
|
|
drop.FNLMKJLKLFH = reward.ItemID
|
|
drop.Num = reward.Count
|
|
default:
|
|
return nil, nil, fmt.Errorf("CW Orb reward %s is unsupported", reward.Reward)
|
|
}
|
|
return updates, []*pb.BCCONKJHDJL{drop}, nil
|
|
}
|