Files
2026-10-28 18:54:43 +07:00

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
}