mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 15:43:49 +01:00
fix(gacha): validate reward pool before charging currency (#175)
Filter out gacha entries with no items at the query level (EXISTS subquery) and reorder Play methods to validate the pool before calling transact(), so players are never charged for a misconfigured gacha.
This commit is contained in:
@@ -186,7 +186,10 @@ func TestHandleMsgMhfReceiveGachaItem_Freeze(t *testing.T) {
|
|||||||
|
|
||||||
func TestHandleMsgMhfPlayNormalGacha_TransactError(t *testing.T) {
|
func TestHandleMsgMhfPlayNormalGacha_TransactError(t *testing.T) {
|
||||||
server := createMockServer()
|
server := createMockServer()
|
||||||
gachaRepo := &mockGachaRepo{txErr: errors.New("transact failed")}
|
gachaRepo := &mockGachaRepo{
|
||||||
|
txErr: errors.New("transact failed"),
|
||||||
|
rewardPool: []GachaEntry{{ID: 10, Weight: 100}},
|
||||||
|
}
|
||||||
server.gachaRepo = gachaRepo
|
server.gachaRepo = gachaRepo
|
||||||
server.userRepo = &mockUserRepoGacha{}
|
server.userRepo = &mockUserRepoGacha{}
|
||||||
ensureGachaService(server)
|
ensureGachaService(server)
|
||||||
@@ -269,7 +272,10 @@ func TestHandleMsgMhfPlayNormalGacha_Success(t *testing.T) {
|
|||||||
|
|
||||||
func TestHandleMsgMhfPlayStepupGacha_TransactError(t *testing.T) {
|
func TestHandleMsgMhfPlayStepupGacha_TransactError(t *testing.T) {
|
||||||
server := createMockServer()
|
server := createMockServer()
|
||||||
gachaRepo := &mockGachaRepo{txErr: errors.New("transact failed")}
|
gachaRepo := &mockGachaRepo{
|
||||||
|
txErr: errors.New("transact failed"),
|
||||||
|
rewardPool: []GachaEntry{{ID: 10, Weight: 100}},
|
||||||
|
}
|
||||||
server.gachaRepo = gachaRepo
|
server.gachaRepo = gachaRepo
|
||||||
server.userRepo = &mockUserRepoGacha{}
|
server.userRepo = &mockUserRepoGacha{}
|
||||||
ensureGachaService(server)
|
ensureGachaService(server)
|
||||||
@@ -473,7 +479,10 @@ func TestHandleMsgMhfGetBoxGachaInfo_Success(t *testing.T) {
|
|||||||
|
|
||||||
func TestHandleMsgMhfPlayBoxGacha_TransactError(t *testing.T) {
|
func TestHandleMsgMhfPlayBoxGacha_TransactError(t *testing.T) {
|
||||||
server := createMockServer()
|
server := createMockServer()
|
||||||
gachaRepo := &mockGachaRepo{txErr: errors.New("transact failed")}
|
gachaRepo := &mockGachaRepo{
|
||||||
|
txErr: errors.New("transact failed"),
|
||||||
|
rewardPool: []GachaEntry{{ID: 10, Weight: 100}},
|
||||||
|
}
|
||||||
server.gachaRepo = gachaRepo
|
server.gachaRepo = gachaRepo
|
||||||
server.userRepo = &mockUserRepoGacha{}
|
server.userRepo = &mockUserRepoGacha{}
|
||||||
ensureGachaService(server)
|
ensureGachaService(server)
|
||||||
|
|||||||
@@ -29,10 +29,14 @@ func (r *GachaRepository) GetEntryForTransaction(gachaID uint32, rollID uint8) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetRewardPool returns the entry_type=100 reward pool for a gacha, ordered by weight descending.
|
// GetRewardPool returns the entry_type=100 reward pool for a gacha, ordered by weight descending.
|
||||||
|
// Entries with no corresponding items in gacha_items are excluded.
|
||||||
func (r *GachaRepository) GetRewardPool(gachaID uint32) ([]GachaEntry, error) {
|
func (r *GachaRepository) GetRewardPool(gachaID uint32) ([]GachaEntry, error) {
|
||||||
var entries []GachaEntry
|
var entries []GachaEntry
|
||||||
rows, err := r.db.Queryx(
|
rows, err := r.db.Queryx(
|
||||||
`SELECT id, weight, rarity FROM gacha_entries WHERE gacha_id = $1 AND entry_type = 100 ORDER BY weight DESC`,
|
`SELECT e.id, e.weight, e.rarity FROM gacha_entries e
|
||||||
|
WHERE e.gacha_id = $1 AND e.entry_type = 100
|
||||||
|
AND EXISTS (SELECT 1 FROM gacha_items gi WHERE gi.entry_id = e.id)
|
||||||
|
ORDER BY e.weight DESC`,
|
||||||
gachaID,
|
gachaID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -164,14 +164,17 @@ func rewardsToItems(rewards []GachaReward) []GachaItem {
|
|||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlayNormalGacha processes a normal gacha roll: deducts cost, selects random
|
// PlayNormalGacha processes a normal gacha roll: validates the reward pool,
|
||||||
// rewards, saves items, and returns the result.
|
// deducts cost, selects random rewards, saves items, and returns the result.
|
||||||
func (svc *GachaService) PlayNormalGacha(userID, charID, gachaID uint32, rollType uint8) (*GachaPlayResult, error) {
|
func (svc *GachaService) PlayNormalGacha(userID, charID, gachaID uint32, rollType uint8) (*GachaPlayResult, error) {
|
||||||
rolls, err := svc.transact(userID, charID, gachaID, rollType)
|
entries, err := svc.gachaRepo.GetRewardPool(gachaID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
entries, err := svc.gachaRepo.GetRewardPool(gachaID)
|
if len(entries) == 0 {
|
||||||
|
return nil, errors.New("gacha has no valid reward entries")
|
||||||
|
}
|
||||||
|
rolls, err := svc.transact(userID, charID, gachaID, rollType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -180,9 +183,17 @@ func (svc *GachaService) PlayNormalGacha(userID, charID, gachaID uint32, rollTyp
|
|||||||
return &GachaPlayResult{Rewards: rewards}, nil
|
return &GachaPlayResult{Rewards: rewards}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlayStepupGacha processes a stepup gacha roll: deducts cost, advances step,
|
// PlayStepupGacha processes a stepup gacha roll: validates the reward pool,
|
||||||
// awards frontier points, selects random + guaranteed rewards, and saves items.
|
// deducts cost, advances step, awards frontier points, selects random +
|
||||||
|
// guaranteed rewards, and saves items.
|
||||||
func (svc *GachaService) PlayStepupGacha(userID, charID, gachaID uint32, rollType uint8) (*StepupPlayResult, error) {
|
func (svc *GachaService) PlayStepupGacha(userID, charID, gachaID uint32, rollType uint8) (*StepupPlayResult, error) {
|
||||||
|
entries, err := svc.gachaRepo.GetRewardPool(gachaID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(entries) == 0 {
|
||||||
|
return nil, errors.New("gacha has no valid reward entries")
|
||||||
|
}
|
||||||
rolls, err := svc.transact(userID, charID, gachaID, rollType)
|
rolls, err := svc.transact(userID, charID, gachaID, rollType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -197,11 +208,6 @@ func (svc *GachaService) PlayStepupGacha(userID, charID, gachaID uint32, rollTyp
|
|||||||
svc.logger.Error("Failed to insert gacha stepup state", zap.Error(err))
|
svc.logger.Error("Failed to insert gacha stepup state", zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
entries, err := svc.gachaRepo.GetRewardPool(gachaID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
guaranteedItems, _ := svc.gachaRepo.GetGuaranteedItems(rollType, gachaID)
|
guaranteedItems, _ := svc.gachaRepo.GetGuaranteedItems(rollType, gachaID)
|
||||||
randomRewards := svc.resolveRewards(entries, rolls, false)
|
randomRewards := svc.resolveRewards(entries, rolls, false)
|
||||||
|
|
||||||
@@ -223,14 +229,18 @@ func (svc *GachaService) PlayStepupGacha(userID, charID, gachaID uint32, rollTyp
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlayBoxGacha processes a box gacha roll: deducts cost, selects random entries
|
// PlayBoxGacha processes a box gacha roll: validates the reward pool, deducts
|
||||||
// without replacement, records drawn entries, saves items, and returns the result.
|
// cost, selects random entries without replacement, records drawn entries,
|
||||||
|
// saves items, and returns the result.
|
||||||
func (svc *GachaService) PlayBoxGacha(userID, charID, gachaID uint32, rollType uint8) (*GachaPlayResult, error) {
|
func (svc *GachaService) PlayBoxGacha(userID, charID, gachaID uint32, rollType uint8) (*GachaPlayResult, error) {
|
||||||
rolls, err := svc.transact(userID, charID, gachaID, rollType)
|
entries, err := svc.gachaRepo.GetRewardPool(gachaID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
entries, err := svc.gachaRepo.GetRewardPool(gachaID)
|
if len(entries) == 0 {
|
||||||
|
return nil, errors.New("gacha has no valid reward entries")
|
||||||
|
}
|
||||||
|
rolls, err := svc.transact(userID, charID, gachaID, rollType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ func TestGachaService_PlayNormalGacha(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "transact error",
|
name: "transact error",
|
||||||
|
pool: []GachaEntry{{ID: 10, Weight: 100}},
|
||||||
txErr: errors.New("tx fail"),
|
txErr: errors.New("tx fail"),
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
@@ -36,6 +37,10 @@ func TestGachaService_PlayNormalGacha(t *testing.T) {
|
|||||||
poolErr: errors.New("pool fail"),
|
poolErr: errors.New("pool fail"),
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "empty reward pool",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "success single roll",
|
name: "success single roll",
|
||||||
txRolls: 1,
|
txRolls: 1,
|
||||||
@@ -95,6 +100,7 @@ func TestGachaService_PlayStepupGacha(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "transact error",
|
name: "transact error",
|
||||||
|
pool: []GachaEntry{{ID: 10, Weight: 100}},
|
||||||
txErr: errors.New("tx fail"),
|
txErr: errors.New("tx fail"),
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
@@ -104,6 +110,10 @@ func TestGachaService_PlayStepupGacha(t *testing.T) {
|
|||||||
poolErr: errors.New("pool fail"),
|
poolErr: errors.New("pool fail"),
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "empty reward pool",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "success with guaranteed",
|
name: "success with guaranteed",
|
||||||
txRolls: 1,
|
txRolls: 1,
|
||||||
|
|||||||
Reference in New Issue
Block a user