服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
389 lines
18 KiB
Go
389 lines
18 KiB
Go
package dnfbridge
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"longheng.io/server/internal/modules/dnf/alignedcmd"
|
|
"longheng.io/server/internal/modules/dnf/dnfenum"
|
|
dnfproto "longheng.io/server/internal/modules/dnf/protocol"
|
|
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
|
"longheng.io/server/internal/modules/dnf/worldmap"
|
|
)
|
|
|
|
func TestTownEnterSelectOp15SendsCurrentOp27AfterAckAndFatigue(t *testing.T) {
|
|
service, session, repositories := newTownMoveTest(t)
|
|
if err := service.handleTownSetUserArea(session, buildTownMoveRequest(38, 0, 900, 250, 5)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := service.handleTownSetUserPosition(session, buildTownPositionRequest(884, 248, 6, 100)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
connection := session.conn.(*bufferConn)
|
|
connection.write.Reset()
|
|
|
|
if err := service.sendEnterSelectDungeonState(session, "test_town_op15", false, true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ack, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
|
|
if ack.Header.Classification != dnfproto.DefaultChannelClassification || ack.Header.MsgID != uint16(dnfenum.CmdPacketEnterSelectDungeon) || !bytes.Equal(ack.Body, []byte{1, 1, 0}) {
|
|
t.Fatalf("op15 ack header=%+v body=%x rest=%x", ack.Header, ack.Body, rest)
|
|
}
|
|
fatigue, rest := splitGameServerUpperPacket(t, rest)
|
|
if fatigue.Header.Classification != 0 || fatigue.Header.MsgID != currentFatigueMsgID || len(fatigue.Body) != 10 {
|
|
t.Fatalf("fatigue header=%+v body=%x rest=%x", fatigue.Header, fatigue.Body, rest)
|
|
}
|
|
skills, rest := splitGameServerUpperPacket(t, rest)
|
|
if skills.Header.Classification != 0 || skills.Header.MsgID != currentSkillInfoMsgID ||
|
|
!bytes.Equal(skills.Body, townMoveSkillProjectionBody(t, repositories, "29")) {
|
|
t.Fatalf("skills header=%+v body=%x rest=%x", skills.Header, skills.Body, rest)
|
|
}
|
|
contextPacket, trailing := splitGameServerUpperPacket(t, rest)
|
|
if contextPacket.Header.Classification != 0 || contextPacket.Header.MsgID != currentDungeonContextMsgID || len(contextPacket.Body) != 37 || len(trailing) != 0 {
|
|
t.Fatalf("op27 header=%+v body=%x trailing=%x", contextPacket.Header, contextPacket.Body, trailing)
|
|
}
|
|
if !session.enterSelectDungeonContextSent {
|
|
t.Fatal("town enter-select op27 was not recorded")
|
|
}
|
|
if !session.townSelectorOriginBound ||
|
|
session.townSelectorOriginSnapshot.CharacterID != 29 ||
|
|
session.townSelectorOriginSnapshot.TownID != 38 ||
|
|
session.townSelectorOriginSnapshot.AreaID != 0 ||
|
|
session.townSelectorOriginSnapshot.PositionX != 884 ||
|
|
session.townSelectorOriginSnapshot.PositionY != 248 ||
|
|
!session.townSelectorOriginSnapshot.PositionValid {
|
|
t.Fatalf("selector origin=%+v bound=%t", session.townSelectorOriginSnapshot, session.townSelectorOriginBound)
|
|
}
|
|
}
|
|
|
|
func TestPartyDungeonSelectorIsLeaderOwnedAndSynchronizesPassiveMemberWithoutOp15Ack(t *testing.T) {
|
|
service, leader, repositories := newTownMoveTest(t)
|
|
service.onlinePlayers = newOnlinePlayerManager()
|
|
if err := repositories.Character.Save(context.Background(), dnfrepo.CharacterRecord{
|
|
CharacterID: "17",
|
|
AccountID: "account-1",
|
|
Name: "follower",
|
|
Job: "0",
|
|
Level: 20,
|
|
Stats: map[string]int64{
|
|
"town_id": 38, "area_id": 0, "pos_x": 870, "pos_y": 248,
|
|
"direction": 6, "area_state": 3, "fatigue": 20,
|
|
},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
enableTownMoveSkillProjection(t, service, repositories, "17")
|
|
followerConn := &bufferConn{}
|
|
follower := &gameSession{
|
|
conn: followerConn,
|
|
channel: leader.channel,
|
|
residentChannel: leader.residentChannel,
|
|
connectionTownActorOwnerChannel: leader.connectionTownActorOwnerChannel,
|
|
townActorOwnerChannel: leader.townActorOwnerChannel,
|
|
selectedCharacterID: 17,
|
|
townSceneReadyCharacterID: 17,
|
|
}
|
|
service.bindGameSessionCharacter(follower, 17)
|
|
|
|
if err := service.handleTownSetUserArea(leader, buildTownMoveRequest(38, 0, 900, 250, 5)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := service.handleTownSetUserPosition(leader, buildTownPositionRequest(884, 248, 6, 100)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := service.handleTownSetUserArea(follower, buildTownMoveRequest(38, 0, 870, 248, 6)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := service.handleTownSetUserPosition(follower, buildTownPositionRequest(870, 248, 6, 100)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
partyState := alignedcmd.PartyState{
|
|
PartyID: 29,
|
|
UserID: 29,
|
|
Members: []alignedcmd.PartyMemberState{
|
|
{UserID: 29, UserState: 1, HPPercent: 100, MPPercent: 100},
|
|
{UserID: 17, UserState: 1, HPPercent: 100, MPPercent: 100},
|
|
},
|
|
}
|
|
storeRuntimePartyState(leader, partyState)
|
|
storeRuntimePartyState(follower, partyState)
|
|
if !service.onlinePlayers.PeerInSameArea(29, 17) {
|
|
t.Fatalf("party fixture members are not in the same area: leader_session=%p follower_session=%p",
|
|
service.onlinePlayers.SessionForCharacter(29),
|
|
service.onlinePlayers.SessionForCharacter(17))
|
|
}
|
|
if onlineFollower, ok := service.onlineGameSession(17); !ok || onlineFollower != follower {
|
|
t.Fatalf("party fixture follower is not online: ok=%t session=%p want=%p", ok, onlineFollower, follower)
|
|
}
|
|
if ready, reason := service.currentTownEnterSelectReady(follower); !ready {
|
|
t.Fatalf("party fixture follower selector not ready: %s snapshot=%+v", reason, follower.townPositionSnapshot)
|
|
}
|
|
leader.conn.(*bufferConn).write.Reset()
|
|
followerConn.write.Reset()
|
|
|
|
// Character 17 is an ordinary member. Same-version party selection is owned
|
|
// by slot-zero: its accidental op15 is consumed and cannot move the party.
|
|
if err := service.sendEnterSelectDungeonState(follower, "test_party_member_op15", false, true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if followerConn.write.Len() != 0 || leader.conn.(*bufferConn).write.Len() != 0 {
|
|
t.Fatalf("non-leader op15 emitted follower=%x leader=%x", followerConn.write.Bytes(), leader.conn.(*bufferConn).write.Bytes())
|
|
}
|
|
|
|
// The leader's op15 keeps its own ACK and fans the passive sequence to the
|
|
// follower without a fabricated class1/op15 response or op9 roster replay.
|
|
if err := service.sendEnterSelectDungeonState(leader, "test_party_leader_op15", false, true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stream := followerConn.write.Bytes()
|
|
fatigue, stream := splitGameServerUpperPacket(t, stream)
|
|
if fatigue.Header.Classification != 0 || fatigue.Header.MsgID != currentFatigueMsgID {
|
|
t.Fatalf("follower selector fatigue header=%+v body=%x rest=%x", fatigue.Header, fatigue.Body, stream)
|
|
}
|
|
contextPacket, trailing := splitGameServerUpperPacket(t, stream)
|
|
if contextPacket.Header.Classification != 0 || contextPacket.Header.MsgID != currentDungeonContextMsgID || len(trailing) != 0 {
|
|
t.Fatalf("follower selector context header=%+v body=%x trailing=%x", contextPacket.Header, contextPacket.Body, trailing)
|
|
}
|
|
for _, packet := range splitAllCurrentGameServerUpperPackets(t, followerConn.write.Bytes()) {
|
|
if packet.Header.MsgID == uint16(dnfenum.CmdPacketEnterSelectDungeon) ||
|
|
packet.Header.MsgID == uint16(dnfenum.CmdPacketRecoverStamina) {
|
|
t.Fatalf("passive follower received forbidden op%d body=%x", packet.Header.MsgID, packet.Body)
|
|
}
|
|
}
|
|
if !leader.enterSelectDungeonAckSent || !leader.enterSelectDungeonContextSent ||
|
|
!follower.enterSelectDungeonContextSent || follower.enterSelectDungeonAckSent ||
|
|
!follower.enterSelectDungeonPassiveParty {
|
|
t.Fatalf("selector flags leader_ack=%t leader_context=%t follower_context=%t follower_ack=%t follower_passive=%t",
|
|
leader.enterSelectDungeonAckSent, leader.enterSelectDungeonContextSent,
|
|
follower.enterSelectDungeonContextSent, follower.enterSelectDungeonAckSent,
|
|
follower.enterSelectDungeonPassiveParty)
|
|
}
|
|
}
|
|
|
|
func TestTownTransportSceneFinalizerOp15IsAcknowledgedWithoutPrematureContext(t *testing.T) {
|
|
service, session, _ := newTownMoveTest(t)
|
|
session.townTransportEnterSelectPending = true
|
|
connection := session.conn.(*bufferConn)
|
|
connection.write.Reset()
|
|
|
|
if err := service.sendEnterSelectDungeonState(session, "test_town_transport_scene_finalizer", false, true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if connection.write.Len() == 0 || session.townTransportEnterSelectPending ||
|
|
!session.enterSelectDungeonSent || !session.enterSelectDungeonAckSent || session.enterSelectDungeonContextSent {
|
|
t.Fatalf("town transport finalizer wrote=%x pending=%t selector_sent=%t ack_sent=%t context_sent=%t",
|
|
connection.write.Bytes(), session.townTransportEnterSelectPending,
|
|
session.enterSelectDungeonSent, session.enterSelectDungeonAckSent, session.enterSelectDungeonContextSent)
|
|
}
|
|
}
|
|
|
|
func TestTownTransportFirstOp15UsesArrivalPositionForSelectorContext(t *testing.T) {
|
|
service, session, _ := newTownMoveTest(t)
|
|
session.townTransportEnterSelectPending = true
|
|
session.townPositionSnapshot = currentTownPositionSnapshot{
|
|
CharacterID: session.selectedCharacterID,
|
|
TownID: 38,
|
|
AreaID: 0,
|
|
PositionX: 916,
|
|
PositionY: 224,
|
|
MovementCode: 5,
|
|
PositionValid: true,
|
|
}
|
|
if err := service.sendEnterSelectDungeonState(session, "test_town_transport_first_gate_click", false, true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !session.enterSelectDungeonAckSent || !session.enterSelectDungeonContextSent ||
|
|
session.townTransportEnterSelectPending {
|
|
t.Fatalf("ack=%t context=%t pending=%t", session.enterSelectDungeonAckSent, session.enterSelectDungeonContextSent, session.townTransportEnterSelectPending)
|
|
}
|
|
packets := splitAllCurrentGameServerUpperPackets(t, session.conn.(*bufferConn).write.Bytes())
|
|
seen := map[uint16]bool{}
|
|
for _, packet := range packets {
|
|
seen[packet.Header.MsgID] = true
|
|
}
|
|
for _, msgID := range []uint16{uint16(dnfenum.CmdPacketEnterSelectDungeon), currentFatigueMsgID, currentSkillInfoMsgID, currentDungeonContextMsgID} {
|
|
if !seen[msgID] {
|
|
t.Fatalf("first transport op15 missing msg=%d packets=%v", msgID, packets)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTownEnterSelectOp15DoesNotSendOp27BeforeTownReady(t *testing.T) {
|
|
service, session, _ := newTownMoveTest(t)
|
|
if err := service.sendEnterSelectDungeonState(session, "test_unready_op15", false, true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, rest := splitGameServerUpperPacket(t, session.conn.(*bufferConn).write.Bytes())
|
|
_, trailing := splitGameServerUpperPacket(t, rest)
|
|
if len(trailing) != 0 || session.enterSelectDungeonContextSent {
|
|
t.Fatalf("unready town op15 emitted trailing=%x context_sent=%t", trailing, session.enterSelectDungeonContextSent)
|
|
}
|
|
}
|
|
|
|
func TestTownEnterSelectOp15DoesNotSendOp27WithoutTownLocationOwner(t *testing.T) {
|
|
service, session, _ := newTownMoveTest(t)
|
|
service.markCurrentTownSceneReady(session)
|
|
if err := service.sendEnterSelectDungeonState(session, "test_missing_location_op15", false, true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, rest := splitGameServerUpperPacket(t, session.conn.(*bufferConn).write.Bytes())
|
|
_, trailing := splitGameServerUpperPacket(t, rest)
|
|
if len(trailing) != 0 || session.enterSelectDungeonContextSent || session.townSelectorOriginBound {
|
|
t.Fatalf("missing-location op15 emitted trailing=%x context_sent=%t origin_bound=%t", trailing, session.enterSelectDungeonContextSent, session.townSelectorOriginBound)
|
|
}
|
|
}
|
|
|
|
func TestTownEnterSelectOp15DoesNotSendOp27WithoutCurrentOp35Position(t *testing.T) {
|
|
service, session, _ := newTownMoveTest(t)
|
|
if err := service.handleTownSetUserArea(session, buildTownMoveRequest(38, 0, 900, 250, 5)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
connection := session.conn.(*bufferConn)
|
|
connection.write.Reset()
|
|
|
|
if err := service.sendEnterSelectDungeonState(session, "test_missing_position_op15", false, true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
|
|
_, trailing := splitGameServerUpperPacket(t, rest)
|
|
if len(trailing) != 0 || session.enterSelectDungeonContextSent || session.townSelectorOriginBound {
|
|
t.Fatalf(
|
|
"missing-position op15 emitted trailing=%x context_sent=%t origin_bound=%t",
|
|
trailing,
|
|
session.enterSelectDungeonContextSent,
|
|
session.townSelectorOriginBound,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestTownEnterSelectOp15DoesNotSendOp27WithActiveDungeonRuntime(t *testing.T) {
|
|
service, session, _ := newTownMoveTest(t)
|
|
service.markCurrentTownSceneReady(session)
|
|
session.dungeon.runtime = &runtimeDungeonState{}
|
|
if err := service.sendEnterSelectDungeonState(session, "test_active_runtime_op15", false, true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, rest := splitGameServerUpperPacket(t, session.conn.(*bufferConn).write.Bytes())
|
|
_, trailing := splitGameServerUpperPacket(t, rest)
|
|
if len(trailing) != 0 || session.enterSelectDungeonContextSent {
|
|
t.Fatalf("active-runtime op15 emitted trailing=%x context_sent=%t", trailing, session.enterSelectDungeonContextSent)
|
|
}
|
|
}
|
|
|
|
func TestTownEnterSelectOp15AfterBackToVillageOpensSelector(t *testing.T) {
|
|
service, session, runtime := newBackToVillageRuntime(t)
|
|
if err := service.handleDungeonBackToVillage(session, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !runtime.townReturnPending || !runtime.townReturnOp24Sent {
|
|
t.Fatalf("return did not complete its full route: %+v", runtime)
|
|
}
|
|
connection := session.conn.(*bufferConn)
|
|
if got := townPostTransitionPacketSignatures(t, connection.write.Bytes()); !reflect.DeepEqual(
|
|
got,
|
|
[]string{"op42", "op376", "mode0", "mode1", "op205", "op24", "op559", "op33weakness"},
|
|
) {
|
|
t.Fatalf("full return route=%v", got)
|
|
}
|
|
if session.dungeon.runtime != nil {
|
|
t.Fatalf("full return retained runtime=%+v", session.dungeon.runtime)
|
|
}
|
|
connection.write.Reset()
|
|
|
|
if err := service.sendEnterSelectDungeonState(session, "test_op15_after_pending_town_return", false, true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
ack, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
|
|
if ack.Header.Classification != dnfproto.DefaultChannelClassification ||
|
|
ack.Header.MsgID != uint16(dnfenum.CmdPacketEnterSelectDungeon) ||
|
|
!bytes.Equal(ack.Body, []byte{1, 1, 0}) {
|
|
t.Fatalf("op15 ack header=%+v body=%x rest=%x", ack.Header, ack.Body, rest)
|
|
}
|
|
fatigue, rest := splitGameServerUpperPacket(t, rest)
|
|
if fatigue.Header.Classification != 0 || fatigue.Header.MsgID != currentFatigueMsgID {
|
|
t.Fatalf("op15 fatigue header=%+v body=%x rest=%x", fatigue.Header, fatigue.Body, rest)
|
|
}
|
|
skills, rest := splitGameServerUpperPacket(t, rest)
|
|
if skills.Header.Classification != 0 || skills.Header.MsgID != currentSkillInfoMsgID {
|
|
t.Fatalf("op15 skills header=%+v body=%x rest=%x", skills.Header, skills.Body, rest)
|
|
}
|
|
contextPacket, trailing := splitGameServerUpperPacket(t, rest)
|
|
if contextPacket.Header.Classification != 0 || contextPacket.Header.MsgID != currentDungeonContextMsgID ||
|
|
len(contextPacket.Body) != 37 || len(trailing) != 0 {
|
|
t.Fatalf("op15 context header=%+v body=%x trailing=%x", contextPacket.Header, contextPacket.Body, trailing)
|
|
}
|
|
if session.dungeon.runtime != nil || !session.enterSelectDungeonContextSent {
|
|
t.Fatalf("first post-return op15 did not open selector runtime=%p context=%t", session.dungeon.runtime, session.enterSelectDungeonContextSent)
|
|
}
|
|
if status := runtime.Session.Snapshot().Run.Status; status != worldmap.DungeonRunAbandoned {
|
|
t.Fatalf("pending active return status=%s want abandoned", status)
|
|
}
|
|
if session.backToVillageEnterSelectPending || session.backToVillageSelectorDuplicateGuard {
|
|
t.Fatalf("full return retained legacy two-stage flags pending=%t duplicate_guard=%t",
|
|
session.backToVillageEnterSelectPending, session.backToVillageSelectorDuplicateGuard)
|
|
}
|
|
if snapshot := session.townPositionSnapshot; snapshot.CharacterID != session.selectedCharacterID ||
|
|
snapshot.TownID != 7 || snapshot.AreaID != 3 || snapshot.PositionX != 474 ||
|
|
snapshot.PositionY != 234 || !snapshot.PositionValid {
|
|
t.Fatalf("post-return town snapshot=%+v", snapshot)
|
|
}
|
|
}
|
|
|
|
func TestCompletedSettlementOp15RetiresRuntimeAndOpensOtherDungeonSelector(t *testing.T) {
|
|
service, runtime, session, connection, _ := prepareCompletedSettlementRuntime(t)
|
|
repositories, ok := service.repositoryGroup()
|
|
if !ok {
|
|
t.Fatal("completed settlement skill repository unavailable")
|
|
}
|
|
enableTownMoveSkillProjection(t, service, repositories, "99")
|
|
connection.write.Reset()
|
|
plan := currentDungeonSettlementPacketPlanForTest(t, runtime, 0)
|
|
runtime.settlementResultPlan = &plan
|
|
if err := service.handleDungeonSetPlayResult(session, make([]byte, currentDungeonPlayResultBaseSize)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := service.handleDungeonCharacterStatistic(session, make([]byte, currentDungeonCharacterStatisticBodySize)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
connection.write.Reset()
|
|
if err := service.sendEnterSelectDungeonState(session, "test_completed_other_dungeon", false, true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
ack, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
|
|
if ack.Header.Classification != dnfproto.DefaultChannelClassification ||
|
|
ack.Header.MsgID != uint16(dnfenum.CmdPacketEnterSelectDungeon) ||
|
|
!bytes.Equal(ack.Body, []byte{1, 1, 0}) {
|
|
t.Fatalf("completed op15 ack=%+v body=%x rest=%x", ack.Header, ack.Body, rest)
|
|
}
|
|
fatigue, rest := splitGameServerUpperPacket(t, rest)
|
|
if fatigue.Header.Classification != 0 || fatigue.Header.MsgID != currentFatigueMsgID {
|
|
t.Fatalf("completed op15 fatigue=%+v body=%x rest=%x", fatigue.Header, fatigue.Body, rest)
|
|
}
|
|
skills, rest := splitGameServerUpperPacket(t, rest)
|
|
if skills.Header.Classification != 0 || skills.Header.MsgID != currentSkillInfoMsgID ||
|
|
!bytes.Equal(skills.Body, backToVillageSkillProjectionBody(t, service)) {
|
|
t.Fatalf("completed op15 skills=%+v body=%x rest=%x", skills.Header, skills.Body, rest)
|
|
}
|
|
contextPacket, trailing := splitGameServerUpperPacket(t, rest)
|
|
if contextPacket.Header.Classification != 0 || contextPacket.Header.MsgID != currentDungeonContextMsgID ||
|
|
len(contextPacket.Body) != 37 || len(trailing) != 0 {
|
|
t.Fatalf("completed op15 context=%+v body=%x trailing=%x", contextPacket.Header, contextPacket.Body, trailing)
|
|
}
|
|
if session.dungeon.runtime != nil || !session.enterSelectDungeonContextSent ||
|
|
session.townSceneReadyCharacterID != session.selectedCharacterID ||
|
|
session.townPositionSnapshot.CharacterID != session.selectedCharacterID ||
|
|
!session.townPositionSnapshot.PositionValid ||
|
|
runtime.Session.Snapshot().Run.Status != worldmap.DungeonRunCompleted {
|
|
t.Fatalf("completed op15 runtime=%p context=%t ready=%d position=%+v run=%+v",
|
|
session.dungeon.runtime,
|
|
session.enterSelectDungeonContextSent,
|
|
session.townSceneReadyCharacterID,
|
|
session.townPositionSnapshot,
|
|
runtime.Session.Snapshot().Run)
|
|
}
|
|
}
|