DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,454 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"math"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
dnfpvp "longheng.io/server/internal/modules/dnf/pvp"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
platformpvf "longheng.io/server/internal/platform/pvf"
|
||||
)
|
||||
|
||||
func pvpDailyTestCatalog() *dnfpvp.Catalog {
|
||||
c := &dnfpvp.Catalog{ExperienceBounds: []uint32{0, 12000, 27000, 47000, 77000, 125000, 185000, 265000, 385000, 529000, 709000}}
|
||||
c.RatingRules = &dnfpvp.RatingRules{InitialRating: 1000, WarmupGames: 10, WarmupWeight: 120,
|
||||
MatchExperiencePercent: [2]int64{50, 100}, ManualExperiencePercent: [2]int64{45, 45},
|
||||
Warmup: []dnfpvp.Weight{{Threshold: 0, Value: 100}}, Rating: []dnfpvp.Weight{{Threshold: 0, Value: 22}}}
|
||||
for i := 1; i < len(c.ExperienceBounds); i++ {
|
||||
c.RatingRules.GradePoints = append(c.RatingRules.GradePoints, [2]int64{0, 1000})
|
||||
}
|
||||
for i, id := range []uint16{113, 118, 131} {
|
||||
c.Pools[i] = []dnfpvp.Mission{{ID: id, Tier: i, Weight: 10, RankMax: 30, Target: 3, Condition: []string{"hereafter join", "first aerial attack", "winning streak"}[i]}}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func TestCurrentPVPDailyWireAndRestore(t *testing.T) {
|
||||
s := pvpRoomTestService(t)
|
||||
session := &gameSession{conn: &bufferConn{}, accountID: "pvp-29", selectedCharacterID: 29, channel: s.catalog.Channels()[1]}
|
||||
session.channel.Type = 8
|
||||
if err := s.sendCurrentPVPDailyState(session); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wire := session.conn.(*bufferConn).write.Bytes()
|
||||
cert, wire := splitCurrentGameServerUpperPacketAuto(t, wire)
|
||||
wallet, wire := splitCurrentGameServerUpperPacketAuto(t, wire)
|
||||
list, wire := splitCurrentGameServerUpperPacketAuto(t, wire)
|
||||
if len(wire) != 0 || cert.Header.MsgID != 48 || len(cert.Body) != 51 || binary.LittleEndian.Uint32(cert.Body[12:]) != 0 || binary.LittleEndian.Uint32(cert.Body[16:]) != 12000 ||
|
||||
wallet.Header.MsgID != 14 || len(wallet.Body) != 122 || binary.LittleEndian.Uint16(wallet.Body[3:]) != 2 || binary.LittleEndian.Uint32(wallet.Body[5:]) != 2 ||
|
||||
list.Header.MsgID != 323 || len(list.Body) != 31 || list.Body[0] != 3 || binary.LittleEndian.Uint16(list.Body[1:]) != 113 || binary.LittleEndian.Uint32(list.Body[7:]) != 3 {
|
||||
t.Fatalf("cert=%+v list=%+v", cert, list)
|
||||
}
|
||||
repos, _ := s.repositoryGroup()
|
||||
c, _, _ := repos.Character.Load(context.Background(), "29")
|
||||
c.Stats[dnfpvp.CertificationKey] = 7
|
||||
c.Stats[dnfpvp.WinsKey] = 4
|
||||
c.Stats[dnfpvp.LossesKey] = 2
|
||||
c.Stats[dnfpvp.EmblemsKey] = 80
|
||||
c.Stats["pvp_rank_point"] = 1240
|
||||
c.Stats["pvp_max_rank_point"] = 1300
|
||||
if err := dnfrepo.SaveCharacterFields(context.Background(), repos.Character, c, dnfrepo.CharacterFieldStats); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
session.conn = &bufferConn{}
|
||||
if err := s.sendCurrentPVPDailyState(session); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cert, wire = splitCurrentGameServerUpperPacketAuto(t, session.conn.(*bufferConn).write.Bytes())
|
||||
wallet, _ = splitCurrentGameServerUpperPacketAuto(t, wire)
|
||||
if binary.LittleEndian.Uint32(cert.Body[34:]) != 7 || binary.LittleEndian.Uint32(cert.Body) != 4 || binary.LittleEndian.Uint32(cert.Body[4:]) != 2 || binary.LittleEndian.Uint32(cert.Body[26:]) != 1240 || binary.LittleEndian.Uint32(cert.Body[30:]) != 1300 || binary.LittleEndian.Uint32(wallet.Body[9:]) != 80 {
|
||||
t.Fatal("certification was not restored")
|
||||
}
|
||||
state, _ := s.prepareCurrentPVPDaily(session)
|
||||
missionBody, err := buildCurrentPVPMissions(state.Rows, state.NormalRows)
|
||||
if err != nil || !reflect.DeepEqual(list.Body, missionBody) {
|
||||
t.Fatal("reconnect rerolled missions")
|
||||
}
|
||||
// The same record must survive a return/login in an ordinary town.
|
||||
session.channel.Type = 1
|
||||
session.conn = &bufferConn{}
|
||||
if err := s.sendCurrentPVPDailyState(session); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
townCert, wire := splitCurrentGameServerUpperPacketAuto(t, session.conn.(*bufferConn).write.Bytes())
|
||||
townWallet, wire := splitCurrentGameServerUpperPacketAuto(t, wire)
|
||||
if len(wire) != 0 || !reflect.DeepEqual(townCert.Body, cert.Body) || !reflect.DeepEqual(townWallet.Body, wallet.Body) {
|
||||
t.Fatal("town lost PVP record/wallet")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentPVPRecordPublishedOnFirstPrivateTownEntry(t *testing.T) {
|
||||
s := pvpRoomTestService(t)
|
||||
repos, _ := s.repositoryGroup()
|
||||
character, _, _ := repos.Character.Load(context.Background(), "29")
|
||||
character.Stats[dnfpvp.WinsKey] = 17
|
||||
character.Stats[dnfpvp.LossesKey] = 3
|
||||
character.Stats[dnfpvp.RankPointKey] = 1142
|
||||
if err := repos.Character.Save(context.Background(), character); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
session := &gameSession{conn: &bufferConn{}, accountID: character.AccountID, selectedCharacterID: 29, channel: s.catalog.Channels()[0]}
|
||||
for _, id := range []uint16{29, 30} {
|
||||
session.selectedCharacterID = id
|
||||
session.accountID = "pvp-" + strconv.Itoa(int(id))
|
||||
session.conn.(*bufferConn).write.Reset()
|
||||
character, _, _ = repos.Character.Load(context.Background(), strconv.Itoa(int(id)))
|
||||
s.publishCurrentTownPresence(session, character, 38, 1, currentSceneTransitionRow{ObjectOrResourceKey: id}, 0,
|
||||
"Cataclysm/Town/Elvengard/new_seria_room.map", "test_first_login")
|
||||
cert, wire := splitCurrentGameServerUpperPacketAuto(t, session.conn.(*bufferConn).write.Bytes())
|
||||
wallet, wire := splitCurrentGameServerUpperPacketAuto(t, wire)
|
||||
if cert.Header.MsgID != 48 || wallet.Header.MsgID != 14 || len(wire) != 0 {
|
||||
t.Fatal("private town did not publish personal record and wallet before any UI request")
|
||||
}
|
||||
if binary.LittleEndian.Uint32(cert.Body) != statU32(character, dnfpvp.WinsKey, 0) ||
|
||||
binary.LittleEndian.Uint32(cert.Body[4:]) != statU32(character, dnfpvp.LossesKey, 0) ||
|
||||
binary.LittleEndian.Uint32(cert.Body[26:]) != statU32(character, dnfpvp.RankPointKey, 0) {
|
||||
t.Fatal("first-open record is stale or leaked from the previous character")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentPVPExperienceBarNativeBounds(t *testing.T) {
|
||||
catalog := pvpDailyTestCatalog()
|
||||
for _, tc := range []struct {
|
||||
experience, grade, progress, needed int64
|
||||
}{
|
||||
{0, 0, 0, 12000},
|
||||
{1000, 0, 1000, 12000},
|
||||
{11950, 0, 11950, 12000},
|
||||
{12000, 1, 0, 15000},
|
||||
{12400, 1, 400, 15000},
|
||||
{13400, 1, 1400, 15000},
|
||||
{27000, 2, 0, 20000},
|
||||
} {
|
||||
state := dnfpvp.Snapshot{Character: dnfrepo.CharacterRecord{Stats: map[string]int64{
|
||||
dnfpvp.ExperienceKey: tc.experience, dnfpvp.GradeKey: tc.grade,
|
||||
}}}
|
||||
body, err := buildCurrentPVPCertification(state, catalog)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Native sub_3292590 uses (EXP - lower) / (upper - lower).
|
||||
experience := int64(binary.LittleEndian.Uint32(body[8:]))
|
||||
lower := int64(binary.LittleEndian.Uint32(body[12:]))
|
||||
upper := int64(binary.LittleEndian.Uint32(body[16:]))
|
||||
if experience-lower != tc.progress || upper-lower != tc.needed {
|
||||
t.Fatalf("EXP %d grade %d: native bar %d/%d, want %d/%d", tc.experience, tc.grade,
|
||||
experience-lower, upper-lower, tc.progress, tc.needed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentPVPMissionReportsValidateAssignment(t *testing.T) {
|
||||
s := pvpRoomTestService(t)
|
||||
room := ¤tPVPRoom{id: 1, battleMode: 2, matchingToken: 1, started: true, seatState: [8]byte{1, 2}, match: ¤tPVPMatch{startedAt: time.Now()}}
|
||||
for seat, id := range []uint16{29, 30} {
|
||||
p := &gameSession{conn: &bufferConn{}, accountID: "pvp-" + strconv.Itoa(int(id)), selectedCharacterID: id, channel: s.catalog.Channels()[1], pvpRoom: room}
|
||||
p.channel.Type = 8
|
||||
room.members[seat] = ¤tPVPMember{session: p, characterID: id}
|
||||
}
|
||||
a := room.members[0].session
|
||||
for _, body := range [][]byte{{1, 113, 0}, {1, 119, 0}, {2, 118, 0, 118, 0}, {1, 118}, {1, 118, 0, 0}} {
|
||||
if err := s.handleCurrentPVPMissionReport(a, 876, body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(room.match.missionReports[0]) != 0 {
|
||||
t.Fatal("unassigned, wrong-kind, duplicate, or malformed report accepted")
|
||||
}
|
||||
}
|
||||
for i := 0; i < 2; i++ {
|
||||
if err := s.handleCurrentPVPMissionReport(a, 876, []byte{1, 118, 0}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if len(room.match.missionReports[0]) != 1 || !room.match.missionReports[0][118] {
|
||||
t.Fatal("valid report lost or duplicated")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPVPEmblemInventoryProjectionPreservesReservedSlot(t *testing.T) {
|
||||
for _, existing := range []int64{0, 2, 600} {
|
||||
t.Run(strconv.FormatInt(existing, 10), func(t *testing.T) {
|
||||
s := pvpRoomTestService(t)
|
||||
repos, _ := s.repositoryGroup()
|
||||
ctx := context.Background()
|
||||
c, _, _ := repos.Character.Load(ctx, "29")
|
||||
c.Stats[dnfpvp.EmblemsKey] = 80
|
||||
if err := repos.Character.Save(ctx, c); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
slots := map[string]dnfrepo.ItemStack{}
|
||||
if existing != 0 {
|
||||
slots["0:2"] = dnfrepo.ItemStack{ItemID: existing, Count: 7}
|
||||
}
|
||||
if err := repos.Inventory.Save(ctx, dnfrepo.InventoryRecord{CharacterID: "29", Slots: slots}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
session := &gameSession{selectedCharacterID: 29, accountID: "pvp-29"}
|
||||
body, _, _, ok := s.buildCurrentItemListBodyForSession(ctx, session, 0)
|
||||
if !ok {
|
||||
t.Fatal("wallet list unavailable")
|
||||
}
|
||||
wantItem, wantCount := uint32(2), uint32(80)
|
||||
if existing == 600 {
|
||||
wantItem, wantCount = 600, 7
|
||||
}
|
||||
count := int(binary.LittleEndian.Uint16(body[3:5]))
|
||||
found := 0
|
||||
for i := 0; i < count; i++ {
|
||||
row := body[5+i*currentItemListEntryWireSize:]
|
||||
if binary.LittleEndian.Uint16(row) != 2 {
|
||||
continue
|
||||
}
|
||||
found++
|
||||
if binary.LittleEndian.Uint32(row[2:]) != wantItem || binary.LittleEndian.Uint32(row[6:]) != wantCount {
|
||||
t.Fatalf("wallet row=%x", row[:10])
|
||||
}
|
||||
}
|
||||
if found != 1 {
|
||||
t.Fatalf("slot2 count=%d", found)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealScriptPVFDailyMissions(t *testing.T) {
|
||||
p := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
|
||||
if p == "" {
|
||||
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE")
|
||||
}
|
||||
a, err := platformpvf.LoadArchive(platformpvf.Options{Path: p})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c, err := dnfpvp.Load(a)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(c.Pools[0]) != 3 || len(c.Pools[1]) != 11 || len(c.Pools[2]) != 3 || c.ExperienceBounds[1] != 12000 {
|
||||
t.Fatalf("runtime PVF catalog=%+v", c)
|
||||
}
|
||||
categories := map[string]int{}
|
||||
for _, mission := range c.Normal {
|
||||
categories[mission.Category]++
|
||||
}
|
||||
if !reflect.DeepEqual(categories, map[string]int{"training": 43, "rating": 19, "battle": 24, "victory": 22}) {
|
||||
t.Fatalf("normal PVF categories=%v", categories)
|
||||
}
|
||||
s := pvpRoomTestService(t)
|
||||
s.pvpDailyCatalog = c
|
||||
session := &gameSession{conn: &bufferConn{}, accountID: "pvp-29", selectedCharacterID: 29, channel: s.catalog.Channels()[1]}
|
||||
session.channel.Type = 8
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
session.conn = &bufferConn{}
|
||||
if err := s.sendCurrentPVPDailyState(session); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, wire := splitCurrentGameServerUpperPacketAuto(t, session.conn.(*bufferConn).write.Bytes())
|
||||
_, wire = splitCurrentGameServerUpperPacketAuto(t, wire)
|
||||
list, rest := splitCurrentGameServerUpperPacketAuto(t, wire)
|
||||
if list.Header.MsgID != 323 || len(rest) != 0 || len(list.Body) != 751 || list.Body[0] != 75 {
|
||||
t.Fatalf("native target list: id=%d bytes=%d count=%d", list.Header.MsgID, len(list.Body), list.Body[0])
|
||||
}
|
||||
rows := map[uint16]dnfpvp.Row{}
|
||||
for offset := 1; offset < len(list.Body); offset += 10 {
|
||||
id := binary.LittleEndian.Uint16(list.Body[offset:])
|
||||
rows[id] = dnfpvp.Row{ID: id, Progress: binary.LittleEndian.Uint32(list.Body[offset+2:]), Target: binary.LittleEndian.Uint32(list.Body[offset+6:])}
|
||||
}
|
||||
if rows[499].Target != 100 || rows[500].Target != 333 || rows[545].Target != 2 || rows[565].Target != 1 || rows[604].Target != math.MaxUint32 || rows[566].ID != 0 || rows[800].ID != 0 {
|
||||
t.Fatalf("native target filtering/sentinels=%v", rows)
|
||||
}
|
||||
}
|
||||
t.Logf("PVF normal definitions=%d categories=%v; swordman native op323=75 rows/751 bytes", len(c.Normal), categories)
|
||||
}
|
||||
|
||||
func TestPVPMissionWireRejectsTruncationAndDuplicates(t *testing.T) {
|
||||
rows := make([]dnfpvp.Row, 255)
|
||||
for i := range rows {
|
||||
rows[i] = dnfpvp.Row{ID: uint16(i + 1), Target: 1}
|
||||
}
|
||||
body, err := buildCurrentPVPMissions(rows)
|
||||
if err != nil || len(body) != 2551 || body[0] != 255 {
|
||||
t.Fatal("maximum native list rejected")
|
||||
}
|
||||
for _, extra := range [][]dnfpvp.Row{{{ID: 499, Target: 1}}, {{ID: 1, Target: 1}}} {
|
||||
if _, err := buildCurrentPVPMissions(rows, extra); err == nil {
|
||||
t.Fatal("wrapped native count accepted")
|
||||
}
|
||||
}
|
||||
if _, err := buildCurrentPVPMissions(rows[:1], rows[:1]); err == nil {
|
||||
t.Fatal("duplicate ID accepted")
|
||||
}
|
||||
}
|
||||
|
||||
type pvpFailNthConn struct {
|
||||
bufferConn
|
||||
writes, failAt int
|
||||
}
|
||||
|
||||
func (c *pvpFailNthConn) Write(body []byte) (int, error) {
|
||||
c.writes++
|
||||
if c.writes == c.failAt {
|
||||
return 0, errors.New("injected PVP progress write failure")
|
||||
}
|
||||
return c.bufferConn.Write(body)
|
||||
}
|
||||
|
||||
func TestPVPDailyResultPublicationRetry(t *testing.T) {
|
||||
s := pvpRoomTestService(t)
|
||||
s.questCatalog = buildPVPResultQuestCatalog(t)
|
||||
s.pvpDailyCatalog.Normal = []dnfpvp.Mission{
|
||||
{ID: 499, Target: 100, Condition: "hereafter join"},
|
||||
{ID: 522, Target: 100, Condition: "hereafter winning count"},
|
||||
}
|
||||
queue := newFakeCurrentDungeonDeathTimerQueue()
|
||||
s.gameplayTimers = queue
|
||||
room := ¤tPVPRoom{id: 1, battleMode: 2, matchingToken: 1, started: true, seatState: [8]byte{1, 2},
|
||||
match: ¤tPVPMatch{startedAt: s.gameplayNow()}}
|
||||
s.pvpRooms = map[uint16]*currentPVPRoom{1: room}
|
||||
for seat, id := range []uint16{29, 30} {
|
||||
p := &gameSession{conn: &pvpFailNthConn{}, accountID: "pvp-" + strconv.Itoa(int(id)), selectedCharacterID: id, channel: s.catalog.Channels()[1], pvpRoom: room}
|
||||
p.channel.Type = 8
|
||||
room.members[seat] = ¤tPVPMember{session: p, characterID: id}
|
||||
}
|
||||
room.members[0].session.conn.(*pvpFailNthConn).failAt = 5
|
||||
if err := s.sendCurrentPVPResultLocked(room, room.members[0].session, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if room.match.resultSent || room.match.resultStage[0] != 3 || len(queue.tasks) != 1 {
|
||||
t.Fatal("failed update lost publication cursor")
|
||||
}
|
||||
queue.tasks[0].callback(s.gameplayNow())
|
||||
if !room.match.resultSent {
|
||||
t.Fatal("retry did not finish result")
|
||||
}
|
||||
for seat, m := range room.members[:2] {
|
||||
wire := m.session.conn.(*pvpFailNthConn).write.Bytes()
|
||||
var ids []uint16
|
||||
seenResult := false
|
||||
for len(wire) > 0 {
|
||||
p, rest := splitCurrentGameServerUpperPacketAuto(t, wire)
|
||||
wire = rest
|
||||
ids = append(ids, p.Header.MsgID)
|
||||
if p.Header.MsgID == 47 {
|
||||
seenResult = true
|
||||
wantEXP := uint32(0)
|
||||
if seat == 0 {
|
||||
wantEXP = 1000
|
||||
}
|
||||
if binary.LittleEndian.Uint32(p.Body[6:]) != wantEXP {
|
||||
t.Fatal("op47 gain still zero or wrong recipient")
|
||||
}
|
||||
}
|
||||
if p.Header.MsgID == 48 && binary.LittleEndian.Uint32(p.Body[34:]) != 1 {
|
||||
t.Fatal("certification repeated")
|
||||
}
|
||||
if p.Header.MsgID == 48 {
|
||||
wantEXP, wantRP := uint32(0), uint32(0)
|
||||
if seenResult {
|
||||
wantRP = 950
|
||||
if seat == 0 {
|
||||
wantEXP, wantRP = 1000, 1050
|
||||
}
|
||||
}
|
||||
if binary.LittleEndian.Uint32(p.Body[8:]) != wantEXP || binary.LittleEndian.Uint32(p.Body[26:]) != wantRP {
|
||||
t.Fatalf("op48 old/new state: seat=%d after=%v body=%x", seat, seenResult, p.Body)
|
||||
}
|
||||
}
|
||||
if p.Header.MsgID == 323 && (p.Body[0] != 5 || binary.LittleEndian.Uint16(p.Body[31:]) != 499 || binary.LittleEndian.Uint32(p.Body[33:]) != 0) {
|
||||
t.Fatal("result reset lost normal task definitions/pre-match progress")
|
||||
}
|
||||
if p.Header.MsgID == 578 {
|
||||
wantCount := byte(2)
|
||||
if seat == 0 {
|
||||
wantCount = 3
|
||||
}
|
||||
if p.Body[0] != wantCount || binary.LittleEndian.Uint32(p.Body[3:]) != 1 || binary.LittleEndian.Uint16(p.Body[11:]) != 499 || binary.LittleEndian.Uint32(p.Body[13:]) != 1 {
|
||||
t.Fatal("daily/normal deltas missing, repeated, or unchanged tasks republished")
|
||||
}
|
||||
}
|
||||
}
|
||||
if !reflect.DeepEqual(ids, []uint16{48, 323, 47, 48, 578}) {
|
||||
t.Fatalf("native order/retry ids=%v", ids)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPVPManualExperiencePublicationAndGradeTransition(t *testing.T) {
|
||||
s := pvpRoomTestService(t)
|
||||
repos, _ := s.repositoryGroup()
|
||||
ctx := context.Background()
|
||||
room := ¤tPVPRoom{id: 2, battleMode: 2, started: true, seatState: [8]byte{1, 2, 3},
|
||||
match: ¤tPVPMatch{startedAt: s.gameplayNow(), completedAt: s.gameplayNow(), winner: 0}}
|
||||
for seat, id := range []uint16{29, 30, 31} {
|
||||
p := &gameSession{conn: &bufferConn{}, accountID: "pvp-" + strconv.Itoa(int(id)), selectedCharacterID: id, channel: s.catalog.Channels()[1], pvpRoom: room}
|
||||
p.channel.Type = 24
|
||||
room.members[seat] = ¤tPVPMember{session: p, characterID: id, rankPoint: 1234}
|
||||
if seat > 1 {
|
||||
continue
|
||||
}
|
||||
character, _, err := repos.Character.Load(ctx, strconv.Itoa(int(id)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
character.Stats[dnfpvp.ExperienceKey] = 11950
|
||||
character.Stats[dnfpvp.RankPointKey] = 1234
|
||||
if err := repos.Character.Save(ctx, character); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := s.settleCurrentPVPDaily(room); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.publishCurrentPVPResults(room); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for seat, member := range room.members[:3] {
|
||||
wire := member.session.conn.(*bufferConn).write.Bytes()
|
||||
if seat == 2 {
|
||||
p, rest := splitCurrentGameServerUpperPacketAuto(t, wire)
|
||||
if p.Header.MsgID != 47 || len(rest) != 0 || room.match.settled[seat] != nil {
|
||||
t.Fatal("observer received rewards")
|
||||
}
|
||||
continue
|
||||
}
|
||||
state := room.match.settled[seat]
|
||||
wantDelta, wantGrade := int64(0), byte(0)
|
||||
if seat == 0 {
|
||||
wantDelta, wantGrade = 450, 1
|
||||
}
|
||||
if state.ExperienceDelta != wantDelta || state.Character.Stats[dnfpvp.CertificationKey] != 0 || state.Character.Stats[dnfpvp.RankPointKey] != 1234 {
|
||||
t.Fatal("manual mode changed ranked state")
|
||||
}
|
||||
seenResult := false
|
||||
for len(wire) > 0 {
|
||||
p, rest := splitCurrentGameServerUpperPacketAuto(t, wire)
|
||||
wire = rest
|
||||
if p.Header.MsgID == 47 {
|
||||
seenResult = true
|
||||
if int64(int32(binary.LittleEndian.Uint32(p.Body[6:]))) != wantDelta || p.Body[5] != wantGrade {
|
||||
t.Fatal("manual op47 gain/grade missing")
|
||||
}
|
||||
}
|
||||
if p.Header.MsgID == 48 && seenResult {
|
||||
if p.Body[20] != wantGrade || binary.LittleEndian.Uint32(p.Body[22:]) != 0 || int64(binary.LittleEndian.Uint32(p.Body[8:])) != 11950+wantDelta {
|
||||
t.Fatal("manual op48 new EXP/old grade missing")
|
||||
}
|
||||
}
|
||||
}
|
||||
beforeLen := member.session.conn.(*bufferConn).write.Len()
|
||||
if err := s.publishCurrentPVPResults(room); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if member.session.conn.(*bufferConn).write.Len() != beforeLen {
|
||||
t.Fatal("manual publication replayed reward")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user