fix(server): separate pass reward claims and implement cumulative purchase bonuses
This commit is contained in:
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user