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

387 lines
12 KiB
Go

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)
})
}