package gameserver import ( "crypto/rand" "fmt" "maps" "math/big" "gac-hkrpg-go/internal/common" pb "gac-hkrpg-go/protocol/proto" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" ) func cwGoldSync(gold uint32) *pb.KFNCAIHIHCM { return &pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_ItemValue{ItemValue: gold}} } func cwNextEntityID(run *pb.MPIMAEMCEFH) (uint32, error) { var max uint32 var visit func(protoreflect.Message) visit = func(m protoreflect.Message) { m.Range(func(f protoreflect.FieldDescriptor, v protoreflect.Value) bool { if string(f.Name()) == "unique_id" && f.Kind() == protoreflect.Uint32Kind && uint32(v.Uint()) > max { max = uint32(v.Uint()) } if f.IsList() && f.Message() != nil { a := v.List() for i := 0; i < a.Len(); i++ { visit(a.Get(i).Message()) } } else if f.IsMap() && f.MapValue().Message() != nil { v.Map().Range(func(_ protoreflect.MapKey, x protoreflect.Value) bool { visit(x.Message()); return true }) } else if !f.IsList() && !f.IsMap() && f.Message() != nil { visit(v.Message()) } return true }) } visit(run.ProtoReflect()) if max == ^uint32(0) { return 0, fmt.Errorf("CW entity identifiers exhausted") } return max + 1, nil } func cwPreparingData(s *Session) (*common.CurrencyWarsCatalog, *pb.GridFightGetDataScRsp, error) { catalog, err := common.LoadCurrencyWarsCatalog() if err != nil { return nil, nil, err } data, revision, err := readCurrencyWarsData(catalog) if err != nil { return nil, nil, err } if data == nil || data.DHFOFLFCLAH == nil || data.DHFOFLFCLAH.FMIGOKIDKJL != nil { return nil, nil, fmt.Errorf("CW is not preparing") } if revision != s.currencyWarsRevision { return nil, nil, common.ErrCurrencyWarsSaveConflict } if _, battle := CURRENT_BATTLE.Load(s.Token); battle { return nil, nil, fmt.Errorf("battle is active") } return catalog, data, nil } func init() { // Native replay confirms HFCBOIOAECK removes an owned role. Its command // is resolved by the current table, including the actual typed response. cwHandle("GridFightRecycleRole", func(s *Session, req *pb.GridFightRecycleRoleCsReq, rsp *pb.GridFightRecycleRoleScRsp) error { rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_PARAM_NOT_MATCH) catalog, data, err := cwPreparingData(s) if err != nil { PacketLog("CW_SELL rejected: " + err.Error()) return nil } player, team, _ := cwComponents(data.DHFOFLFCLAH) if player == nil || team == nil { return nil } index := -1 for i, role := range team.GridGameRoleList { if role.UniqueId == req.UniqueId { index = i break } } if index < 0 { rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_ROLE_NOT_EXIST) return nil } role := team.GridGameRoleList[index] if len(role.PNJIPHJJONG) != 0 { // Returning attached equipment needs its own observed ownership sync. // Do not remove the role and leave the Client with orphaned equipment. PacketLog("CW_SELL rejected: unequip the role before selling") return nil } price, err := catalog.CapturedSalePrice(role.Id, role.RoleStar) if err != nil { rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_CONF_MISS) PacketLog("CW_SELL rejected: " + err.Error()) return nil } gold := uint64(player.Gold) + uint64(price) if gold > uint64(^uint32(0)) { gold = uint64(^uint32(0)) } player.Gold = uint32(gold) team.GridGameRoleList = append(team.GridGameRoleList[:index], team.GridGameRoleList[index+1:]...) if err = s.saveCurrencyWarsData(data, catalog); err != nil { PacketLog("CW_SELL save failed: " + err.Error()) return nil } notify := cwNotify(cwGoldSync(player.Gold), &pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_HFCBOIOAECK{HFCBOIOAECK: req.UniqueId}}) notify.GCPNIHHBGGD[0].GridUpdateSrc = pb.JJDFGMMMCMI_JJDFGMMMCMI_JOMIDGFCHPH if err = cwSend(s, notify); err != nil { return err } rsp.Retcode = 0 PacketLog(fmt.Sprintf("CW_SELL saved unique=%d role=%d credit=%d gold=%d revision=%d", req.UniqueId, role.Id, price, player.Gold, s.currencyWarsRevision)) return nil }) // Current table names this response BuyGoodRsp. Controlled native capture // confirms that name/command, including zero-based indices and sync-before-rsp. cwHandleNamed("GridFightBuyGoods", "BuyGoodRsp", func(s *Session, req *pb.GridFightBuyGoodsCsReq, rsp *pb.BuyGoodRsp) error { rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_PARAM_NOT_MATCH) catalog, data, err := cwPreparingData(s) if err != nil { PacketLog("CW_BUY rejected: " + err.Error()) return nil } run := data.DHFOFLFCLAH player, team, shop := cwComponents(run) if player == nil || team == nil || shop == nil { return nil } indices := append([]uint32(nil), req.CGAFCMIAKLI...) if req.CIBONIOHELB { indices = nil for i, g := range shop.OGDKJPDFFAI { if !g.LHGIFDNPIDE { indices = append(indices, uint32(i)) } } } if len(indices) == 0 { return nil } used := map[uint32]bool{} var cost uint64 for _, i := range indices { if used[i] || uint64(i) >= uint64(len(shop.OGDKJPDFFAI)) { return nil } used[i] = true offer := shop.OGDKJPDFFAI[i] if offer.LHGIFDNPIDE || offer.GetBDPIMPJOJBK() == nil { return nil } cost += uint64(offer.KDKPDJNMMCM) } if cost > uint64(player.Gold) { rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_COIN_NOT_ENOUGH) return nil } // Initial-entry prototype supplies shared property scales. Refuse to copy // character-specific modifiers into another role if this assumption changes. if len(team.GridGameRoleList) == 0 { return nil } scales := team.GridGameRoleList[0].ConvertPropertyToFixpoint for _, r := range team.GridGameRoleList { if !maps.Equal(scales, r.ConvertPropertyToFixpoint) { PacketLog(fmt.Sprintf("CW_BUY rejected: party property scales differ role_uid=%d", r.UniqueId)) rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_CONF_MISS) return nil } } front, e := catalog.IntegerRule("GridFight_Front_AvatarMaxNum") if e != nil { return e } back, e := catalog.IntegerRule("GridFight_Back_AvatarMaxNum") if e != nil { return e } var groups []*pb.ABPLALCHNFA for _, i := range indices { offer := shop.OGDKJPDFFAI[i] roleOffer := offer.GetBDPIMPJOJBK() if _, e := catalog.CapturedRoleVariant(roleOffer.RoleId, roleOffer.RoleStar); e != nil { PacketLog(fmt.Sprintf("CW_BUY rejected: missing captured role variant role=%d star=%d error=%v", roleOffer.RoleId, roleOffer.RoleStar, e)) rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_CONF_MISS) return nil } var pos uint32 occupied := map[uint32]bool{} for _, r := range team.GridGameRoleList { occupied[r.Pos] = true } for _, r := range team.KCOMPKMJCMC { occupied[r.Pos] = true } for _, f := range team.PMEOPPDAMLM { occupied[f.Pos] = true } for p := uint32(front+back) + 1; uint64(p) <= uint64(front+back)+uint64(player.MICEOJBAKGN); p++ { if !occupied[p] { pos = p break } } id, e := cwNextEntityID(run) if e != nil { return e } role := &pb.CDEAABODKCP{Id: roleOffer.RoleId, RoleStar: roleOffer.RoleStar, UniqueId: id, Pos: pos, ConvertPropertyToFixpoint: maps.Clone(scales)} team.GridGameRoleList = append(team.GridGameRoleList, role) offer.LHGIFDNPIDE = true groups = append(groups, cwNotify(&pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_PIDDDHKPDDM{PIDDDHKPDDM: role}}).GCPNIHHBGGD[0]) merges, err := cwMergeRoles(catalog, run, id) if err != nil { PacketLog("CW_BUY merge rejected: " + err.Error()) return nil } groups = append(groups, merges...) // A transient unplaced third copy is legal only if the merge consumes // it. A rejected transaction never writes this detached snapshot. for _, r := range team.GridGameRoleList { if r.Pos == 0 { rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_NO_EMPTY_POS) return nil } } } player.Gold -= uint32(cost) if err = s.saveCurrencyWarsData(data, catalog); err != nil { PacketLog("CW_BUY save failed: " + err.Error()) return nil } notify := cwNotify(cwGoldSync(player.Gold)) notify.GCPNIHHBGGD[0].OOEOCGLLHFO = indices notify.GCPNIHHBGGD = append(notify.GCPNIHHBGGD, groups...) notify.GCPNIHHBGGD = append(notify.GCPNIHHBGGD, cwNotify(cwShopSync(shop)).GCPNIHHBGGD...) if err = cwSend(s, notify); err != nil { return err } rsp.CGAFCMIAKLI = indices rsp.Retcode = 0 PacketLog(fmt.Sprintf("CW_BUY saved slots=%v gold=%d revision=%d", indices, player.Gold, s.currencyWarsRevision)) return nil }) cwHandle("GridFightLockShop", func(s *Session, req *pb.GridFightLockShopCsReq, rsp *pb.GridFightLockShopScRsp) error { rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_PARAM_NOT_MATCH) catalog, data, err := cwPreparingData(s) if err != nil { return nil } _, _, shop := cwComponents(data.DHFOFLFCLAH) if shop == nil { return nil } shop.ABGEKPJHHIA = req.IsLocked if err = s.saveCurrencyWarsData(data, catalog); err != nil { return nil } if err = cwSend(s, cwNotify(cwShopSync(shop))); err != nil { return err } rsp.Retcode = 0 return nil }) cwHandle("GridFightRefreshShop", func(s *Session, _ *pb.GridFightRefreshShopCsReq, rsp *pb.GridFightRefreshShopScRsp) error { rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_PARAM_NOT_MATCH) catalog, data, err := cwPreparingData(s) if err != nil { return nil } player, team, shop := cwComponents(data.DHFOFLFCLAH) if player == nil || team == nil || shop == nil { return nil } if shop.OPDCMCFOFAA == 0 && player.Gold < shop.NFGMOPCCPIC { rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_COIN_NOT_ENOUGH) return nil } pool, err := catalog.NewCapturedCardPool() if err != nil { return err } reserve := func(id uint32) error { r, ok := pool.Roles[id] if !ok { return nil } if r.Remaining == 0 { return fmt.Errorf("CW pool exhausted for %d", id) } r.Remaining-- pool.Roles[id] = r return nil } for _, r := range team.GridGameRoleList { if e := pool.ReserveOwned(catalog, r.Id, r.RoleStar); e != nil { return e } } current := &common.CurrencyWarsShop{Pool: pool, Locked: shop.ABGEKPJHHIA, FreeRefreshTimes: shop.OPDCMCFOFAA} for _, g := range shop.OGDKJPDFFAI { r := g.GetBDPIMPJOJBK() if r == nil { return nil } if !g.LHGIFDNPIDE { if err = reserve(r.RoleId); err != nil { return err } } current.Offers = append(current.Offers, common.CurrencyWarsShopOffer{RoleID: r.RoleId, Star: r.RoleStar, Cost: g.KDKPDJNMMCM, Sold: g.LHGIFDNPIDE}) } rules, err := catalog.BaseShopRules() if err != nil { return err } rules.RefreshCost = shop.NFGMOPCCPIC level, err := catalog.PlayerLevel(player.EHNJDIDABHD) if err != nil { return err } weights := level.RarityWeights() next, gold, err := current.PlanRefresh(catalog, rules, player.Gold, weights[:], common.CurrencyWarsDrawRemainingCopies, func(upper uint64) (uint64, error) { v, e := rand.Int(rand.Reader, new(big.Int).SetUint64(upper)) if e != nil { return 0, e } return v.Uint64(), nil }, func(id uint32) (uint32, uint32, error) { r, e := catalog.CapturedRole(id) return r.DefaultStar, r.Price, e }) if err != nil { PacketLog("CW_REFRESH rejected: " + err.Error()) return nil } shop.OGDKJPDFFAI = nil for _, offer := range next.Offers { g, e := currencyWarsRoleOfferProto(catalog, offer, pb.FKGIEPHGACD_FKGIEPHGACD_EJKDKAJPDCO) if e != nil { return e } shop.OGDKJPDFFAI = append(shop.OGDKJPDFFAI, g) } player.Gold = gold shop.OPDCMCFOFAA = next.FreeRefreshTimes if err = s.saveCurrencyWarsData(data, catalog); err != nil { return nil } if err = cwSend(s, cwNotify(cwGoldSync(gold), cwShopSync(shop))); err != nil { return err } rsp.Retcode = 0 return nil }) // This revision omits Cs in the request's name; bind that exact table name. rawNamed("GridFightBackToPrepareReq", func(s *Session, payload []byte) error { req := new(pb.GridFightBackToPrepareReq) if err := proto.Unmarshal(payload, req); err != nil { return err } rsp := &pb.GridFightBackToPrepareScRsp{Retcode: uint32(pb.Retcode_RET_GRID_FIGHT_PARAM_NOT_MATCH)} _, data, err := cwPreparingData(s) if err == nil { _, _, shop := cwComponents(data.DHFOFLFCLAH) if shop != nil { if err = cwSend(s, cwNotify(cwShopSync(shop), cwUnlock())); err != nil { return err } rsp.Retcode = 0 } } return cwSend(s, rsp) }) }