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

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
}