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,415 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
dnftown "longheng.io/server/internal/modules/dnf/town"
|
||||
platformpvf "longheng.io/server/internal/platform/pvf"
|
||||
)
|
||||
|
||||
func newWeddingRoomExitTest(t *testing.T) (*Service, *gameSession, dnfrepo.Group) {
|
||||
t.Helper()
|
||||
s, session, repos := newTownMoveTest(t)
|
||||
table, err := dnftown.Load(context.Background(), townMoveTestSource{
|
||||
"town/town.lst": "2 `old.twn` 38 `seria.twn` 42 `underfoot.twn` 101 `couple.twn`",
|
||||
"town/old.twn": "[area]\n5 `Town/Underfoot/Underfoot_DT_Or_Icewall.map` `[normal]`\n[/area]",
|
||||
"town/seria.twn": "[area]\n0 `Cataclysm/Town/Elvengard/new_Elvengard.map` `[normal]`\n[/area]\n[area]\n1 `Cataclysm/Town/Elvengard/new_seria_room.map` `[gate]` 450 234\n[/area]",
|
||||
"town/underfoot.twn": "[area]\n0 `Cataclysm/Town/Underfoot/Underfoot_north.map` `[normal]`\n[/area]",
|
||||
"town/couple.twn": "[exclude user pretown info]\n[area]\n0 `CoupleRoom/CoupleRoom.map` `[couple room]` 474 234\n[/area]",
|
||||
"map/Cataclysm/Town/Elvengard/new_seria_room.map": "[town movable area]\n858 183 30 100 38 0 49 224 34 52 101 0\n[/town movable area]",
|
||||
"map/Cataclysm/Town/Elvengard/new_Elvengard.map": "[town movable area]\n10 196 90 142 38 2 622 196 0 0 38 1 571 129 50 50 38 1\n[/town movable area]",
|
||||
}, dnftown.Options{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.townCatalog = table
|
||||
c := weddingExitLoad(t, repos)
|
||||
c.Stats["wedding_state"], c.Stats["wedding_id"], c.Stats["wedding_partner_id"] = 4, 1900574, 30
|
||||
weddingExitSave(t, repos, c)
|
||||
peer := dnfrepo.CloneCharacter(c)
|
||||
peer.CharacterID, peer.AccountID = "30", "dnf:2"
|
||||
peer.Stats["wedding_partner_id"] = 29
|
||||
weddingExitSave(t, repos, peer)
|
||||
return s, session, repos
|
||||
}
|
||||
|
||||
func weddingExitLoad(t *testing.T, repos dnfrepo.Group) dnfrepo.CharacterRecord {
|
||||
t.Helper()
|
||||
c, found, err := repos.Character.Load(context.Background(), "29")
|
||||
if err != nil || !found {
|
||||
t.Fatalf("load character: found=%v err=%v", found, err)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func weddingExitSave(t *testing.T, repos dnfrepo.Group, c dnfrepo.CharacterRecord) {
|
||||
t.Helper()
|
||||
if err := repos.Character.Save(context.Background(), c); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func weddingNativeExitPacket(t *testing.T) []byte {
|
||||
t.Helper()
|
||||
// Captured 2026-09-12 02:36:51: old Underfoot 2/5, 300/200, source 101.
|
||||
body, err := hex.DecodeString("02052c01c80005650000000000000000")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
func TestWeddingRoomExitNativeRoundTrip(t *testing.T) {
|
||||
for _, remembered := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("native_remembered_entry_%v", remembered), func(t *testing.T) {
|
||||
s, session, repos := newWeddingRoomExitTest(t)
|
||||
c := weddingExitLoad(t, repos)
|
||||
c.Stats["town_id"], c.Stats["area_id"], c.Stats["pos_x"], c.Stats["pos_y"] = 42, 0, 497, 289
|
||||
weddingExitSave(t, repos, c)
|
||||
entry := buildTownCrossTownPortalRequest(101, 0, 900, 250, 5, 38)
|
||||
entry[9] = 1 // Live 02:57 entry carries source area 1 in opaque B.
|
||||
for _, body := range [][]byte{
|
||||
buildTownSeriaRoomPortalRequest(38, 1, 433, 311, 5, 42),
|
||||
entry,
|
||||
} {
|
||||
if err := s.handleTownSetUserArea(session, body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if got := weddingExitLoad(t, repos); got.Stats["town_id"] != 101 || got.Stats[currentTownPrevVillageTownStat] != 42 {
|
||||
t.Fatalf("entry/previous town: %+v", got.Stats)
|
||||
}
|
||||
body := weddingNativeExitPacket(t)
|
||||
if remembered {
|
||||
body[0], body[1] = 38, 1
|
||||
}
|
||||
session.conn.(*bufferConn).write.Reset()
|
||||
if err := s.handleTownSetUserArea(session, body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, rest := splitTownAreaTransitionSequence(t, session.conn.(*bufferConn).write.Bytes(), 29, 38, 1, 100, 250, 5, 3)
|
||||
if len(rest) != 0 {
|
||||
t.Fatalf("extra exit packets: %x", rest)
|
||||
}
|
||||
c = weddingExitLoad(t, repos)
|
||||
if c.Stats["town_id"] != 38 || c.Stats["area_id"] != 1 || c.Stats["pos_x"] != 100 || c.Stats["pos_y"] != 250 ||
|
||||
c.Stats[currentTownPrevVillageTownStat] != 42 || c.Stats[currentTownPrevVillagePosXStat] != 497 ||
|
||||
session.townPrevVillageSnapshot.TownID != 42 || c.Stats["wedding_state"] != 4 {
|
||||
t.Fatalf("exit location/previous town/wedding state: %+v %+v", c.Stats, session.townPrevVillageSnapshot)
|
||||
}
|
||||
// A replay after leaving 101 must not teleport to legacy Underfoot.
|
||||
session.conn.(*bufferConn).write.Reset()
|
||||
if err := s.handleTownSetUserArea(session, weddingNativeExitPacket(t)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if session.conn.(*bufferConn).write.Len() != 0 {
|
||||
t.Fatal("replayed exit emitted transition")
|
||||
}
|
||||
if err := s.handleCurrentPrevVillage(session, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c = weddingExitLoad(t, repos)
|
||||
if c.Stats["town_id"] != 42 || c.Stats["area_id"] != 0 || c.Stats["pos_x"] != 497 || c.Stats["pos_y"] != 289 {
|
||||
t.Fatalf("Seria door lost public origin: %+v", c.Stats)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWeddingRoomExitSendFailureRollsBack(t *testing.T) {
|
||||
for failAt := 1; failAt <= 3; failAt++ {
|
||||
t.Run(fmt.Sprint(failAt), func(t *testing.T) {
|
||||
s, session, repos := newWeddingRoomExitTest(t)
|
||||
c := weddingExitLoad(t, repos)
|
||||
c.Stats["town_id"], c.Stats["area_id"], c.Stats["pos_x"], c.Stats["pos_y"] = 101, 0, 1000, 250
|
||||
weddingExitSave(t, repos, c)
|
||||
want := errors.New("wedding exit transport failure")
|
||||
session.conn = &failNthDungeonWriteConn{failAt: failAt, err: want}
|
||||
if err := s.handleTownSetUserArea(session, weddingNativeExitPacket(t)); !errors.Is(err, want) {
|
||||
t.Fatalf("error=%v", err)
|
||||
}
|
||||
if got := weddingExitLoad(t, repos); !reflect.DeepEqual(got.Stats, c.Stats) {
|
||||
t.Fatalf("failed exit persisted location: %+v", got.Stats)
|
||||
}
|
||||
if session.townPrevVillageSnapshot.PositionValid {
|
||||
t.Fatal("failed exit committed return snapshot")
|
||||
}
|
||||
session.conn = &bufferConn{}
|
||||
if err := s.handleTownSetUserArea(session, weddingNativeExitPacket(t)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := weddingExitLoad(t, repos); got.Stats["town_id"] != 38 {
|
||||
t.Fatalf("retry: %+v", got.Stats)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 工作区与上游在这一点上的锚点约定不同,这里断言**不变量**而不是上游的坐标:
|
||||
//
|
||||
// - 上游的 currentWeddingPublicReturnOrigin 读的是当前城镇那扇"零尺寸反向传送门"
|
||||
// (width==0 && height==0),并另外在登录路径上擦洗被污染的 prev_village。
|
||||
// - 工作区的 town.seriaRoomPortalTrigger 只认正尺寸传送门(零尺寸行是纯触发器,
|
||||
// 不构成"门"),且回城点修复收敛在 handleCurrentPrevVillage 一处。
|
||||
//
|
||||
// 两边都必须保证的不变量是同一个:**污染的(101)回城点绝不能被当成目的地**,
|
||||
// 而且 op1425 必须给出一个真正位于城镇居民区(38/0)、不在任何传送门触发矩形里的
|
||||
// 落点。下面按这个不变量断言。
|
||||
func TestWeddingRoomExitRelogAndStaleOrigin(t *testing.T) {
|
||||
for _, currentTown := range []int64{38, 101} {
|
||||
for _, oldOrigin := range []byte{42, 101} {
|
||||
t.Run(fmt.Sprintf("current_%d_origin_%d", currentTown, oldOrigin), func(t *testing.T) {
|
||||
s, session, repos := newWeddingRoomExitTest(t)
|
||||
c := weddingExitLoad(t, repos)
|
||||
c.Stats["town_id"], c.Stats["area_id"] = currentTown, 0
|
||||
if currentTown == 38 {
|
||||
c.Stats["area_id"] = 1
|
||||
}
|
||||
currentTownPrevVillageApplyOriginStats(c.Stats, currentTownPositionSnapshot{CharacterID: 29, TownID: oldOrigin, AreaID: 0, PositionX: 497, PositionY: 289, MovementCode: 5, PositionValid: true})
|
||||
weddingExitSave(t, repos, c)
|
||||
// 登录仍然进赛丽亚房间,且被污染的 101 回城点不能留到会话里。
|
||||
_, town, area, row, _, err := s.currentCharacterListLoginTransition(context.Background(), session, 29, c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if town != 38 || area != 1 || row.Value1 != 450 || row.Value2 != 234 {
|
||||
t.Fatalf("login transition: town=%d area=%d row=%+v", town, area, row)
|
||||
}
|
||||
if err := s.handleCurrentPrevVillage(session, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := weddingExitLoad(t, repos)
|
||||
if got.Stats[currentTownPrevVillageTownStat] == 101 {
|
||||
t.Fatalf("wedding room survived as the previous town: %+v", got.Stats)
|
||||
}
|
||||
if oldOrigin == 42 {
|
||||
// 记忆里是一个正常的城镇 -> 就回那个城镇,不要被婚房场景干扰。
|
||||
if got.Stats["town_id"] != 42 || got.Stats["area_id"] != 0 ||
|
||||
got.Stats["pos_x"] != 497 || got.Stats["pos_y"] != 289 {
|
||||
t.Fatalf("legitimate previous town lost: %+v", got.Stats)
|
||||
}
|
||||
return
|
||||
}
|
||||
// 记忆本身就是婚房 -> 必须换成城镇居民区里的安全落点。
|
||||
if got.Stats["town_id"] != 38 || got.Stats["area_id"] != 0 {
|
||||
t.Fatalf("door landed outside the public town: %+v", got.Stats)
|
||||
}
|
||||
// 落点必须是安全站立点:不落在 38/0 的任何传送门触发矩形里。
|
||||
area38, ok := s.townArea(38, 0)
|
||||
if !ok {
|
||||
t.Fatal("town 38 area 0 missing from the fixture")
|
||||
}
|
||||
for _, portal := range area38.MovableAreas {
|
||||
if portal.Width <= 0 || portal.Height <= 0 {
|
||||
continue
|
||||
}
|
||||
if got.Stats["pos_x"] >= portal.X && got.Stats["pos_x"] <= portal.X+portal.Width &&
|
||||
got.Stats["pos_y"] >= portal.Y && got.Stats["pos_y"] <= portal.Y+portal.Height {
|
||||
t.Fatalf("landing retriggers portal %+v: %+v", portal, got.Stats)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 会话里残留的婚房快照同样不能被当成目的地。
|
||||
func TestWeddingRoomExitStaleOnlineOrigin(t *testing.T) {
|
||||
s, session, repos := newWeddingRoomExitTest(t)
|
||||
c := weddingExitLoad(t, repos)
|
||||
stale := currentTownPositionSnapshot{CharacterID: 29, TownID: 101, AreaID: 0, PositionX: 871, PositionY: 250, MovementCode: 6, PositionValid: true}
|
||||
currentTownPrevVillageApplyOriginStats(c.Stats, stale)
|
||||
weddingExitSave(t, repos, c)
|
||||
session.townPrevVillageSnapshot = stale
|
||||
if err := s.handleCurrentPrevVillage(session, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := weddingExitLoad(t, repos)
|
||||
if got.Stats["town_id"] != 38 || got.Stats["area_id"] != 0 {
|
||||
t.Fatalf("stale online door left the public town: %+v", got.Stats)
|
||||
}
|
||||
if got.Stats[currentTownPrevVillageTownStat] == 101 {
|
||||
t.Fatalf("stale wedding-room origin was committed: %+v", got.Stats)
|
||||
}
|
||||
if got.Stats["pos_x"] == 871 && got.Stats["pos_y"] == 250 {
|
||||
t.Fatalf("stale wedding-room position was reused verbatim: %+v", got.Stats)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWeddingRoomExitRoutingBoundaries(t *testing.T) {
|
||||
for _, variant := range []string{"normal_underfoot", "teleport_from_room", "source_mismatch", "scene_mismatch", "missing_gate", "unmarried_entry"} {
|
||||
t.Run(variant, func(t *testing.T) {
|
||||
s, session, repos := newWeddingRoomExitTest(t)
|
||||
c := weddingExitLoad(t, repos)
|
||||
c.Stats["town_id"], c.Stats["area_id"] = 101, 0
|
||||
body := weddingNativeExitPacket(t)
|
||||
wantTown, wantArea, wantX, blocked := int64(101), int64(0), int64(450), true
|
||||
switch variant {
|
||||
case "normal_underfoot":
|
||||
c.Stats["town_id"] = 42
|
||||
body[7] = 42
|
||||
wantTown, wantArea, wantX, blocked = 2, 5, 300, false
|
||||
case "teleport_from_room":
|
||||
body = buildTownTeleportArrayRequest(42, 0, 497, 289, 5)
|
||||
wantTown, wantArea, wantX, blocked = 42, 0, 497, false
|
||||
case "source_mismatch":
|
||||
body[7] = 38
|
||||
case "scene_mismatch":
|
||||
// 场景快照对不上时**放行客户端原请求**,不再中止移动。
|
||||
// 教训:早先这里硬拒,一旦装载期推导不出落点,玩家会被永久困在
|
||||
// 婚房里出不去(2026-10-04 真机)。困住玩家远比落点不精确糟糕。
|
||||
session.townPositionSnapshot = currentTownPositionSnapshot{CharacterID: 29, TownID: 42, AreaID: 0}
|
||||
wantTown, wantArea, wantX, blocked = 2, 5, 300, false
|
||||
case "missing_gate":
|
||||
// 目录里根本没有 38/1(推导不出落点)→ 同样放行客户端原请求。
|
||||
s.townCatalog, _ = dnftown.Load(context.Background(), townMoveTestSource{"town/town.lst": "2 `old.twn`", "town/old.twn": "[area]\n5 `old.map` `[normal]`\n[/area]"}, dnftown.Options{})
|
||||
wantTown, wantArea, wantX, blocked = 2, 5, 300, false
|
||||
case "unmarried_entry":
|
||||
c.Stats["town_id"], c.Stats["area_id"], c.Stats["wedding_state"] = 38, 1, 0
|
||||
body = buildTownCrossTownPortalRequest(101, 0, 900, 250, 5, 38)
|
||||
wantTown, wantArea = 38, 1
|
||||
}
|
||||
weddingExitSave(t, repos, c)
|
||||
if err := s.handleTownSetUserArea(session, body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := weddingExitLoad(t, repos)
|
||||
if got.Stats["town_id"] != wantTown || got.Stats["area_id"] != wantArea || got.Stats["pos_x"] != wantX {
|
||||
t.Fatalf("routing: %+v", got.Stats)
|
||||
}
|
||||
if blocked && session.conn.(*bufferConn).write.Len() != 0 {
|
||||
t.Fatal("blocked route emitted packets")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 落点由装载期从 PVF 推导(town/wedding_room_return.go),其等式本身在
|
||||
// town/wedding_room_return_test.go 里断言。这里证明 bridge 取到的就是目录里那个
|
||||
// 点,且确实被送到了 38/1 的婚房门旁(而不是城镇居民区)。
|
||||
func TestWeddingRoomExitDoorCoordinatesFollowPVF(t *testing.T) {
|
||||
s, session, repos := newWeddingRoomExitTest(t)
|
||||
seria, found := s.townArea(38, 1)
|
||||
if !found || seria.WeddingRoomReturn == nil {
|
||||
t.Fatalf("fixture couple-room doorway missing: found=%v area=%+v", found, seria)
|
||||
}
|
||||
if got := *seria.WeddingRoomReturn; got.X != 100 || got.Y != 250 {
|
||||
t.Fatalf("couple-room doorway = %+v, want (100,250)", got)
|
||||
}
|
||||
c := weddingExitLoad(t, repos)
|
||||
c.Stats["town_id"], c.Stats["area_id"] = 101, 0
|
||||
weddingExitSave(t, repos, c)
|
||||
session.townMu.Lock()
|
||||
session.townPositionSnapshot = currentTownPositionSnapshot{
|
||||
CharacterID: session.selectedCharacterID, TownID: 101, AreaID: 0, PositionValid: true,
|
||||
}
|
||||
session.townMu.Unlock()
|
||||
|
||||
normalized, exit, err := s.normalizeCurrentWeddingRoomExit(session, c, currentTownSetUserAreaRequest{
|
||||
TownID: 1, AreaID: 1, PositionX: 300, PositionY: 200, Direction: 5,
|
||||
OpaqueU16A: 101, OpaqueU16B: 0, OpaqueU32: 0, OpaqueTailU8: 0,
|
||||
})
|
||||
if err != nil || !exit {
|
||||
t.Fatalf("normalize: exit=%v err=%v", exit, err)
|
||||
}
|
||||
if normalized.TownID != 38 || normalized.AreaID != 1 ||
|
||||
normalized.PositionX != 100 || normalized.PositionY != 250 ||
|
||||
normalized.Direction != newCharacterInitialDirection {
|
||||
t.Fatalf("normalized request = %+v", normalized)
|
||||
}
|
||||
}
|
||||
|
||||
// 地图里没有这道门时**必须放行**,而不是拒绝 —— 绝不把玩家困在婚房里。
|
||||
// 落点优先级:WeddingRoomReturn → SeriaRoomReturn → 地图自己的 [gate]。
|
||||
func TestWeddingRoomExitWithoutDoorFallsBackInsteadOfBlocking(t *testing.T) {
|
||||
s, session, repos := newWeddingRoomExitTest(t)
|
||||
table, err := dnftown.Load(context.Background(), townMoveTestSource{
|
||||
"town/town.lst": "38 `seria.twn`",
|
||||
"town/seria.twn": "[area]\n1 `Cataclysm/Town/Elvengard/new_seria_room.map` `[gate]` 450 234\n[/area]",
|
||||
"map/Cataclysm/Town/Elvengard/new_seria_room.map": "[town movable area]\n858 183 30 100 38 0\n[/town movable area]",
|
||||
}, dnftown.Options{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.townCatalog = table
|
||||
c := weddingExitLoad(t, repos)
|
||||
c.Stats["town_id"], c.Stats["area_id"] = 101, 0
|
||||
weddingExitSave(t, repos, c)
|
||||
normalized, exit, err := s.normalizeCurrentWeddingRoomExit(session, c, currentTownSetUserAreaRequest{
|
||||
TownID: 1, AreaID: 1, PositionX: 300, PositionY: 200, Direction: 5,
|
||||
OpaqueU16A: 101, OpaqueU16B: 0, OpaqueU32: 0, OpaqueTailU8: 0,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("缺少门时不应报错(会把玩家困住): %v", err)
|
||||
}
|
||||
if !exit {
|
||||
t.Fatalf("应当按回退链归一化到地图 [gate]: %+v", normalized)
|
||||
}
|
||||
// 回退链第三级:地图自己的 [gate] 450 234(该图没有 101 传送门,
|
||||
// 所以 WeddingRoomReturn/SeriaRoomReturn 都是 nil)。
|
||||
if normalized.TownID != 38 || normalized.AreaID != 1 || normalized.PositionX != 450 || normalized.PositionY != 234 {
|
||||
t.Fatalf("回退落点不正确: %+v", normalized)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWeddingRoomExitRealPVF(t *testing.T) {
|
||||
path := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
|
||||
if path == "" {
|
||||
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE")
|
||||
}
|
||||
archive, err := platformpvf.LoadArchive(platformpvf.Options{Path: path})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
table, err := dnftown.Load(context.Background(), archive, dnftown.Options{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s, session, repos := newWeddingRoomExitTest(t)
|
||||
s.townCatalog = table
|
||||
seria, found := table.FindArea(38, 1)
|
||||
if !found || seria.WeddingRoomReturn == nil {
|
||||
t.Fatalf("real PVF couple-room doorway missing: found=%v area=%+v", found, seria)
|
||||
}
|
||||
arrival := *seria.WeddingRoomReturn
|
||||
// 落点必须在这张图的每一处传送门触发矩形之外,否则客户端一落地就被送回婚房。
|
||||
for _, portal := range seria.MovableAreas {
|
||||
if portal.Width <= 0 || portal.Height <= 0 {
|
||||
continue
|
||||
}
|
||||
if arrival.X >= portal.X && arrival.X <= portal.X+portal.Width &&
|
||||
arrival.Y >= portal.Y && arrival.Y <= portal.Y+portal.Height {
|
||||
t.Fatalf("arrival would retrigger portal: %+v", portal)
|
||||
}
|
||||
}
|
||||
c := weddingExitLoad(t, repos)
|
||||
c.Stats["town_id"], c.Stats["area_id"] = 101, 0
|
||||
weddingExitSave(t, repos, c)
|
||||
if err := s.handleTownSetUserArea(session, weddingNativeExitPacket(t)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
splitTownAreaTransitionSequence(t, session.conn.(*bufferConn).write.Bytes(),
|
||||
29, 38, 1, uint16(arrival.X), uint16(arrival.Y), 5, 3)
|
||||
if err := s.handleCurrentPrevVillage(session, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// op1425 的恢复锚点必须落在城镇居民区(38/0),不是赛丽亚房间——否则"回到上个
|
||||
// 城镇"会把玩家塞回房间里。
|
||||
origin, ok := s.currentWeddingPublicReturnOrigin(29)
|
||||
if !ok || origin.TownID != 38 || origin.AreaID != 0 || !origin.PositionValid {
|
||||
t.Fatalf("real PVF public arrival: %+v found=%v", origin, ok)
|
||||
}
|
||||
// 目录不能对外暴露可变的落点别名。
|
||||
area, _ := table.FindArea(38, 0)
|
||||
area.SeriaRoomReturn.X = 999
|
||||
if again, ok := s.currentWeddingPublicReturnOrigin(29); !ok || again.PositionX == 999 {
|
||||
t.Fatalf("mutable catalog alias: %+v", again)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user