fix(server): separate pass reward claims and implement cumulative purchase bonuses

This commit is contained in:
2026-10-05 13:22:44 +08:00
parent 5ee669a59a
commit eab6b514e8
9 changed files with 846 additions and 24 deletions
@@ -0,0 +1,157 @@
package eventtasks
import (
"bd2server/internal/server/gamedata"
"bd2server/internal/server/wire"
"bytes"
"testing"
)
func passClaimRequest(seq, rewardType uint64, all bool, level uint64) []byte {
b := req(seq)
if all {
b = wire.AppendVarint(b, 2, 1)
}
b = wire.AppendVarint(b, 3, 8)
if level != 0 {
b = wire.AppendVarint(b, 4, level)
}
if rewardType != 0 {
b = wire.AppendVarint(b, 5, rewardType)
}
return b
}
func claimPass(t *testing.T, s *Service, request []byte) []byte {
t.Helper()
code, response, handled, err := s.Handle("/PassReward", request)
if err != nil || code != 126 || !handled {
t.Fatalf("pass claim failed: code=%d handled=%t err=%v", code, handled, err)
}
if _, present, err := wire.Bytes(response, 1); err != nil || !present {
t.Fatalf("native receiver requires a non-null reward bundle: %x, %v", response, err)
}
return response
}
func TestPassRewardNativeOneClickAfterPurchaseAndRestart(t *testing.T) {
s, e, store := setup(t)
s.design.PassLevels[8] = []gamedata.EventPassLevel{
{ID: 1, NeedExp: 20, Basic: gamedata.Reward{Type: 3, Count: 1}, Premium: gamedata.Reward{Type: 4, Count: 10}},
{ID: 2, NeedExp: 30, Basic: gamedata.Reward{Type: 3, Count: 2}, Premium: gamedata.Reward{Type: 4, Count: 20}},
{ID: 3, NeedExp: 40, Basic: gamedata.Reward{Type: 3, Count: 3}, Premium: gamedata.Reward{Type: 4, Count: 30}},
{ID: 4, Basic: gamedata.Reward{Type: 3, Count: 4}, Premium: gamedata.Reward{Type: 4, Count: 40}},
}
s.design.PassBuys = map[uint64][]gamedata.EventPassBuy{8: {{ID: 1, Type: 1, LevelsGranted: 3, Cost: gamedata.Reward{Type: 3, Count: 1000}}}}
buy := wire.AppendVarint(req(1), 2, 8)
buy = wire.AppendVarint(buy, 3, 1)
if _, _, _, err := s.Handle("/PassBuy", buy); err != nil {
t.Fatal(err)
}
// PassRootUI.ReceiveAllReward sends BASIC first, then PREMIUM_1 in its callback.
basic := passClaimRequest(2, 0, true, 0)
basicReply := claimPass(t, s, basic)
for _, lv := range s.design.PassLevels[8] {
p := s.pass(s.registry.List()[2])
if !p.Claimed[key(lv.ID, 0)] || p.Claimed[key(lv.ID, 1)] {
t.Fatalf("basic phase incorrectly claimed premium at level %d", lv.ID)
}
}
if len(e.rewards) != 4 || e.rewards[0].Type != 3 || e.rewards[3].Count != 4 {
t.Fatalf("basic phase granted wrong rewards: %+v", e.rewards)
}
reopened, err := Open(store, s.design, s.registry, e)
if err != nil {
t.Fatal(err)
}
reopened.now = s.now
reopened.SetSession("test")
if replay := claimPass(t, reopened, basic); !bytes.Equal(replay, basicReply) || e.calls != 2 {
t.Fatal("restart replay repeated basic grants")
}
premium := passClaimRequest(3, 1, true, 0)
premiumReply := claimPass(t, reopened, premium)
if len(e.rewards) != 8 || e.rewards[4].Type != 4 || e.rewards[7].Count != 40 || e.calls != 3 {
t.Fatalf("premium phase granted wrong rewards: %+v", e.rewards)
}
for _, lv := range reopened.design.PassLevels[8] {
p := reopened.pass(reopened.registry.List()[2])
if !p.Claimed[key(lv.ID, 0)] || !p.Claimed[key(lv.ID, 1)] {
t.Fatalf("claim state incomplete at level %d", lv.ID)
}
}
if replay := claimPass(t, reopened, premium); !bytes.Equal(replay, premiumReply) || e.calls != 3 {
t.Fatal("premium replay repeated grants")
}
// A new one-click request after claiming must complete without granting again.
for typ := uint64(0); typ <= 1; typ++ {
response := claimPass(t, reopened, passClaimRequest(4+typ, typ, true, 0))
bundle, _, _ := wire.Bytes(response, 1)
if len(bundle) != 0 || e.calls != 3 || len(e.rewards) != 8 {
t.Fatal("already-claimed all request repeated rewards")
}
}
}
func TestPassRewardBasicAlreadyClaimedBeforePremiumPurchase(t *testing.T) {
s, e, _ := setup(t)
s.design.PassLevels[8] = []gamedata.EventPassLevel{{ID: 1, Basic: gamedata.Reward{Type: 3, Count: 5}, Premium: gamedata.Reward{Type: 4, Count: 50}}}
claimPass(t, s, passClaimRequest(1, 0, true, 0))
s.design.PassBuys = map[uint64][]gamedata.EventPassBuy{8: {{ID: 1, Type: 1, Cost: gamedata.Reward{Type: 3, Count: 1000}}}}
buy := wire.AppendVarint(req(2), 2, 8)
buy = wire.AppendVarint(buy, 3, 1)
if _, _, _, err := s.Handle("/PassBuy", buy); err != nil {
t.Fatal(err)
}
claimPass(t, s, passClaimRequest(3, 0, true, 0))
if len(e.rewards) != 1 || e.calls != 2 {
t.Fatal("empty basic phase granted again")
}
claimPass(t, s, passClaimRequest(4, 1, true, 0))
if len(e.rewards) != 2 || e.rewards[1].Type != 4 || e.rewards[1].Count != 50 || e.calls != 3 {
t.Fatalf("empty basic phase prevented premium claim: %+v", e.rewards)
}
}
func TestPassRewardSingleClaimsAndLockedLevels(t *testing.T) {
s, e, _ := setup(t)
s.design.PassLevels[8] = []gamedata.EventPassLevel{
{ID: 1, NeedExp: 20, Basic: gamedata.Reward{Type: 3, Count: 5}, Premium: gamedata.Reward{Type: 4, Count: 50}},
{ID: 2, Basic: gamedata.Reward{Type: 3, Count: 6}, Premium: gamedata.Reward{Type: 4, Count: 60}},
}
if _, _, _, err := s.Handle("/PassReward", passClaimRequest(1, 1, true, 0)); err == nil || e.calls != 0 {
t.Fatal("premium reward granted without purchase")
}
claimPass(t, s, passClaimRequest(2, 0, false, 1))
if len(e.rewards) != 1 || e.rewards[0].Count != 5 {
t.Fatal("single basic reward incorrect")
}
for _, request := range [][]byte{passClaimRequest(3, 0, false, 1), passClaimRequest(4, 0, false, 2), passClaimRequest(5, 0, false, 99), passClaimRequest(6, 2, true, 0)} {
if _, _, _, err := s.Handle("/PassReward", request); err == nil || e.calls != 1 {
t.Fatal("duplicate, locked or fabricated reward accepted")
}
}
s.pass(s.registry.List()[2]).Premium = true
claimPass(t, s, passClaimRequest(7, 1, false, 1))
if len(e.rewards) != 2 || e.rewards[1].Count != 50 || s.pass(s.registry.List()[2]).Claimed[key(2, 1)] {
t.Fatal("single premium claim changed another level")
}
}
func TestPassRewardKeepsCurrentNewbieStepInEveryResponse(t *testing.T) {
s, _, _ := setup(t)
d := s.design.Passes[8]
d.NewbieStep = 1
s.design.Passes[8] = d
s.state.NewbieStep = 1
s.design.PassLevels[8] = []gamedata.EventPassLevel{{ID: 1, NeedExp: 20, Basic: gamedata.Reward{Type: 3, Count: 5}}, {ID: 2, Basic: gamedata.Reward{Type: 3, Count: 6}}}
response := claimPass(t, s, passClaimRequest(1, 0, true, 0))
if scalar(response, 2) != 1 || s.state.NewbieStep != 1 {
t.Fatal("partial claim resets native guide step")
}
response = claimPass(t, s, passClaimRequest(2, 0, true, 0))
if scalar(response, 2) != 1 {
t.Fatal("empty phase resets native guide step")
}
}
+29 -23
View File
@@ -687,7 +687,10 @@ func (s *Service) handle(path string, b []byte, identity string) ([]byte, error)
}
all, id, rt := scalar(b, 2) != 0, scalar(b, 4), scalar(b, 5)
if rt > 1 {
return nil, errors.New("eventtasks: invalid pass reward kind")
return nil, fmt.Errorf("eventtasks: invalid pass reward kind pass=%d level=%d reward_type=%d all=%t", v.ID, id, rt, all)
}
if rt == 1 && !p.Premium {
return nil, fmt.Errorf("eventtasks: premium pass required pass=%d level=%d reward_type=%d all=%t", v.ID, id, rt, all)
}
var rewards []gamedata.Reward
var claims []string
@@ -699,34 +702,36 @@ func (s *Service) handle(path string, b []byte, identity string) ([]byte, error)
if !eligible || !all && lv.ID != id {
continue
}
for typ := uint64(0); typ <= 1; typ++ {
if !all && typ != rt || typ == 1 && !p.Premium {
continue
}
ck := key(lv.ID, typ)
if p.Claimed[ck] {
continue
}
r := lv.Basic
if typ == 1 {
r = lv.Premium
}
if r.Type > 0 && r.Count > 0 {
rewards = append(rewards, r)
}
claims = append(claims, ck)
// IsAll selects levels within RewardType. The native one-click flow
// sends an all-basic request followed by a separate all-premium one.
ck := key(lv.ID, rt)
if p.Claimed[ck] {
continue
}
r := lv.Basic
if rt == 1 {
r = lv.Premium
}
if r.Type > 0 && r.Count > 0 {
rewards = append(rewards, r)
}
claims = append(claims, ck)
}
if len(claims) == 0 {
return nil, errors.New("eventtasks: pass reward not eligible")
if len(claims) == 0 && !all {
return nil, fmt.Errorf("eventtasks: pass reward not eligible pass=%d level=%d reward_type=%d all=%t exp=%d premium=%t", v.ID, id, rt, all, p.Exp, p.Premium)
}
bundle, e := s.economy.Apply(identity, nil, rewards)
if e != nil {
return nil, e
var bundle []byte
if len(claims) > 0 {
bundle, e = s.economy.Apply(identity, nil, rewards)
if e != nil {
return nil, fmt.Errorf("eventtasks: pass reward grant failed pass=%d level=%d reward_type=%d all=%t: %w", v.ID, id, rt, all, e)
}
}
for _, k := range claims {
p.Claimed[k] = true
}
// An already-claimed all-basic phase must still succeed so the client
// can proceed to all-premium. Include a non-null empty reward bundle.
out := wire.AppendBytes(nil, 1, bundle)
if d.NewbieStep > 0 {
complete := true
@@ -747,8 +752,9 @@ func (s *Service) handle(path string, b []byte, identity string) ([]byte, error)
}
if complete && s.state.NewbieStep == d.NewbieStep {
s.state.NewbieStep++
out = wire.AppendVarint(out, 2, s.state.NewbieStep)
}
// The native receiver assigns this field even on an incomplete step.
out = wire.AppendVarint(out, 2, s.state.NewbieStep)
}
return out, nil
case "/PassBuy":