303 lines
9.4 KiB
Go
303 lines
9.4 KiB
Go
package gameserver
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"fmt"
|
|
"math/big"
|
|
|
|
"gac-hkrpg-go/internal/common"
|
|
pb "gac-hkrpg-go/protocol/proto"
|
|
)
|
|
|
|
func init() {
|
|
handle("GetGachaInfo", func(s *Session, req *pb.GetGachaInfoCsReq, rsp *pb.GetGachaInfoScRsp) error {
|
|
banners, _, err := common.LoadBrowsableGachaBanners()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(banners) == 0 {
|
|
rsp.Retcode = uint32(pb.Retcode_RET_GACHA_NOT_SUPPORT)
|
|
PacketLog("GACHA entry rejected: no verified installed banner assets")
|
|
return nil
|
|
}
|
|
config, err := common.LoadRuntimeConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, banner := range banners {
|
|
info := &pb.GachaInfo{GachaId: banner.ID, BeginTime: *config.ActivitySchedule.BeginTime, EndTime: *config.ActivitySchedule.EndTime, GachaCeiling: &pb.GachaCeiling{}}
|
|
if banner.AvatarID != 0 {
|
|
info.PrizeItemList = []uint32{banner.AvatarID}
|
|
info.ItemDetailList = []uint32{banner.AvatarID}
|
|
}
|
|
rsp.GachaInfoList = append(rsp.GachaInfoList, info)
|
|
}
|
|
// Synthetic IDs are not native Client config rows. The presentation
|
|
// adapter registers its own pools; sending these IDs in GetGachaInfo has
|
|
// caused Client response handlers to throw. Keep the preview opt-in.
|
|
mapped, _, err := common.LoadLimitedGachaBanners()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, _, policy, err := common.LoadSandboxDrawPool()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
count := policy.MappedBannerPreviewCount
|
|
if count > uint32(len(mapped)) {
|
|
count = uint32(len(mapped))
|
|
}
|
|
for i := uint32(0); i < count; i++ {
|
|
banner := mapped[i]
|
|
info := &pb.GachaInfo{GachaId: banner.GachaID, BeginTime: *config.ActivitySchedule.BeginTime, EndTime: *config.ActivitySchedule.EndTime, GachaCeiling: &pb.GachaCeiling{}}
|
|
info.PrizeItemList = []uint32{banner.AvatarID}
|
|
info.ItemDetailList = append([]uint32{banner.AvatarID}, banner.FourStarIDs...)
|
|
rsp.GachaInfoList = append(rsp.GachaInfoList, info)
|
|
}
|
|
PacketLog(fmt.Sprintf("GACHA_INFO banners=%d mapped_preview=%d", len(banners), count))
|
|
return nil
|
|
})
|
|
handle("DoGacha", func(s *Session, req *pb.DoGachaCsReq, rsp *pb.DoGachaScRsp) error {
|
|
rsp.GachaId = req.GachaId
|
|
rsp.Retcode = uint32(pb.Retcode_RET_GACHA_ID_NOT_EXIST)
|
|
banners, _, err := common.LoadFeaturedGachaBanners()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var avatarID uint32
|
|
for _, row := range banners {
|
|
if row.ID == req.GachaId {
|
|
avatarID = row.AvatarID
|
|
break
|
|
}
|
|
}
|
|
if avatarID == 0 {
|
|
mapped, _, err := common.LoadLimitedGachaBanners()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, row := range mapped {
|
|
if row.GachaID == req.GachaId {
|
|
return drawLimitedAvatar(s, req, rsp, row)
|
|
}
|
|
}
|
|
}
|
|
if avatarID != 0 {
|
|
return drawFeaturedAvatar(s, req, rsp, avatarID)
|
|
}
|
|
// Featured-only path found nothing. Verified browsable banners may
|
|
// still draw from the policy-declared sandbox pool: every pick is a
|
|
// duplicate grant on an avatar from the live imported roster, so no
|
|
// invented IDs or first-acquisition synthesis are involved.
|
|
sandboxDrawn, err := drawSandboxPool(s, req, rsp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if sandboxDrawn {
|
|
return nil
|
|
}
|
|
rsp.Retcode = uint32(pb.Retcode_RET_GACHA_NOT_SUPPORT)
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// drawFeaturedAvatar grants req.GachaNum copies of one featured avatar using
|
|
// its configured first Eidolon cost as the duplicate conversion for captured
|
|
// basename-derived banners. Mapped limited banners use drawLimitedAvatar.
|
|
func drawFeaturedAvatar(s *Session, req *pb.DoGachaCsReq, rsp *pb.DoGachaScRsp, avatarID uint32) error {
|
|
_, policy, err := common.LoadFeaturedGachaBanners()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rsp.Retcode = uint32(pb.Retcode_RET_GACHA_NUM_INVALID)
|
|
allowed := false
|
|
for _, n := range policy.DrawCounts {
|
|
allowed = allowed || n == req.GachaNum
|
|
}
|
|
if !allowed {
|
|
return nil
|
|
}
|
|
rsp.Retcode = uint32(pb.Retcode_RET_GACHA_NOT_SUPPORT)
|
|
if _, active := CURRENT_BATTLE.Load(s.Token); active {
|
|
return nil
|
|
}
|
|
player := s.JSON()
|
|
if player == nil || player.Avatars[avatarID] == nil {
|
|
return nil
|
|
}
|
|
resources, err := loadEidolonResources()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
row, exists := resources.Avatars[avatarID]
|
|
if !exists || len(row.Ranks) == 0 {
|
|
return nil
|
|
}
|
|
additions := map[uint32]uint32{}
|
|
for _, cost := range row.Ranks[0].Cost {
|
|
amount := uint64(cost.ItemNum)*uint64(req.GachaNum) + uint64(additions[cost.ItemID])
|
|
if amount > uint64(^uint32(0)) {
|
|
return nil
|
|
}
|
|
additions[cost.ItemID] = uint32(amount)
|
|
}
|
|
bag, err := bagMaterials(player)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
baseline := map[uint32]uint32{}
|
|
for _, item := range bag {
|
|
baseline[item.Tid] = item.Num
|
|
}
|
|
updated, err := common.GrantGachaDuplicate(avatarID, additions, baseline)
|
|
if err != nil {
|
|
PacketLog("GACHA rejected: " + err.Error())
|
|
return nil
|
|
}
|
|
if player.Materials == nil {
|
|
player.Materials = map[uint32]uint32{}
|
|
}
|
|
sync := &pb.PlayerSyncScNotify{}
|
|
for id, count := range updated {
|
|
player.Materials[id] = count
|
|
sync.MaterialList = append(sync.MaterialList, &pb.Material{Tid: id, Num: count})
|
|
}
|
|
for i := uint32(0); i < req.GachaNum; i++ {
|
|
transfer := &pb.ItemList{}
|
|
for _, cost := range row.Ranks[0].Cost {
|
|
transfer.ItemList = append(transfer.ItemList, &pb.Item{ItemId: cost.ItemID, Num: cost.ItemNum})
|
|
}
|
|
rsp.GachaItemList = append(rsp.GachaItemList, &pb.GachaItem{GachaItem: &pb.Item{ItemId: avatarID, Num: 1}, TokenItem: &pb.ItemList{}, TransferItemList: transfer})
|
|
}
|
|
if err = s.Send(sync); err != nil {
|
|
return err
|
|
}
|
|
rsp.Retcode = 0
|
|
rsp.GachaNum = req.GachaNum
|
|
PacketLog(fmt.Sprintf("GACHA banner=%d avatar=%d draws=%d sandbox=featured", req.GachaId, avatarID, req.GachaNum))
|
|
return nil
|
|
}
|
|
|
|
// drawSandboxPool serves a draw from the policy-declared sandbox pool for an
|
|
// asset-verified browsable banner. Every pick is a duplicate-material grant on
|
|
// an avatar from the live imported roster; nothing is synthesized. Returns
|
|
// false when the request is not a verified banner or the pool is disabled, so
|
|
// the caller keeps its original rejection.
|
|
func drawSandboxPool(s *Session, req *pb.DoGachaCsReq, rsp *pb.DoGachaScRsp) (bool, error) {
|
|
verified, _, err := common.LoadBrowsableGachaBanners()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
isVerified := false
|
|
for _, row := range verified {
|
|
isVerified = isVerified || row.ID == req.GachaId
|
|
}
|
|
if !isVerified {
|
|
return false, nil
|
|
}
|
|
enabled, pool, policy, err := common.LoadSandboxDrawPool()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if !enabled || len(pool) == 0 {
|
|
return false, nil
|
|
}
|
|
rsp.Retcode = uint32(pb.Retcode_RET_GACHA_NUM_INVALID)
|
|
allowed := false
|
|
for _, n := range policy.DrawCounts {
|
|
allowed = allowed || n == req.GachaNum
|
|
}
|
|
if !allowed {
|
|
return true, nil
|
|
}
|
|
rsp.Retcode = uint32(pb.Retcode_RET_GACHA_NOT_SUPPORT)
|
|
if _, active := CURRENT_BATTLE.Load(s.Token); active {
|
|
return true, nil
|
|
}
|
|
resources, err := loadEidolonResources()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
// Uniform sandbox picks. Avatars without a configured first-rank Eidolon
|
|
// cost are excluded: their duplicate grant has no declared conversion.
|
|
type rosterAvatar struct {
|
|
id uint32
|
|
itemID, itemNum uint32
|
|
}
|
|
effective := make([]rosterAvatar, 0, len(pool))
|
|
for _, id := range pool {
|
|
row, exists := resources.Avatars[id]
|
|
if !exists || len(row.Ranks) == 0 || len(row.Ranks[0].Cost) == 0 {
|
|
continue
|
|
}
|
|
effective = append(effective, rosterAvatar{id: id, itemID: row.Ranks[0].Cost[0].ItemID, itemNum: row.Ranks[0].Cost[0].ItemNum})
|
|
}
|
|
if len(effective) == 0 {
|
|
PacketLog("GACHA sandbox pool empty: no roster avatar with configured Eidolon cost")
|
|
return true, nil
|
|
}
|
|
player := s.JSON()
|
|
if player == nil {
|
|
return true, nil
|
|
}
|
|
picks := make([]rosterAvatar, 0, req.GachaNum)
|
|
for i := uint32(0); i < req.GachaNum; i++ {
|
|
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(effective))))
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
picks = append(picks, effective[n.Int64()])
|
|
}
|
|
counts := map[uint32]uint32{}
|
|
costOf := map[uint32]rosterAvatar{}
|
|
for _, pick := range picks {
|
|
counts[pick.id]++
|
|
costOf[pick.id] = pick
|
|
}
|
|
bag, err := bagMaterials(player)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
baseline := map[uint32]uint32{}
|
|
for _, item := range bag {
|
|
baseline[item.Tid] = item.Num
|
|
}
|
|
updated := map[uint32]uint32{}
|
|
for id, times := range counts {
|
|
cost := costOf[id]
|
|
total := uint64(cost.itemNum) * uint64(times)
|
|
if total > uint64(^uint32(0)) {
|
|
return true, nil
|
|
}
|
|
additions := map[uint32]uint32{cost.itemID: uint32(total)}
|
|
got, err := common.GrantGachaDuplicate(id, additions, baseline)
|
|
if err != nil {
|
|
PacketLog("GACHA sandbox rejected: " + err.Error())
|
|
return true, nil
|
|
}
|
|
for itemID, count := range got {
|
|
updated[itemID] = count
|
|
}
|
|
}
|
|
if player.Materials == nil {
|
|
player.Materials = map[uint32]uint32{}
|
|
}
|
|
sync := &pb.PlayerSyncScNotify{}
|
|
for id, count := range updated {
|
|
player.Materials[id] = count
|
|
sync.MaterialList = append(sync.MaterialList, &pb.Material{Tid: id, Num: count})
|
|
}
|
|
for _, pick := range picks {
|
|
transfer := &pb.ItemList{}
|
|
transfer.ItemList = append(transfer.ItemList, &pb.Item{ItemId: pick.itemID, Num: pick.itemNum})
|
|
rsp.GachaItemList = append(rsp.GachaItemList, &pb.GachaItem{GachaItem: &pb.Item{ItemId: pick.id, Num: 1}, TokenItem: &pb.ItemList{}, TransferItemList: transfer})
|
|
}
|
|
if err = s.Send(sync); err != nil {
|
|
return false, err
|
|
}
|
|
rsp.Retcode = 0
|
|
rsp.GachaNum = req.GachaNum
|
|
PacketLog(fmt.Sprintf("GACHA banner=%d draws=%d sandbox=imported_roster pool=%d distinct=%d", req.GachaId, req.GachaNum, len(effective), len(counts)))
|
|
return true, nil
|
|
}
|