package gameserver import ( "crypto/rand" "encoding/json" "fmt" "math/big" "gac-hkrpg-go/internal/common" pb "gac-hkrpg-go/protocol/proto" "google.golang.org/protobuf/proto" ) // Wire names are revision-specific. Mappings were established by the native // RoundBegin -> ReturnPreparation -> UpdatePos replay, not opcode aliases. func cwComponents(run *pb.MPIMAEMCEFH) (player *pb.NIHNGPKHEPC, team *pb.HKFECLCHBFJ, shop *pb.POCJDMMANBM) { for _, c := range run.NAGDJGMMCBE { if v := c.GetFAOCFBOFCJP(); v != nil { player = v } if v := c.GetEJIPINIKNCK(); v != nil { team = v } if v := c.GetEBHDIOGALIE(); v != nil { shop = v } } return } func cwNotify(updates ...*pb.KFNCAIHIHCM) *pb.GridFightSyncUpdateResultScNotify { return &pb.GridFightSyncUpdateResultScNotify{GCPNIHHBGGD: []*pb.ABPLALCHNFA{{JKPJFKCBIEM: updates}}} } func cwFinish(queue uint32) *pb.KFNCAIHIHCM { return &pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_PMFBIGLPJJA{PMFBIGLPJJA: queue}} } func cwUnlock() *pb.KFNCAIHIHCM { return &pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_JOMFJHOCHJP{JOMFJHOCHJP: &pb.GMBMNFLMKPI{}}} } func cwShopSync(shop *pb.POCJDMMANBM) *pb.KFNCAIHIHCM { return &pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_MOFNIOJHBBL{MOFNIOJHBBL: &pb.HONNGJEMGGH{ ABGEKPJHHIA: shop.ABGEKPJHHIA, OPDCMCFOFAA: shop.OPDCMCFOFAA, NFGMOPCCPIC: shop.NFGMOPCCPIC, ABBNGCPECAI: shop.ABBNGCPECAI, OGDKJPDFFAI: shop.OGDKJPDFFAI, }}} } func cwInitialRound(catalog *common.CurrencyWarsCatalog, run *pb.MPIMAEMCEFH) (*pb.GridFightSyncUpdateResultScNotify, error) { raw, err := common.ReadResourceFile("resources/CurrencyWarsPreparation.json") if err != nil { return nil, err } var recipe struct { Format uint32 `json:"format_version"` Client string `json:"target_client_version"` Assembly string `json:"target_assembly_sha256"` Pending []byte `json:"return_preparation_wire"` Policy common.CurrencyWarsDrawPolicy `json:"shop_draw_policy"` } if err = json.Unmarshal(raw, &recipe); err != nil { return nil, err } if recipe.Format != 1 || recipe.Client != catalog.ClientVersion || recipe.Assembly != catalog.Provenance.GameAssemblySHA256 { return nil, fmt.Errorf("CW preparation recipe revision differs") } pending := new(pb.CHCHAJIANIM) if err = proto.Unmarshal(recipe.Pending, pending); err != nil { return nil, err } if pending.GetLGNPHHBGKFH() == nil || pending.QueuePosition <= run.FMIGOKIDKJL.QueuePosition { return nil, fmt.Errorf("CW preparation recipe has invalid pending action") } player, team, shop := cwComponents(run) if player == nil || team == nil || shop == nil || len(shop.OGDKJPDFFAI) != 0 { return nil, fmt.Errorf("CW initial round components differ") } pool, err := catalog.NewCapturedCardPool() if err != nil { return nil, err } // Reserve starter copies from the same finite pool before drawing the shop. for _, role := range team.GridGameRoleList { r, ok := pool.Roles[role.Id] if !ok { continue } config, e := catalog.CapturedRole(role.Id) if e != nil { return nil, e } if role.RoleStar != config.DefaultStar || r.Remaining == 0 { return nil, fmt.Errorf("CW starter pool reservation is not supported") } r.Remaining-- pool.Roles[role.Id] = r } level, err := catalog.PlayerLevel(player.EHNJDIDABHD) if err != nil { return nil, err } weights := level.RarityWeights() rules, err := catalog.BaseShopRules() if err != nil { return nil, err } planned, err := (&common.CurrencyWarsShop{Pool: pool}).PlanEnterNode(catalog, rules, weights[:], recipe.Policy, 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 { return nil, err } for _, offer := range planned.Offers { value, e := currencyWarsRoleOfferProto(catalog, offer, pb.FKGIEPHGACD_FKGIEPHGACD_EJKDKAJPDCO) if e != nil { return nil, e } shop.OGDKJPDFFAI = append(shop.OGDKJPDFFAI, value) } shop.NFGMOPCCPIC = rules.RefreshCost queue := run.FMIGOKIDKJL.QueuePosition run.FMIGOKIDKJL = pending player.GAJGBBFELEL = &pb.GMBMNFLMKPI{IBCPAKJAPFF: pb.KMJNPEMPNMC_KMJNPEMPNMC_AEINKINPAFL, LMICDNNABNM: pb.GGNKDLAHAGH_GGNKDLAHAGH_FHOHPFBDNOH} return cwNotify(cwShopSync(shop), cwFinish(queue), cwUnlock(), &pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_JOMFJHOCHJP{JOMFJHOCHJP: player.GAJGBBFELEL}}, &pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_FMIGOKIDKJL{FMIGOKIDKJL: pending}}), nil } func cwReturnPreparation(run *pb.MPIMAEMCEFH) (*pb.GridFightSyncUpdateResultScNotify, error) { player, team, shop := cwComponents(run) if player == nil || team == nil || shop == nil { return nil, fmt.Errorf("CW preparation components missing") } queue := run.FMIGOKIDKJL.QueuePosition run.FMIGOKIDKJL = nil player.GAJGBBFELEL = &pb.GMBMNFLMKPI{} return cwNotify(cwShopSync(shop), cwFinish(queue), cwUnlock()), nil } // Refresh a node's shop without charging the player. Pool accounting includes // owned copies and unsold reservations, and a locked shop keeps its offers. func cwRestockForNode(catalog *common.CurrencyWarsCatalog, run *pb.MPIMAEMCEFH) error { player, team, shop := cwComponents(run) if player == nil || team == nil || shop == nil { return fmt.Errorf("CW node shop components missing") } 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 card pool exhausted") } 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 fmt.Errorf("CW special shop offer not supported") } 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 } level, err := catalog.PlayerLevel(player.EHNJDIDABHD) if err != nil { return err } weights := level.RarityWeights() next, err := current.PlanEnterNode(catalog, rules, 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 { return err } shop.OGDKJPDFFAI = nil for _, offer := range next.Offers { value, e := currencyWarsRoleOfferProto(catalog, offer, pb.FKGIEPHGACD_FKGIEPHGACD_EJKDKAJPDCO) if e != nil { return e } shop.OGDKJPDFFAI = append(shop.OGDKJPDFFAI, value) } shop.NFGMOPCCPIC = rules.RefreshCost return nil } func cwNextRound(catalog *common.CurrencyWarsCatalog, run *pb.MPIMAEMCEFH) (*pb.GridFightSyncUpdateResultScNotify, error) { stage, _ := cwStage(run) if stage == nil || run.FMIGOKIDKJL == nil { return nil, fmt.Errorf("CW next round missing") } entries, err := cwNodeTransitions(catalog) if err != nil { return nil, err } var pending *pb.CHCHAJIANIM for _, e := range entries { level := new(pb.AINFNFIDGFF) before := new(pb.CHCHAJIANIM) if err = proto.Unmarshal(e.LevelWire, level); err != nil { return nil, err } if err = proto.Unmarshal(e.PendingWire, before); err != nil { return nil, err } if e.Route == stage.EMOHLJHPOPC && e.Division == run.DivisionId && e.Overclock == run.IsOverlock && level.DCAHJELNJNP == stage.DCAHJELNJNP && level.SectionId == stage.SectionId && before.QueuePosition == run.FMIGOKIDKJL.QueuePosition { if pending != nil { return nil, fmt.Errorf("CW next round recipe duplicated") } pending = new(pb.CHCHAJIANIM) if err = proto.Unmarshal(e.RoundWire, pending); err != nil { return nil, err } } } if pending == nil { pending, err = cwGeneratedRoundPreparation(catalog, run) if err != nil { return nil, err } } if pending.QueuePosition <= run.FMIGOKIDKJL.QueuePosition { return nil, fmt.Errorf("CW next round recipe missing") } if err = cwRestockForNode(catalog, run); err != nil { return nil, err } player, _, shop := cwComponents(run) queue := run.FMIGOKIDKJL.QueuePosition run.FMIGOKIDKJL = pending aligned, err := cwAlignEncounterCamp(catalog, run) if err != nil { return nil, err } updates := []*pb.KFNCAIHIHCM{cwShopSync(shop), cwFinish(queue), cwUnlock(), {MNLCHEJKDHO: &pb.KFNCAIHIHCM_JOMFJHOCHJP{JOMFJHOCHJP: player.GAJGBBFELEL}}} if aligned { updates = append(updates, &pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_OOICHLAMNIG{OOICHLAMNIG: &pb.AINFNFIDGFF{DCAHJELNJNP: stage.DCAHJELNJNP, SectionId: stage.SectionId, IMPNAHBCADN: stage.IMPNAHBCADN}}}) } updates = append(updates, &pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_FMIGOKIDKJL{FMIGOKIDKJL: pending}}) return cwNotify(updates...), nil } func init() { cwHandle("GridFightUpdatePos", func(s *Session, req *pb.GridFightUpdatePosCsReq, rsp *pb.GridFightUpdatePosScRsp) error { rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_PARAM_NOT_MATCH) if _, active := CURRENT_BATTLE.Load(s.Token); active { PacketLog("CW_UPDATE_POS rejected reason=battle_active") return nil } catalog, err := common.LoadCurrencyWarsCatalog() if err != nil { return err } data, revision, err := readCurrencyWarsData(catalog) if err != nil { return err } if data == nil || data.DHFOFLFCLAH == nil { rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_NOT_IN_GAMEPLAY) return nil } if revision != s.currencyWarsRevision { return nil } run := data.DHFOFLFCLAH if run.FMIGOKIDKJL != nil || len(req.AAKDHMHCMLP) == 0 { PacketLog("CW_UPDATE_POS rejected reason=not_preparing_or_empty") return nil } player, team, _ := cwComponents(run) if player == nil || team == nil { return nil } front, err := catalog.IntegerRule("GridFight_Front_AvatarMaxNum") if err != nil { return err } back, err := catalog.IntegerRule("GridFight_Back_AvatarMaxNum") if err != nil { return err } activeEnd := uint64(front + back) overflow, err := catalog.IntegerRule("GridFight_Bench_OverFlow_AvatarNum") if err != nil { return err } benchEnd := activeEnd + uint64(player.MICEOJBAKGN) + uint64(overflow) changes := map[uint32]uint32{} for _, v := range req.AAKDHMHCMLP { if v == nil || v.UniqueId == 0 { return nil } if _, ok := changes[v.UniqueId]; ok { return nil } if uint64(v.Pos) > benchEnd { rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_POS_NOT_VALID) return nil } changes[v.UniqueId] = v.Pos } // Validate final positions together, so swaps need no intermediate empty slot. occupied := map[uint32]bool{} owned := map[uint32]bool{} active := uint32(0) activeAvatars := map[uint32]bool{} for _, role := range team.GridGameRoleList { owned[role.UniqueId] = true pos := role.Pos if v, ok := changes[role.UniqueId]; ok { pos = v } if pos != 0 { if occupied[pos] { rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_POS_EXIST_ROLE) return nil } occupied[pos] = true } if pos != 0 && uint64(pos) <= activeEnd { config, err := catalog.CapturedRoleVariant(role.Id, role.RoleStar) if err != nil { return err } if activeAvatars[config.Config.AvatarID] { rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_SAME_ROLE_IN_BATTLE) return nil } activeAvatars[config.Config.AvatarID] = true active++ } } for _, forge := range team.KCOMPKMJCMC { pos := forge.Pos if _, changed := changes[forge.UniqueId]; changed { return nil } // Projection movement needs its separately verified sync arm. if pos != 0 { if occupied[pos] { rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_POS_EXIST_ROLE) return nil } occupied[pos] = true } } for _, forge := range team.PMEOPPDAMLM { owned[forge.UniqueId] = true pos := forge.Pos if v, ok := changes[forge.UniqueId]; ok { pos = v } if pos == 0 || uint64(pos) <= activeEnd || uint64(pos) > benchEnd { return nil } if occupied[pos] { rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_POS_EXIST_ROLE) return nil } occupied[pos] = true } for id := range changes { if !owned[id] { rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_ROLE_NOT_EXIST) return nil } } if active > player.AFFCFLFIFPH { rsp.Retcode = uint32(pb.Retcode_RET_GRID_FIGHT_REGION_FULL) return nil } var updates []*pb.KFNCAIHIHCM for _, role := range team.GridGameRoleList { if pos, ok := changes[role.UniqueId]; ok { role.Pos = pos updates = append(updates, &pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_CFOCDALNCBP{CFOCDALNCBP: role}}) } } for _, forge := range team.PMEOPPDAMLM { if pos, ok := changes[forge.UniqueId]; ok { forge.Pos = pos updates = append(updates, &pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_FEKCPKAPHHO{FEKCPKAPHHO: forge}}) } } if err = s.saveCurrencyWarsData(data, catalog); err != nil { return err } if err = cwSend(s, cwNotify(updates...)); err != nil { return err } rsp.AAKDHMHCMLP = req.AAKDHMHCMLP rsp.Retcode = 0 PacketLog(fmt.Sprintf("CW_UPDATE_POS saved count=%d active=%d revision=%d", len(changes), active, s.currencyWarsRevision)) return nil }) }