服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
450 lines
17 KiB
Go
450 lines
17 KiB
Go
package dnfbridge
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/binary"
|
|
"strconv"
|
|
"testing"
|
|
|
|
"longheng.io/server/internal/modules/dnf/dnfenum"
|
|
dnfproto "longheng.io/server/internal/modules/dnf/protocol"
|
|
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
|
dnfrepomemory "longheng.io/server/internal/modules/dnf/repository/memory"
|
|
)
|
|
|
|
func pvpRoomTestService(t *testing.T) *Service {
|
|
t.Helper()
|
|
s := pvpChannelInfoTestService(t)
|
|
s.pvpDailyCatalog = pvpDailyTestCatalog()
|
|
repos := dnfrepomemory.NewMemoryGroup()
|
|
for id := 29; id <= 38; id++ {
|
|
if err := repos.Character.Save(context.Background(), dnfrepo.CharacterRecord{
|
|
CharacterID: strconv.Itoa(id), AccountID: "pvp-" + strconv.Itoa(id),
|
|
Name: "player" + strconv.Itoa(id), Job: "0", Level: 90,
|
|
Stats: map[string]int64{"grow_type": 1},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repos.Skill.Save(context.Background(), dnfrepo.SkillRecord{
|
|
CharacterID: strconv.Itoa(id), Skills: map[int64]dnfrepo.SkillState{46: {Level: id - 20, Enabled: true}},
|
|
Trees: map[int]dnfrepo.SkillTreeState{1: {Skills: map[int64]dnfrepo.SkillState{46: {Level: 1, Enabled: true}}}},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
s.repositoryProvider = func() (dnfrepo.Group, bool) { return repos, true }
|
|
return s
|
|
}
|
|
|
|
func seedPVPRoomTestSession(service *Service, session *gameSession) {
|
|
room := ¤tPVPRoom{id: 1, channelID: session.channel.ID, battleMode: 2}
|
|
room.request.roomNameType = 5
|
|
room.members[0] = ¤tPVPMember{session: session, characterID: session.selectedCharacterID, position: session.townPositionSnapshot}
|
|
room.seatState[0] = 1
|
|
session.pvpRoom = room
|
|
service.pvpRooms = map[uint16]*currentPVPRoom{1: room}
|
|
service.pvpSubscribers = map[*gameSession]int{session: session.channel.ID}
|
|
}
|
|
|
|
func TestParseCurrentPVPMakeRoomRequest(t *testing.T) {
|
|
for _, body := range [][]byte{
|
|
{5, 0, 0, 0, 0, 0},
|
|
{12, 0, 0, 0, 0, 0},
|
|
} {
|
|
request, err := parseCurrentPVPMakeRoomRequest(body)
|
|
if err != nil || request.roomNameType == 0 || request.mapIndex != 0 || request.hasPassword {
|
|
t.Fatalf("default pvp room request %x parsed as %+v, err=%v", body, request, err)
|
|
}
|
|
}
|
|
|
|
body := []byte{0, 3, 0, 0, 0, 'a', 'b', 'c', 2, 0, 1, 2, 0, 0, 0, 'x', 'y', 1}
|
|
request, err := parseCurrentPVPMakeRoomRequest(body)
|
|
if err != nil || !bytes.Equal(request.roomName, []byte("abc")) || request.mapIndex != 2 || !request.hasPassword || !bytes.Equal(request.password, []byte("xy")) || request.specialBattleMode != 1 {
|
|
t.Fatalf("named pvp room request parsed as %+v, err=%v", request, err)
|
|
}
|
|
}
|
|
|
|
func TestHandleMakePVPRoomSendsCurrentSequence(t *testing.T) {
|
|
service := pvpRoomTestService(t)
|
|
conn := &bufferConn{}
|
|
session := &gameSession{
|
|
conn: conn,
|
|
accountID: "pvp-29",
|
|
selectedCharacterID: 29,
|
|
channel: service.catalog.Channels()[1],
|
|
townPositionSnapshot: currentTownPositionSnapshot{
|
|
CharacterID: 29,
|
|
TownID: newCharacterInitialTownID,
|
|
AreaID: newCharacterInitialAreaID,
|
|
PositionX: newCharacterInitialPosX,
|
|
PositionY: newCharacterInitialPosY,
|
|
MovementCode: newCharacterInitialDirection,
|
|
PositionValid: true,
|
|
},
|
|
}
|
|
if err := service.handleMakePVPRoom(session, []byte{5, 0, 0, 0, 0, 0}, "test"); err != nil {
|
|
t.Fatalf("handle make pvp room: %v", err)
|
|
}
|
|
|
|
stream := conn.write.Bytes()
|
|
wantIDs := []uint16{
|
|
uint16(dnfenum.CmdPacketSetUDPIPPort),
|
|
currentPVPRoomInfoMsgID,
|
|
currentPVPUserAreaMsgID,
|
|
uint16(dnfenum.CmdPacketNotifyUserState),
|
|
currentActorUserStateMsgID,
|
|
}
|
|
for index, wantID := range wantIDs {
|
|
packet, rest := splitCurrentGameServerUpperPacketAuto(t, stream)
|
|
if packet.Header.Classification != 0 || packet.Header.MsgID != wantID {
|
|
t.Fatalf("packet %d = class=%d msg=%d, want msg=%d", index, packet.Header.Classification, packet.Header.MsgID, wantID)
|
|
}
|
|
if index == 1 {
|
|
if len(packet.Body) != 39 || binary.LittleEndian.Uint16(packet.Body[:2]) != 1 || binary.LittleEndian.Uint16(packet.Body[2:4]) != currentPVPEntireRoomID {
|
|
t.Fatalf("room info body=%x", packet.Body)
|
|
}
|
|
// sub_1D67AF0 consumes eight three-byte seats, then password + matching.
|
|
if !bytes.Equal(packet.Body[10:13], []byte{1, 29, 0}) ||
|
|
!bytes.Equal(packet.Body[13:34], bytes.Repeat([]byte{0xff}, 21)) ||
|
|
!bytes.Equal(packet.Body[34:], make([]byte, 5)) {
|
|
t.Fatalf("native seat/tail layout=%x", packet.Body[10:])
|
|
}
|
|
}
|
|
if index == 3 && !bytes.Equal(packet.Body, []byte{1, 29, 0, currentPVPUserState}) {
|
|
t.Fatalf("user state body=%x", packet.Body)
|
|
}
|
|
if index == 4 && !bytes.Equal(packet.Body, []byte{29, 0, 3}) {
|
|
t.Fatalf("default avatar visibility body=%x", packet.Body)
|
|
}
|
|
stream = rest
|
|
}
|
|
if len(stream) != 0 {
|
|
t.Fatalf("unexpected trailing pvp packets: %x", stream)
|
|
}
|
|
}
|
|
|
|
func TestHandlePVPSeatStateExitSendsRoomTeardownSequence(t *testing.T) {
|
|
for _, route := range []string{"legacy", "upper"} {
|
|
t.Run(route, func(t *testing.T) {
|
|
service := pvpChannelInfoTestService(t)
|
|
conn := &bufferConn{}
|
|
session := &gameSession{
|
|
conn: conn,
|
|
selectedCharacterID: 29,
|
|
channel: service.catalog.Channels()[1],
|
|
townPositionSnapshot: currentTownPositionSnapshot{
|
|
CharacterID: 29,
|
|
TownID: newCharacterInitialTownID,
|
|
AreaID: newCharacterInitialAreaID,
|
|
PositionX: newCharacterInitialPosX,
|
|
PositionY: newCharacterInitialPosY,
|
|
MovementCode: newCharacterInitialDirection,
|
|
PositionValid: true,
|
|
},
|
|
}
|
|
seedPVPRoomTestSession(service, session)
|
|
body := []byte{0, 0xFE}
|
|
if route == "legacy" {
|
|
if err := service.handleGameCommand(session, byte(dnfenum.GameCmdCommand), uint16(dnfenum.CmdPacketSetPVPSeatState), body); err != nil {
|
|
t.Fatalf("handle legacy pvp exit: %v", err)
|
|
}
|
|
} else {
|
|
request, err := dnfproto.BuildChannelPacket(uint16(dnfenum.CmdPacketSetPVPSeatState), body, 0, dnfproto.DefaultChannelClassification)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := service.handleGameUpper(session, request); err != nil {
|
|
t.Fatalf("handle upper pvp exit: %v", err)
|
|
}
|
|
}
|
|
|
|
stream := conn.write.Bytes()
|
|
wantIDs := []uint16{currentPVPUserAreaMsgID, currentSceneTransitionMsgID, uint16(dnfenum.CmdPacketNotifyUserState), currentPVPSeatStateMsgID, currentPVPRoomStateMsgID}
|
|
for index, wantID := range wantIDs {
|
|
packet, rest := splitCurrentGameServerUpperPacketAuto(t, stream)
|
|
if packet.Header.Classification != 0 || packet.Header.MsgID != wantID {
|
|
t.Fatalf("packet %d = class=%d msg=%d, want msg=%d", index, packet.Header.Classification, packet.Header.MsgID, wantID)
|
|
}
|
|
switch index {
|
|
case 0:
|
|
if packet.Body[2] != newCharacterInitialTownID || packet.Body[3] != newCharacterInitialAreaID {
|
|
t.Fatalf("town return area body=%x", packet.Body)
|
|
}
|
|
case 1:
|
|
if len(packet.Body) != 12 || packet.Body[0] != newCharacterInitialTownID || packet.Body[1] != newCharacterInitialAreaID || binary.LittleEndian.Uint16(packet.Body[4:6]) != 29 {
|
|
t.Fatalf("town scene transition body=%x", packet.Body)
|
|
}
|
|
case 2:
|
|
if !bytes.Equal(packet.Body, []byte{1, 29, 0, 0}) {
|
|
t.Fatalf("returned user state body=%x", packet.Body)
|
|
}
|
|
case 3:
|
|
if !bytes.Equal(packet.Body, []byte{1, 0, 2, 1, 0, 0xff, 0xff, 0xff}) {
|
|
t.Fatalf("native room pointer must be cleared before destruction: %x", packet.Body)
|
|
}
|
|
case 4:
|
|
if !bytes.Equal(packet.Body, []byte{1, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0}) {
|
|
t.Fatalf("destroyed room body=%x", packet.Body)
|
|
}
|
|
}
|
|
stream = rest
|
|
}
|
|
if len(stream) != 0 {
|
|
t.Fatalf("unexpected trailing pvp exit packets: %x", stream)
|
|
}
|
|
if session.pvpRoom != nil || len(service.pvpRooms) != 0 {
|
|
t.Fatal("PVP room state survived exit")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHandlePVPSeatStatePublishesSingleSeatForEnter(t *testing.T) {
|
|
for _, route := range []string{"legacy", "upper"} {
|
|
t.Run(route, func(t *testing.T) {
|
|
service := pvpChannelInfoTestService(t)
|
|
conn := &bufferConn{}
|
|
session := &gameSession{
|
|
conn: conn,
|
|
selectedCharacterID: 29,
|
|
channel: service.catalog.Channels()[1],
|
|
}
|
|
seedPVPRoomTestSession(service, session)
|
|
body := []byte{0, 2}
|
|
if route == "legacy" {
|
|
if err := service.handleGameCommand(session, byte(dnfenum.GameCmdCommand), uint16(dnfenum.CmdPacketSetPVPSeatState), body); err != nil {
|
|
t.Fatalf("handle legacy pvp seat state: %v", err)
|
|
}
|
|
} else {
|
|
request, err := dnfproto.BuildChannelPacket(uint16(dnfenum.CmdPacketSetPVPSeatState), body, 0, dnfproto.DefaultChannelClassification)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := service.handleGameUpper(session, request); err != nil {
|
|
t.Fatalf("handle upper pvp seat state: %v", err)
|
|
}
|
|
}
|
|
packet, rest := splitCurrentGameServerUpperPacketAuto(t, conn.write.Bytes())
|
|
if len(rest) != 0 || packet.Header.MsgID != currentPVPSeatStateMsgID || !bytes.Equal(packet.Body, []byte{1, 0, 2, 1, 0, 2, 29, 0}) {
|
|
t.Fatalf("seat response header=%+v body=%x rest=%x", packet.Header, packet.Body, rest)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHandlePVPTeamModeAndMapIndexPublishRoomState(t *testing.T) {
|
|
for _, route := range []string{"legacy", "upper"} {
|
|
t.Run(route, func(t *testing.T) {
|
|
service := pvpChannelInfoTestService(t)
|
|
conn := &bufferConn{}
|
|
session := &gameSession{
|
|
conn: conn,
|
|
selectedCharacterID: 29,
|
|
channel: service.catalog.Channels()[1],
|
|
}
|
|
seedPVPRoomTestSession(service, session)
|
|
dispatch := func(msgID uint16, body []byte) {
|
|
if route == "legacy" {
|
|
if err := service.handleGameCommand(session, byte(dnfenum.GameCmdCommand), msgID, body); err != nil {
|
|
t.Fatalf("handle legacy pvp command %d: %v", msgID, err)
|
|
}
|
|
return
|
|
}
|
|
request, err := dnfproto.BuildChannelPacket(msgID, body, 0, dnfproto.DefaultChannelClassification)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := service.handleGameUpper(session, request); err != nil {
|
|
t.Fatalf("handle upper pvp command %d: %v", msgID, err)
|
|
}
|
|
}
|
|
|
|
dispatch(uint16(dnfenum.CmdPacketSetPVPTeamMode), []byte{3})
|
|
modeStream := conn.write.Bytes()
|
|
modePacket, _ := splitCurrentGameServerUpperPacketAuto(t, modeStream)
|
|
if modePacket.Header.MsgID != currentPVPSeatStateMsgID || !bytes.Equal(modePacket.Body, []byte{1, 0, 3, 1, 0, 1, 29, 0}) {
|
|
t.Fatalf("team-mode response header=%+v body=%x", modePacket.Header, modePacket.Body)
|
|
}
|
|
|
|
dispatch(uint16(dnfenum.CmdPacketSetPVPMapIndex), []byte{5, 0})
|
|
mapPacket, trailing := splitCurrentGameServerUpperPacketAuto(t, conn.write.Bytes()[len(modeStream):])
|
|
if len(trailing) != 0 || mapPacket.Header.MsgID != currentPVPRoomStateMsgID || !bytes.Equal(mapPacket.Body, []byte{1, 0, 1, 0, 5, 0, 3, 0, 0, 0, 0}) {
|
|
t.Fatalf("map response header=%+v body=%x trailing=%x", mapPacket.Header, mapPacket.Body, trailing)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPVPSharedDirectoryAndMembership(t *testing.T) {
|
|
for _, route := range []string{"legacy", "upper"} {
|
|
t.Run(route, func(t *testing.T) {
|
|
s := pvpRoomTestService(t)
|
|
newSession := func(id uint16, channel int) *gameSession {
|
|
peer := &gameSession{conn: &bufferConn{}, selectedCharacterID: id, accountID: "pvp-" + strconv.Itoa(int(id)), channel: s.catalog.Channels()[1], sceneBootstrapTailSent: true}
|
|
peer.channel.ID = channel
|
|
peer.townPositionSnapshot = currentTownPositionSnapshot{CharacterID: id, TownID: 10, AreaID: 0, PositionX: 474, PositionY: 234, MovementCode: 5, PositionValid: true}
|
|
peer.partyPeer = currentPartyPeerEndpointRegistration{InnerIPv4: [4]byte{127, 0, 0, 1}, OuterIPv4: [4]byte{127, 0, 0, 1}, Port: 12000 + id, MTU: 512}
|
|
return peer
|
|
}
|
|
dispatch := func(peer *gameSession, op dnfenum.CmdPacket, body []byte) {
|
|
t.Helper()
|
|
var err error
|
|
if route == "legacy" {
|
|
err = s.handleGameCommand(peer, byte(dnfenum.GameCmdCommand), uint16(op), body)
|
|
} else {
|
|
wire, buildErr := dnfproto.BuildChannelPacket(uint16(op), body, 0, dnfproto.DefaultChannelClassification)
|
|
if buildErr != nil {
|
|
t.Fatal(buildErr)
|
|
}
|
|
err = s.handleGameUpper(peer, wire)
|
|
}
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
take := func(peer *gameSession) []dnfproto.ChannelPacket {
|
|
t.Helper()
|
|
conn := peer.conn.(*bufferConn)
|
|
wire := append([]byte(nil), conn.write.Bytes()...)
|
|
conn.write.Reset()
|
|
var packets []dnfproto.ChannelPacket
|
|
for len(wire) > 0 {
|
|
p, rest := splitCurrentGameServerUpperPacketAuto(t, wire)
|
|
packets = append(packets, p)
|
|
wire = rest
|
|
}
|
|
return packets
|
|
}
|
|
a, b, other := newSession(29, 501), newSession(30, 501), newSession(31, 502)
|
|
if err := s.sendCurrentPVPDirectory(b); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.sendCurrentPVPDirectory(other); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
dispatch(a, dnfenum.CmdPacketMakePVPRoom, []byte{5, 0, 0, 0, 0, 0})
|
|
packets := take(b)
|
|
if len(packets) != 1 || packets[0].Header.MsgID != 41 || binary.LittleEndian.Uint16(packets[0].Body[2:]) != a.pvpRoom.id {
|
|
t.Fatalf("B did not receive A's room: %+v", packets)
|
|
}
|
|
if len(take(other)) != 0 {
|
|
t.Fatal("room crossed channel boundary")
|
|
}
|
|
late := newSession(32, 501)
|
|
if _, err := s.handleRuntimePartyDirectoryRefresh(late, []byte{0}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if packets = take(late); len(packets) != 1 || packets[0].Header.MsgID != 41 {
|
|
t.Fatalf("late subscriber missed room: %+v", packets)
|
|
}
|
|
take(a)
|
|
roomID := a.pvpRoom.id
|
|
join := []byte{byte(roomID), byte(roomID >> 8), 0}
|
|
dispatch(other, dnfenum.CmdPacketEnterPVPRoom, join)
|
|
if other.pvpRoom != nil {
|
|
t.Fatal("cross-channel join accepted")
|
|
}
|
|
for _, malformed := range [][]byte{nil, {1, 0}, {1, 0, 2}, {1, 0, 1, 9, 0, 0, 0}, {1, 0, 0, 1}} {
|
|
dispatch(b, dnfenum.CmdPacketEnterPVPRoom, malformed)
|
|
if b.pvpRoom != nil {
|
|
t.Fatal("malformed join accepted")
|
|
}
|
|
}
|
|
take(b)
|
|
port := b.partyPeer.Port
|
|
b.partyPeer.Port = 0
|
|
dispatch(b, dnfenum.CmdPacketEnterPVPRoom, join)
|
|
if b.pvpRoom != nil {
|
|
t.Fatal("membership published before UDP endpoint discovery")
|
|
}
|
|
b.partyPeer.Port = port
|
|
take(b)
|
|
a.pvpRoom.request.hasPassword = true
|
|
a.pvpRoom.request.password = []byte("x")
|
|
dispatch(b, dnfenum.CmdPacketEnterPVPRoom, join)
|
|
if b.pvpRoom != nil {
|
|
t.Fatal("password-free join entered protected room")
|
|
}
|
|
take(b)
|
|
join = []byte{byte(roomID), byte(roomID >> 8), 1, 1, 0, 0, 0, 'x'}
|
|
dispatch(b, dnfenum.CmdPacketEnterPVPRoom, join)
|
|
if b.pvpRoom != a.pvpRoom || a.pvpRoom.seatFor(b) != 1 {
|
|
t.Fatal("join did not use shared room")
|
|
}
|
|
packets = take(b)
|
|
endpointIndex, roomIndex := -1, -1
|
|
for index, packet := range packets {
|
|
if packet.Header.Classification == 0 && packet.Header.MsgID == 11 {
|
|
endpointIndex = index
|
|
if len(packet.Body) != 23 || packet.Body[0] != 1 || binary.LittleEndian.Uint16(packet.Body[1:3]) != 29 ||
|
|
binary.BigEndian.Uint16(packet.Body[11:13]) != 12029 || packet.Body[17] != 1 ||
|
|
binary.LittleEndian.Uint32(packet.Body[13:17]) != a.partyRoutingKey {
|
|
t.Fatalf("peer endpoint body=%x", packet.Body)
|
|
}
|
|
}
|
|
if packet.Header.Classification == 0 && packet.Header.MsgID == 41 {
|
|
roomIndex = index
|
|
}
|
|
}
|
|
if endpointIndex < 0 || roomIndex <= endpointIndex {
|
|
t.Fatal("PVP membership started before the native UDP endpoint table")
|
|
}
|
|
last := packets[len(packets)-1]
|
|
if last.Header.Classification != 1 || last.Header.MsgID != 51 || len(last.Body) != 17 || last.Body[0] != 1 {
|
|
t.Fatalf("native join ack=%+v %x", last.Header, last.Body)
|
|
}
|
|
for seat := 0; seat < currentPVPRoomSeatCount; seat++ {
|
|
if last.Body[1+seat*2] != a.pvpRoom.ready[seat] || last.Body[2+seat*2] != 100 {
|
|
t.Fatalf("seat %d must retain readiness and start at full HP: ack=%x", seat, last.Body)
|
|
}
|
|
}
|
|
if len(packets) < 7 || packets[0].Header.MsgID != 2 || packets[0].Body[0] != 0 || packets[1].Body[0] != 1 {
|
|
t.Fatal("peer actor missing before room membership")
|
|
}
|
|
take(a)
|
|
take(late)
|
|
dispatch(b, dnfenum.CmdPacketSetPVPReadyState, []byte{1})
|
|
for _, peer := range []*gameSession{a, b} {
|
|
ready := take(peer)
|
|
if len(ready) != 1 || ready[0].Header.MsgID != 44 || !bytes.Equal(ready[0].Body, []byte{1, 1}) {
|
|
t.Fatalf("ready notification=%+v", ready)
|
|
}
|
|
}
|
|
if len(take(late)) != 0 {
|
|
t.Fatal("ready state leaked to another room/lobby")
|
|
}
|
|
dispatch(b, dnfenum.CmdPacketSetPVPMapIndex, []byte{3, 0})
|
|
if a.pvpRoom.request.mapIndex != 0 {
|
|
t.Fatal("non-owner changed map")
|
|
}
|
|
dispatch(a, dnfenum.CmdPacketSetPVPMapIndex, []byte{5, 0})
|
|
if a.pvpRoom.request.mapIndex != 5 {
|
|
t.Fatal("room map not shared")
|
|
}
|
|
dispatch(late, dnfenum.CmdPacketMakePVPRoom, []byte{6, 0, 0, 0, 0, 0})
|
|
if late.pvpRoom.id == roomID {
|
|
t.Fatal("multiple rooms reused ID")
|
|
}
|
|
take(b)
|
|
dispatch(a, dnfenum.CmdPacketSetPVPSeatState, []byte{0, 0xfe})
|
|
if a.pvpRoom != nil || b.pvpRoom == nil || b.pvpRoom.manager != 1 {
|
|
t.Fatal("owner departure did not preserve shared room")
|
|
}
|
|
take(late)
|
|
s.unbindGameSession(b)
|
|
if s.pvpRooms[roomID] != nil {
|
|
t.Fatal("disconnected room survived")
|
|
}
|
|
packets = take(late)
|
|
if len(packets) != 2 || packets[0].Header.MsgID != 43 || packets[1].Header.MsgID != 42 {
|
|
t.Fatalf("directory teardown=%+v", packets)
|
|
}
|
|
s.resetGameSessionForCharacterSelect(late)
|
|
if len(s.pvpRooms) != 0 {
|
|
t.Fatal("reselect left a ghost room")
|
|
}
|
|
})
|
|
}
|
|
}
|