Files
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

203 lines
5.8 KiB
Go

package dnfbridge
import (
"bytes"
"context"
"encoding/hex"
"testing"
dnfenum "longheng.io/server/internal/modules/dnf/dnfenum"
)
func TestParseCurrentSoloTeleportRequestLiveSamples(t *testing.T) {
tests := []struct {
hex string
x, y int16
}{
{hex: "ffffffffffffffff28049d014b0105", x: 413, y: 331},
{hex: "ffffffffffffffff28040c010f0105", x: 268, y: 271},
{hex: "ffffffffffffffff28042101390105", x: 289, y: 313},
{hex: "ffffffffffffffff2804c701510105", x: 455, y: 337},
{hex: "ffffffffffffffff2804c7013f0105", x: 455, y: 319},
}
for _, test := range tests {
body, err := hex.DecodeString(test.hex)
if err != nil {
t.Fatal(err)
}
request, err := parseCurrentSoloTeleportRequest(body)
if err != nil {
t.Fatal(err)
}
if request.OpaqueI32A != -1 || request.OpaqueI32B != -1 ||
request.TownID != 40 || request.AreaID != 4 ||
request.PositionX != test.x || request.PositionY != test.y ||
request.Direction != 5 {
t.Fatalf("sample %s parsed=%+v", test.hex, request)
}
}
}
func TestParseCurrentSoloTeleportRequestRejectsWrongBoundary(t *testing.T) {
for _, size := range []int{
0,
currentSoloTeleportRequestWireSize - 1,
currentSoloTeleportRequestWireSize + 1,
} {
if _, err := parseCurrentSoloTeleportRequest(make([]byte, size)); err == nil {
t.Fatalf("size %d unexpectedly accepted", size)
}
}
}
func TestHandleCurrentSoloTeleportCommitsThroughOp24WithoutOp470Ack(t *testing.T) {
service, session, repositories := newTownMoveTest(t)
body, err := hex.DecodeString("ffffffffffffffff26008403fa0005")
if err != nil {
t.Fatal(err)
}
if err := service.handleCurrentSoloTeleport(session, body); err != nil {
t.Fatal(err)
}
if session.conn.(*bufferConn).write.Len() == 0 {
stored, found, loadErr := repositories.Character.Load(context.Background(), "29")
t.Fatalf("solo teleport wrote no transition; found=%t err=%v stats=%+v", found, loadErr, stored.Stats)
}
packet, rest := splitTownTransitionAndPostState(
t,
session.conn.(*bufferConn).write.Bytes(),
session,
29,
38,
0,
900,
250,
5,
3,
townMoveSkillProjectionBody(t, repositories, "29"),
false,
)
if packet.Header.MsgID != currentSceneTransitionMsgID ||
packet.Header.Classification != 0 {
t.Fatalf(
"solo teleport response header=%+v body=%x trailing=%x",
packet.Header,
packet.Body,
rest,
)
}
if packet.Header.MsgID == uint16(dnfenum.CmdPacketSoloTelepoart) {
t.Fatalf("solo teleport emitted failure-callback op470 body=%x", packet.Body)
}
// The committed move now arms the client's own five-minute countdown, so the
// transition is followed by exactly one S2C 378.
cooldown, trailing := splitCurrentGameServerUpperPacketAuto(t, rest)
if cooldown.Header.MsgID != currentSoloTeleportCooldownOpcode ||
cooldown.Header.Classification != 0 ||
!bytes.Equal(cooldown.Body, buildCurrentSoloTeleportCooldownBody(
currentSoloTeleportCooldownDefaultIndices,
currentSoloTeleportCooldownSeconds)) ||
len(trailing) != 0 {
t.Fatalf(
"solo teleport cooldown header=%+v body=%x trailing=%x",
cooldown.Header,
cooldown.Body,
trailing,
)
}
stored, found, err := repositories.Character.Load(context.Background(), "29")
if err != nil || !found ||
stored.Stats["town_id"] != 38 ||
stored.Stats["area_id"] != 0 ||
stored.Stats["pos_x"] != 900 ||
stored.Stats["pos_y"] != 250 ||
stored.Stats["direction"] != 5 {
t.Fatalf("solo teleport location found=%t err=%v stats=%+v", found, err, stored.Stats)
}
}
func TestHandleCurrentSoloTeleportUsesPVFTransportRouteForCrossTownTarget(t *testing.T) {
service, session, repositories := newTownMoveTest(t)
body, err := hex.DecodeString("ffffffffffffffff27008403fa0005")
if err != nil {
t.Fatal(err)
}
if err := service.handleCurrentSoloTeleport(session, body); err != nil {
t.Fatal(err)
}
if session.conn.(*bufferConn).write.Len() == 0 {
stored, found, loadErr := repositories.Character.Load(context.Background(), "29")
t.Fatalf("cross-town solo teleport wrote no transition; found=%t err=%v stats=%+v", found, loadErr, stored.Stats)
}
packet, rest := splitTownTransitionAndPostState(
t,
session.conn.(*bufferConn).write.Bytes(),
session,
29,
39,
0,
900,
250,
5,
3,
townMoveSkillProjectionBody(t, repositories, "29"),
false,
)
if packet.Header.MsgID != currentSceneTransitionMsgID ||
packet.Header.Classification != 0 {
t.Fatalf(
"cross-town solo teleport response header=%+v body=%x trailing=%x",
packet.Header,
packet.Body,
rest,
)
}
cooldown, trailing := splitCurrentGameServerUpperPacketAuto(t, rest)
if cooldown.Header.MsgID != currentSoloTeleportCooldownOpcode ||
cooldown.Header.Classification != 0 ||
len(trailing) != 0 {
t.Fatalf(
"cross-town solo teleport cooldown header=%+v body=%x trailing=%x",
cooldown.Header,
cooldown.Body,
trailing,
)
}
stored, found, err := repositories.Character.Load(context.Background(), "29")
if err != nil || !found ||
stored.Stats["town_id"] != 39 ||
stored.Stats["area_id"] != 0 ||
stored.Stats["pos_x"] != 900 ||
stored.Stats["pos_y"] != 250 {
t.Fatalf(
"cross-town solo teleport location found=%t err=%v stats=%+v",
found,
err,
stored.Stats,
)
}
}
func TestBuildCurrentSoloTeleportMoveBodyMarksTransportSource(t *testing.T) {
request := currentSoloTeleportRequest{
TownID: 40,
AreaID: 4,
PositionX: 330,
PositionY: 264,
Direction: 5,
}
body := buildCurrentSoloTeleportMoveBody(request, 38)
parsed, err := parseCurrentTownSetUserAreaRequest(body)
if err != nil {
t.Fatal(err)
}
if parsed.TownID != 40 || parsed.AreaID != 4 ||
parsed.PositionX != 330 || parsed.PositionY != 264 ||
parsed.Direction != 5 || parsed.OpaqueU16A != 38 ||
parsed.OpaqueU16B != 0 || parsed.OpaqueU32 != 0 ||
parsed.OpaqueTailU8 != 5 || !parsed.LooksLikeTeleportArraySelection() {
t.Fatalf("solo teleport move body parsed=%+v", parsed)
}
}