387 lines
12 KiB
Go
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)
|
|
})
|
|
}
|