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 break } } var uids []uint32 if req.IsGetAll { if bag != nil { for _, orb := range bag.MDKGCIKOMLI { if orb != nil && orb.UniqueId != 0 { uids = append(uids, orb.UniqueId) } } } if len(uids) == 0 { // All rewards already claimed; acknowledge cleanly without error. return nil } } else { uids = slices.Clone(req.CBMGDBJHLHO) } if len(uids) == 0 { return nil } owned := make(map[uint32]*pb.IBPDPCOGDEP) if bag != nil { for _, orb := range bag.MDKGCIKOMLI { if orb != nil && orb.UniqueId != 0 && owned[orb.UniqueId] == nil { owned[orb.UniqueId] = orb } } } var toConsume []uint32 var alreadyConsumed []uint32 seen := make(map[uint32]bool, len(uids)) for _, uid := range uids { if uid == 0 || seen[uid] { continue } seen[uid] = true if owned[uid] != nil { toConsume = append(toConsume, uid) } else { alreadyConsumed = append(alreadyConsumed, uid) } } // If none of the requested orbs are currently owned, all of them were already // consumed or despawned. Acknowledge idempotently and re-send removal notify // so any lingering ghost entities disappear from the client board without 6900 error. if len(toConsume) == 0 { var removalUpdates []*pb.KFNCAIHIHCM for _, uid := range alreadyConsumed { removalUpdates = append(removalUpdates, &pb.KFNCAIHIHCM{ MNLCHEJKDHO: &pb.KFNCAIHIHCM_GOHHJLAPMBB{GOHHJLAPMBB: uid}, }) } if len(removalUpdates) > 0 { _ = cwSend(s, cwNotify(removalUpdates...)) } PacketLog(fmt.Sprintf("CW_USE_ORB ghost/already consumed uids=%v (acknowledged with removal sync)", alreadyConsumed)) return nil } 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(toConsume)) for _, uid := range toConsume { orb := owned[uid] if orb == nil || used[uid] { continue } 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. // Include both source1 group and general removal group for complete entity cleanup. if err = cwSend(s, &pb.GridFightSyncUpdateResultScNotify{ GCPNIHHBGGD: []*pb.ABPLALCHNFA{ { GridUpdateSrc: pb.JJDFGMMMCMI_JJDFGMMMCMI_DBIEKOMKBIN, OOEOCGLLHFO: []uint32{payout.UID}, JKPJFKCBIEM: payout.Updates, }, { JKPJFKCBIEM: []*pb.KFNCAIHIHCM{ {MNLCHEJKDHO: &pb.KFNCAIHIHCM_GOHHJLAPMBB{GOHHJLAPMBB: payout.UID}}, }, }, }, }); err != nil { return err } } if len(alreadyConsumed) > 0 { var removalUpdates []*pb.KFNCAIHIHCM for _, uid := range alreadyConsumed { removalUpdates = append(removalUpdates, &pb.KFNCAIHIHCM{ MNLCHEJKDHO: &pb.KFNCAIHIHCM_GOHHJLAPMBB{GOHHJLAPMBB: uid}, }) } _ = cwSend(s, cwNotify(removalUpdates...)) } 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 }