Files
bd2/go/internal/server/eventactions/service_test.go
T
Flechazo d8771caef2 feat(all): add hunting and events, commerce and rewards, binary calendars, GameData caching and login diagnostics
Implement hunting AP and progress, event tasks, exchanges, games, stories and actions; connect battle and reward handling.

Add CashShop plugin and installation, server purchases and entitlements, recharge rules and plugin version metadata.

Distribute binary calendars independently; cache GameData in memory and fix single-connection query deadlocks.

Improve automatic login and recovery diagnostics; document configuration and release flows.
2026-10-05 11:50:31 +08:00

140 lines
5.3 KiB
Go

package eventactions
import (
"bd2server/internal/server/events"
"bd2server/internal/server/gamedata"
"bd2server/internal/server/stateio"
"bd2server/internal/server/wire"
"bytes"
"testing"
"time"
)
type economyStub struct {
calls int
costs, rewards []gamedata.Reward
}
func (e *economyStub) Apply(_ string, c, r []gamedata.Reward) ([]byte, error) {
e.calls++
e.costs = append(e.costs, c...)
e.rewards = append(e.rewards, r...)
return []byte{10, 0}, nil
}
func row(v map[int]uint64) gamedata.EventActionRow { return gamedata.EventActionRow{Values: v} }
func setup(t *testing.T) (*Service, *economyStub, stateio.Store) {
t.Helper()
now := time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC)
r := events.NewRegistry()
if e := r.Replace([]events.Schedule{{UID: 1, Type: 23, ID: 1, Start: now.Add(-time.Hour).UnixMilli(), End: now.Add(time.Hour).UnixMilli()}, {UID: 2, Type: 20, ID: 1, Start: now.Add(-time.Hour).UnixMilli(), End: now.Add(time.Hour).UnixMilli()}, {UID: 3, Type: 21, ID: 1, Start: now.Add(-time.Hour).UnixMilli(), End: now.Add(time.Hour).UnixMilli()}}); e != nil {
t.Fatal(e)
}
d := &gamedata.EventActionsDesign{Tables: map[string][]gamedata.EventActionRow{"VotingEventTable": {row(map[int]uint64{6: 1, 10: 1, 14: 1, 7: 1, 8: 6001, 9: 8, 1: 1, 2: 6002, 3: 8})}, "VotingCandidateTable": {row(map[int]uint64{1: 1, 2: 9})}, "VotingRoundTable": {row(map[int]uint64{2: 1, 3: 1})}, "TacticsBingoGroupTable": {row(map[int]uint64{3: 1, 1: 5, 4: 2})}, "TacticsBingoTable": {row(map[int]uint64{4: 5, 5: 1, 2: 99})}, "FieldSpawnEventTable": {row(map[int]uint64{4: 1, 5: 1, 2: 1})}, "FieldEventMonsterTable": {row(map[int]uint64{3: 1, 4: 1, 1: 3001, 5: 3009})}, "FieldEventDefaultTable": {row(map[int]uint64{4: 10, 5: 2, 12: 40})}}, SpawnRewards: map[[2]uint64]gamedata.Reward{{3009, 3001}: {Type: 4, Count: 10000}}}
economy := &economyStub{}
store := stateio.NewMemory()
s, e := Open(store, d, r, economy)
if e != nil {
t.Fatal(e)
}
s.now = func() time.Time { return now }
s.BeginSession("test")
return s, economy, store
}
func req(n uint64) []byte { return wire.AppendVarint(nil, 1, n) }
func TestVoteConsumesDesignCostRejectsRepeatAndExactReplay(t *testing.T) {
s, e, store := setup(t)
b := req(1)
b = wire.AppendVarint(b, 2, 9)
b = wire.AppendVarint(b, 4, 1)
_, reply, _, err := s.Handle("/CharVoteSave", b)
if err != nil {
t.Fatal(err)
}
if e.calls != 1 || e.costs[0].ID != 6001 {
t.Fatal("vote cost not applied")
}
next, err := Open(store, s.design, s.registry, e)
if err != nil {
t.Fatal(err)
}
next.now = s.now
next.BeginSession("test")
_, replay, _, err := next.Handle("/CharVoteSave", b)
if err != nil || !bytes.Equal(reply, replay) || e.calls != 1 {
t.Fatal("replay duplicated vote")
}
changed := wire.AppendVarint(b, 5, 1)
if _, _, _, err = next.Handle("/CharVoteSave", changed); err == nil {
t.Fatal("changed replay accepted")
}
b = wire.AppendVarint(req(2), 2, 9)
b = wire.AppendVarint(b, 4, 1)
if _, _, _, err = next.Handle("/CharVoteSave", b); err == nil {
t.Fatal("second normal vote accepted")
}
}
func TestTacticsBattleRequiresStageAndBindsWinReceipt(t *testing.T) {
s, _, _ := setup(t)
enter := req(1)
enter = wire.AppendVarint(enter, 4, 99)
enter = wire.AppendVarint(enter, 5, 29)
enter = wire.AppendVarint(enter, 8, 5)
enter = wire.AppendVarint(enter, 9, 1)
if _, err := s.EnterBattle(enter, "enter1"); err != nil {
t.Fatal(err)
}
end := wire.AppendVarint(req(2), 2, 1)
reply, err := s.CompleteBattle(end, "end2")
if err != nil || val(reply, 29) != 1 {
t.Fatal("stage not cleared")
}
replay, err := s.CompleteBattle(end, "end2")
if err != nil || !bytes.Equal(reply, replay) {
t.Fatal("battle retry lost")
}
loss := wire.AppendVarint(req(2), 2, 2)
if _, err = s.CompleteBattle(loss, "end2"); err == nil {
t.Fatal("changed battle result accepted")
}
}
func TestSpawnNeedsStartAndRejectsRecatch(t *testing.T) {
s, e, _ := setup(t)
reward := req(1)
for f, v := range map[int]uint64{2: 1, 3: 1, 4: 1, 5: 1, 6: 3} {
reward = wire.AppendVarint(reward, f, v)
}
if _, _, _, err := s.Handle("/FieldEventSpawnReward", reward); err == nil {
t.Fatal("unstarted catch accepted")
}
start := wire.AppendVarint(req(2), 2, 3)
start = wire.AppendVarint(start, 3, 1)
start = wire.AppendVarint(start, 4, 1)
if _, _, _, err := s.Handle("/FieldEventSpawnStart", start); err != nil {
t.Fatal(err)
}
if _, _, _, err := s.Handle("/FieldEventSpawnReward", reward); err != nil {
t.Fatal(err)
}
if e.calls != 1 || s.state.DailyNormal != 1 {
t.Fatal("catch not settled")
}
}
func TestVotingNextRoundAdvancesRealTopCandidates(t *testing.T) {
s, _, _ := setup(t)
s.design.Tables["VotingCandidateTable"] = append(s.design.Tables["VotingCandidateTable"], row(map[int]uint64{1: 1, 2: 10}))
s.design.Tables["VotingRoundTable"] = []gamedata.EventActionRow{row(map[int]uint64{2: 1, 3: 1, 1: 1}), row(map[int]uint64{2: 1, 3: 2, 4: 1})}
s.state.Votes[key(1, 1, 9)] = &vote{Round: 1, Candidate: 9, Normal: 1}
s.state.Votes[key(1, 1, 10)] = &vote{Round: 1, Candidate: 10, Additional: 2}
v, r, _ := s.voteEvent()
advanced := s.advancedCandidates(v, r, 2, s.candidates(r))
if len(advanced) != 1 || advanced[0] != 10 {
t.Fatalf("wrong advanced list %v", advanced)
}
s.state.Votes[key(1, 1, 9)].Additional = 1
rows := s.rankRows(1, 1, s.candidates(r))
if val(rows[0], 3) != 1 || val(rows[1], 3) != 1 {
t.Fatal("equal votes did not share rank")
}
}