diff --git a/server/channelserver/handlers_gacha_test.go b/server/channelserver/handlers_gacha_test.go index c05f09c31..501f0eb9a 100644 --- a/server/channelserver/handlers_gacha_test.go +++ b/server/channelserver/handlers_gacha_test.go @@ -186,7 +186,10 @@ func TestHandleMsgMhfReceiveGachaItem_Freeze(t *testing.T) { func TestHandleMsgMhfPlayNormalGacha_TransactError(t *testing.T) { 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.userRepo = &mockUserRepoGacha{} ensureGachaService(server) @@ -269,7 +272,10 @@ func TestHandleMsgMhfPlayNormalGacha_Success(t *testing.T) { func TestHandleMsgMhfPlayStepupGacha_TransactError(t *testing.T) { 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.userRepo = &mockUserRepoGacha{} ensureGachaService(server) @@ -473,7 +479,10 @@ func TestHandleMsgMhfGetBoxGachaInfo_Success(t *testing.T) { func TestHandleMsgMhfPlayBoxGacha_TransactError(t *testing.T) { 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.userRepo = &mockUserRepoGacha{} ensureGachaService(server) diff --git a/server/channelserver/repo_gacha.go b/server/channelserver/repo_gacha.go index b2efcf303..208e76b7d 100644 --- a/server/channelserver/repo_gacha.go +++ b/server/channelserver/repo_gacha.go @@ -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. +// Entries with no corresponding items in gacha_items are excluded. func (r *GachaRepository) GetRewardPool(gachaID uint32) ([]GachaEntry, error) { var entries []GachaEntry 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, ) if err != nil { diff --git a/server/channelserver/svc_gacha.go b/server/channelserver/svc_gacha.go index 7908af26b..685f7d214 100644 --- a/server/channelserver/svc_gacha.go +++ b/server/channelserver/svc_gacha.go @@ -164,14 +164,17 @@ func rewardsToItems(rewards []GachaReward) []GachaItem { return items } -// PlayNormalGacha processes a normal gacha roll: deducts cost, selects random -// rewards, saves items, and returns the result. +// PlayNormalGacha processes a normal gacha roll: validates the reward pool, +// deducts cost, selects random rewards, saves items, and returns the result. 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 { 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 { return nil, err } @@ -180,9 +183,17 @@ func (svc *GachaService) PlayNormalGacha(userID, charID, gachaID uint32, rollTyp return &GachaPlayResult{Rewards: rewards}, nil } -// PlayStepupGacha processes a stepup gacha roll: deducts cost, advances step, -// awards frontier points, selects random + guaranteed rewards, and saves items. +// PlayStepupGacha processes a stepup gacha roll: validates the reward pool, +// 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) { + 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) if err != nil { 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)) } - entries, err := svc.gachaRepo.GetRewardPool(gachaID) - if err != nil { - return nil, err - } - guaranteedItems, _ := svc.gachaRepo.GetGuaranteedItems(rollType, gachaID) randomRewards := svc.resolveRewards(entries, rolls, false) @@ -223,14 +229,18 @@ func (svc *GachaService) PlayStepupGacha(userID, charID, gachaID uint32, rollTyp }, nil } -// PlayBoxGacha processes a box gacha roll: deducts cost, selects random entries -// without replacement, records drawn entries, saves items, and returns the result. +// PlayBoxGacha processes a box gacha roll: validates the reward pool, deducts +// 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) { - rolls, err := svc.transact(userID, charID, gachaID, rollType) + entries, err := svc.gachaRepo.GetRewardPool(gachaID) if err != nil { 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 { return nil, err } diff --git a/server/channelserver/svc_gacha_test.go b/server/channelserver/svc_gacha_test.go index 92b9ecd0e..ba247e07e 100644 --- a/server/channelserver/svc_gacha_test.go +++ b/server/channelserver/svc_gacha_test.go @@ -27,6 +27,7 @@ func TestGachaService_PlayNormalGacha(t *testing.T) { }{ { name: "transact error", + pool: []GachaEntry{{ID: 10, Weight: 100}}, txErr: errors.New("tx fail"), wantErr: true, }, @@ -36,6 +37,10 @@ func TestGachaService_PlayNormalGacha(t *testing.T) { poolErr: errors.New("pool fail"), wantErr: true, }, + { + name: "empty reward pool", + wantErr: true, + }, { name: "success single roll", txRolls: 1, @@ -95,6 +100,7 @@ func TestGachaService_PlayStepupGacha(t *testing.T) { }{ { name: "transact error", + pool: []GachaEntry{{ID: 10, Weight: 100}}, txErr: errors.New("tx fail"), wantErr: true, }, @@ -104,6 +110,10 @@ func TestGachaService_PlayStepupGacha(t *testing.T) { poolErr: errors.New("pool fail"), wantErr: true, }, + { + name: "empty reward pool", + wantErr: true, + }, { name: "success with guaranteed", txRolls: 1,