294 lines
9.7 KiB
Go
294 lines
9.7 KiB
Go
package gameserver
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"encoding/json"
|
||
|
|
"errors"
|
||
|
|
"os"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"gac-hkrpg-go/internal/common"
|
||
|
|
pb "gac-hkrpg-go/protocol/proto"
|
||
|
|
"google.golang.org/protobuf/proto"
|
||
|
|
)
|
||
|
|
|
||
|
|
func cwEndFixture(t *testing.T) (*common.CurrencyWarsCatalog, *pb.GridFightGetDataScRsp, *Session, uint32) {
|
||
|
|
t.Helper()
|
||
|
|
c := isolatedCWTest(t)
|
||
|
|
data, err := newCurrencyWarsData(c)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
request := &pb.GridFightStartGamePlayCsReq{DivisionId: c.CapturedStages[0].ID, LJNGPCKPJLF: c.ActiveSeasonID}
|
||
|
|
// Select a division actually present in the captured initial-run resource.
|
||
|
|
raw, err := common.ReadResourceFile("resources/CurrencyWarsInitialRuns.json")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
var resource struct {
|
||
|
|
Entries []struct {
|
||
|
|
Division uint32 `json:"division_id"`
|
||
|
|
Overclock bool `json:"is_overclock"`
|
||
|
|
} `json:"entries"`
|
||
|
|
}
|
||
|
|
if err := json.Unmarshal(raw, &resource); err != nil || len(resource.Entries) == 0 {
|
||
|
|
t.Fatalf("initial runs: %v", err)
|
||
|
|
}
|
||
|
|
request.DivisionId, request.IsOverlock = resource.Entries[0].Division, resource.Entries[0].Overclock
|
||
|
|
run, err := buildCurrencyWarsInitialRun(c, request, 7)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
offer := run.FMIGOKIDKJL.GetMFFCPLBNHCF()
|
||
|
|
if offer == nil || len(offer.NPHOIMEEDHM) == 0 {
|
||
|
|
t.Fatal("portal offer missing")
|
||
|
|
}
|
||
|
|
run, _, err = currencyWarsEntryChoice(c, run, run.FMIGOKIDKJL.QueuePosition, offer.NPHOIMEEDHM[0])
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
data.DHFOFLFCLAH = run
|
||
|
|
s := &Session{Token: 444, currencyWarsOpen: true}
|
||
|
|
if err := s.saveCurrencyWarsData(data, c); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
cfg, err := common.LoadRuntimeConfig()
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
return c, data, s, cfg.PlayerUID
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestCurrencyWarsEndWaitsForBattleAndSettlesPendingRun(t *testing.T) {
|
||
|
|
c, data, s, uid := cwEndFixture(t)
|
||
|
|
runID := data.DHFOFLFCLAH.UniqueId
|
||
|
|
if data.DHFOFLFCLAH.FMIGOKIDKJL == nil {
|
||
|
|
t.Fatal("fixture must have a pending choice")
|
||
|
|
}
|
||
|
|
if err := common.PatchPersistentFields(map[string]json.RawMessage{"ex_ranks_unlocked": json.RawMessage(`{"1204":false}`), "unrelated": json.RawMessage(`{"keep":true}`)}); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
// An ordinary grant queued first must not prevent ending a stuck page.
|
||
|
|
if _, err := common.QueueCurrencyWarsCommand(uid, runID, "gold", 99); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
endID, err := common.QueueCurrencyWarsCommand(uid, runID, common.CurrencyWarsCommandEnd, 0)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
before, _ := os.ReadFile("persistent")
|
||
|
|
CURRENT_BATTLE.Store(s.Token, &pb.SceneBattleInfo{})
|
||
|
|
if err := s.applyCurrencyWarsCommands(); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
CURRENT_BATTLE.Delete(s.Token)
|
||
|
|
after, _ := os.ReadFile("persistent")
|
||
|
|
if !bytes.Equal(before, after) {
|
||
|
|
t.Fatal("end ran while Battle was active")
|
||
|
|
}
|
||
|
|
if err := s.applyCurrencyWarsCommands(); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
saved, _, err := readCurrencyWarsData(c)
|
||
|
|
if err != nil || saved.DHFOFLFCLAH != nil {
|
||
|
|
t.Fatalf("run not ended: %v", err)
|
||
|
|
}
|
||
|
|
snapshot, err := cwArchiveSnapshot(c)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
archives, err := cwReadArchives(snapshot)
|
||
|
|
if err != nil || len(archives) != 1 {
|
||
|
|
t.Fatalf("archive: %v", err)
|
||
|
|
}
|
||
|
|
if archives[0].BFLLJJBHLDF.Reason != pb.PEIJNEFOCHI_PEIJNEFOCHI_NOCIJDINKPJ {
|
||
|
|
t.Fatal("end must settle as quit")
|
||
|
|
}
|
||
|
|
queued, err := common.ReadCurrencyWarsCommands()
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
for _, cmd := range queued {
|
||
|
|
if cmd.ID == endID {
|
||
|
|
t.Fatal("end was not atomically acknowledged")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
var root map[string]json.RawMessage
|
||
|
|
raw, _ := os.ReadFile("persistent")
|
||
|
|
if err := json.Unmarshal(raw, &root); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
var ex, unrelated map[string]bool
|
||
|
|
if err := json.Unmarshal(root["ex_ranks_unlocked"], &ex); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if err := json.Unmarshal(root["unrelated"], &unrelated); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if len(ex) != 1 || ex["1204"] || !unrelated["keep"] || len(unrelated) != 1 {
|
||
|
|
t.Fatal("unrelated state changed")
|
||
|
|
}
|
||
|
|
if err := s.applyCurrencyWarsCommands(); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
again, err := cwArchiveSnapshot(c)
|
||
|
|
if err != nil || len(again.Archives) != 1 {
|
||
|
|
t.Fatal("duplicate settlement")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestCurrencyWarsEndRejectsStaleAndDifferentRun(t *testing.T) {
|
||
|
|
c, data, s, uid := cwEndFixture(t)
|
||
|
|
runID := data.DHFOFLFCLAH.UniqueId
|
||
|
|
cmdID, err := common.QueueCurrencyWarsCommand(uid, runID, common.CurrencyWarsCommandEnd, 0)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
s.currencyWarsRevision--
|
||
|
|
before, _ := os.ReadFile("persistent")
|
||
|
|
if err := s.applyCurrencyWarsCommands(); !errors.Is(err, common.ErrCurrencyWarsSaveConflict) {
|
||
|
|
t.Fatalf("stale open session: %v", err)
|
||
|
|
}
|
||
|
|
after, _ := os.ReadFile("persistent")
|
||
|
|
if !bytes.Equal(before, after) {
|
||
|
|
t.Fatal("stale session changed save")
|
||
|
|
}
|
||
|
|
if err := common.AcknowledgeCurrencyWarsCommand(cmdID); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if _, err := common.QueueCurrencyWarsCommand(uid, runID+1, common.CurrencyWarsCommandEnd, 0); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if err := s.applyCurrencyWarsCommands(); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
saved, _, err := readCurrencyWarsData(c)
|
||
|
|
if err != nil || saved.DHFOFLFCLAH == nil || saved.DHFOFLFCLAH.UniqueId != runID {
|
||
|
|
t.Fatal("wrong-run command ended active run")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestCurrencyWarsEndWorksAfterLeavingCW(t *testing.T) {
|
||
|
|
c, data, s, uid := cwEndFixture(t)
|
||
|
|
if _, err := common.QueueCurrencyWarsCommand(uid, data.DHFOFLFCLAH.UniqueId, common.CurrencyWarsCommandEnd, 0); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
s.currencyWarsOpen = false
|
||
|
|
s.currencyWarsRevision = 0 // A newly connected session has not read CW yet.
|
||
|
|
if err := s.applyCurrencyWarsCommands(); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
saved, _, err := readCurrencyWarsData(c)
|
||
|
|
if err != nil || saved.DHFOFLFCLAH != nil {
|
||
|
|
t.Fatal("closed CW did not end")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestCurrencyWarsEndNotifiesOpenClient(t *testing.T) {
|
||
|
|
_, data, savedSession, uid := cwEndFixture(t)
|
||
|
|
s, receive := replySession(t)
|
||
|
|
s.currencyWarsRevision = savedSession.currencyWarsRevision
|
||
|
|
s.currencyWarsOpen = true
|
||
|
|
if _, err := common.QueueCurrencyWarsCommand(uid, data.DHFOFLFCLAH.UniqueId, common.CurrencyWarsCommandEnd, 0); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if err := s.applyCurrencyWarsCommands(); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
receive(&pb.GridFightSyncKeepWinCntNotify{})
|
||
|
|
settlement := &pb.GridFightSettleNotify{}
|
||
|
|
receive(settlement)
|
||
|
|
if settlement.BFLLJJBHLDF == nil || settlement.BFLLJJBHLDF.Reason != pb.PEIJNEFOCHI_PEIJNEFOCHI_NOCIJDINKPJ {
|
||
|
|
t.Fatal("client did not receive quit settlement")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestCurrencyWarsEndDoesNotCommitIfCommandIsMissing(t *testing.T) {
|
||
|
|
_, data, s, uid := cwEndFixture(t)
|
||
|
|
before, _ := os.ReadFile("persistent")
|
||
|
|
cmd := common.CurrencyWarsCommand{ID: "missing", UID: uid, RunID: data.DHFOFLFCLAH.UniqueId, Field: common.CurrencyWarsCommandEnd}
|
||
|
|
if err := s.applyCurrencyWarsEnd(cmd); err == nil {
|
||
|
|
t.Fatal("missing command should reject settlement")
|
||
|
|
}
|
||
|
|
after, _ := os.ReadFile("persistent")
|
||
|
|
if !bytes.Equal(before, after) {
|
||
|
|
t.Fatal("failed acknowledgement committed settlement or archive")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestCurrencyWarsOpenStateTracksStartLeaveAndResume(t *testing.T) {
|
||
|
|
_, data, writer, uid := cwEndFixture(t)
|
||
|
|
previous := data.DHFOFLFCLAH
|
||
|
|
if _, err := common.QueueCurrencyWarsCommand(uid, previous.UniqueId, common.CurrencyWarsCommandEnd, 0); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
writer.currencyWarsOpen = false
|
||
|
|
if err := writer.applyCurrencyWarsCommands(); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
s, receive := replySession(t)
|
||
|
|
s.currencyWarsRevision = writer.currencyWarsRevision
|
||
|
|
req := &pb.GridFightStartGamePlayCsReq{DivisionId: previous.DivisionId, IsOverlock: previous.IsOverlock, LJNGPCKPJLF: previous.LJNGPCKPJLF}
|
||
|
|
callReviewRequest(t, s, req)
|
||
|
|
start := &pb.GridFightStartGamePlayScRsp{}
|
||
|
|
receive(start)
|
||
|
|
if start.Retcode != 0 || start.DHFOFLFCLAH == nil || !s.currencyWarsOpen {
|
||
|
|
t.Fatal("successful start did not mark CW open")
|
||
|
|
}
|
||
|
|
callReviewRequest(t, s, &pb.GridFightQuitLeaveGamePlayCsReq{})
|
||
|
|
receive(&pb.GridFightQuitLeaveGamePlayScRsp{})
|
||
|
|
if s.currencyWarsOpen {
|
||
|
|
t.Fatal("leave did not close CW")
|
||
|
|
}
|
||
|
|
callReviewRequest(t, s, &pb.GridFightResumeGamePlayCsReq{UniqueId: start.DHFOFLFCLAH.UniqueId})
|
||
|
|
resume := &pb.GridFightResumeGamePlayScRsp{}
|
||
|
|
receive(resume)
|
||
|
|
if resume.Retcode != 0 || !s.currencyWarsOpen {
|
||
|
|
t.Fatal("successful resume did not mark CW open")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestCurrencyWarsInitialRoundReservesHigherStarStarter(t *testing.T) {
|
||
|
|
c, data, _, _ := cwEndFixture(t)
|
||
|
|
run := data.DHFOFLFCLAH
|
||
|
|
_, team, shop := cwComponents(run)
|
||
|
|
if team == nil || shop == nil || len(team.GridGameRoleList) == 0 {
|
||
|
|
t.Fatal("starter team fixture missing")
|
||
|
|
}
|
||
|
|
pool, err := c.NewCapturedCardPool()
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
var selected common.CurrencyWarsRoleVariant
|
||
|
|
for _, v := range c.CapturedVariants {
|
||
|
|
base, err := c.CapturedRole(v.Config.ID)
|
||
|
|
if err != nil || v.Star <= base.DefaultStar {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if _, present := pool.Roles[v.Config.ID]; present {
|
||
|
|
selected = v
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if selected.Config.ID == 0 {
|
||
|
|
t.Fatal("higher-star pool role missing")
|
||
|
|
}
|
||
|
|
// The live failure occurs after a portal has supplied a non-default star.
|
||
|
|
team.GridGameRoleList = []*pb.CDEAABODKCP{{Id: selected.Config.ID, RoleStar: selected.Star, UniqueId: 1, Pos: 1}}
|
||
|
|
if run.FMIGOKIDKJL.GetNNDGEJFLGGF() == nil || len(shop.OGDKJPDFFAI) != 0 {
|
||
|
|
t.Fatal("fixture is not at initial round")
|
||
|
|
}
|
||
|
|
invalid := proto.Clone(run).(*pb.MPIMAEMCEFH)
|
||
|
|
_, invalidTeam, _ := cwComponents(invalid)
|
||
|
|
invalidTeam.GridGameRoleList[0].RoleStar = ^uint32(0)
|
||
|
|
before := proto.Clone(invalid)
|
||
|
|
if _, err := cwInitialRound(c, invalid); err == nil || !proto.Equal(before, invalid) {
|
||
|
|
t.Fatal("invalid starter star was accepted or changed state")
|
||
|
|
}
|
||
|
|
if _, err := cwInitialRound(c, run); err != nil {
|
||
|
|
t.Fatalf("higher-star starter rejected: %v", err)
|
||
|
|
}
|
||
|
|
if run.FMIGOKIDKJL.GetLGNPHHBGKFH() == nil || len(shop.OGDKJPDFFAI) == 0 {
|
||
|
|
t.Fatal("initial round did not advance to preparation/shop")
|
||
|
|
}
|
||
|
|
}
|