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

131 lines
5.4 KiB
Go

package dnfbridge
import (
"bytes"
"context"
"encoding/binary"
"testing"
"longheng.io/server/internal/modules/dnf/alignedcmd"
"longheng.io/server/internal/modules/dnf/dnfenum"
dnfpvp "longheng.io/server/internal/modules/dnf/pvp"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
"longheng.io/server/internal/modules/dnf/skillcmd"
)
func TestPVPDojoNativeSkillSnapshot(t *testing.T) {
record := dnfrepo.SkillRecord{Skills: map[int64]dnfrepo.SkillState{46: {Level: 7}}, Points: dnfrepo.SkillPointState{RemainingSP: 105, RemainingTP: 9},
Trees: map[int]dnfrepo.SkillTreeState{1: {Skills: map[int64]dnfrepo.SkillState{47: {Level: 3}}, Points: dnfrepo.SkillPointState{RemainingSP: 93, RemainingTP: 6}}},
Layouts: map[int]dnfrepo.SkillLayout{0: {198: 46}, 1: {150: 47}}}
body, err := buildCurrentPVPDojoSkills(record)
if err != nil || len(body) != 18 || body[0] != 1 || body[1] != 198 || binary.LittleEndian.Uint16(body[2:]) != 46 || body[4] != 7 || body[5] != 1 || body[6] != 150 || binary.LittleEndian.Uint16(body[7:]) != 47 || body[9] != 3 {
t.Fatalf("dojo snapshot=%x %v", body, err)
}
for i, want := range []uint16{105, 93, 9, 6} {
if got := binary.LittleEndian.Uint16(body[10+2*i:]); got != want {
t.Fatal("dojo SP/TP order incorrect")
}
}
record.Points.RemainingSP = 65536
if _, err := buildCurrentPVPDojoSkills(record); err == nil {
t.Fatal("overflow accepted")
}
}
func TestCurrentPVPDojoSkillModeRestoreAndPersistence(t *testing.T) {
s := pvpRoomTestService(t)
session := &gameSession{conn: &bufferConn{}, accountID: "pvp-29", selectedCharacterID: 29, channel: s.catalog.Channels()[0]}
check := func(want byte) {
t.Helper()
p, rest := splitCurrentGameServerUpperPacketAuto(t, session.conn.(*bufferConn).write.Bytes())
if p.Header.MsgID != 1229 || p.Header.Classification != 0 || len(p.Body) != 1 || p.Body[0] != want || len(rest) != 0 {
t.Fatalf("native dojo state=%+v", p)
}
session.conn.(*bufferConn).write.Reset()
}
if err := s.sendCurrentPVPDojoSkillMode(session); err != nil {
t.Fatal(err)
}
check(0)
for _, mode := range []byte{1, 2} {
if err := s.handleCurrentPVPDojoSkillMode(session, []byte{mode}); err != nil {
t.Fatal(err)
}
check(mode)
if err := s.sendCurrentPVPDojoSkillMode(session); err != nil {
t.Fatal(err)
}
check(mode)
}
for _, body := range [][]byte{nil, {0}, {3}, {1, 2}} {
if err := s.handleCurrentPVPDojoSkillMode(session, body); err != nil || session.conn.(*bufferConn).write.Len() != 0 {
t.Fatal("malformed dojo mode accepted")
}
}
session.accountID = "other"
if err := s.handleCurrentPVPDojoSkillMode(session, []byte{1}); err == nil {
t.Fatal("unowned character changed")
}
repos, _ := s.repositoryGroup()
character, _, _ := repos.Character.Load(context.Background(), "29")
if dnfpvp.DojoSkillMode(character) != 2 {
t.Fatal("invalid request changed saved mode")
}
session.accountID, session.selectedCharacterID = "pvp-30", 30
if err := s.sendCurrentPVPDojoSkillMode(session); err != nil {
t.Fatal(err)
}
check(0)
}
func TestPVPDojoResetRefreshUsesNativeManagerAfterAck(t *testing.T) {
s := pvpRoomTestService(t)
repos, _ := s.repositoryGroup()
ctx := context.Background()
record, _, err := repos.Skill.Load(ctx, "29")
if err != nil {
t.Fatal(err)
}
record.Layouts = map[int]dnfrepo.SkillLayout{0: {0: 46}, 1: {150: 46}}
if err := repos.Skill.Save(ctx, record); err != nil {
t.Fatal(err)
}
if _, err := skillcmd.PrepareDojoSkills(ctx, repos, "29"); err != nil {
t.Fatal(err)
}
record, _, err = repos.Skill.Load(ctx, "29")
if err != nil {
t.Fatal(err)
}
dnfrepo.SetSkillTree(&record, 2, dnfrepo.SkillTreeState{
Skills: map[int64]dnfrepo.SkillState{47: {Level: 3, Enabled: true}},
Points: dnfrepo.SkillPointState{RemainingSP: 93, RemainingTP: 6},
})
record.Layouts[2] = dnfrepo.SkillLayout{0: 47}
if err := repos.Skill.Save(ctx, record); err != nil {
t.Fatal(err)
}
session := &gameSession{conn: &bufferConn{}, accountID: "pvp-29", selectedCharacterID: 29,
townPositionSnapshot: currentTownPositionSnapshot{CharacterID: 29, PositionValid: true, TownID: 10, AreaID: 4}}
result := alignedcmd.Result{Operation: "skill_init", UpperResponses: []alignedcmd.UpperResponse{{
MsgID: uint16(dnfenum.CmdPacketSkillInit), Classification: 1, Body: []byte{1, 0, 3},
}}, PostActions: []alignedcmd.PostAction{alignedcmd.PostActionRefreshSelectedActorSkills}}
if err := s.sendAlignedUpperResponses(session, result); err != nil {
t.Fatal(err)
}
ack, wire := splitCurrentGameServerUpperPacketAuto(t, session.conn.(*bufferConn).write.Bytes())
refresh, wire := splitCurrentGameServerUpperPacketAuto(t, wire)
want, _, err := buildCurrentSceneSkillInfoBody(skillcmd.DojoSkillProjection(record), skillcmd.DojoSkillProjection(record).Layouts, 0)
if err != nil || ack.Header.MsgID != uint16(dnfenum.CmdPacketSkillInit) || ack.Header.Classification != 1 ||
refresh.Header.MsgID != 1231 || refresh.Header.Classification != 0 || !bytes.Equal(refresh.Body, want) || len(wire) != 0 {
t.Fatalf("dojo reset route ack=%+v refresh=%+v error=%v", ack.Header, refresh.Header, err)
}
if slots := currentSkillInfoFirstTreeSlots(t, refresh.Body); slots[0] != 47 {
t.Fatalf("dojo refresh loaded ordinary skills: %v", slots)
}
session.townPositionSnapshot.AreaID = 1
if active, err := s.currentPVPDojoSkillsActive(session); err != nil || active {
t.Fatalf("dojo mode leaked outside area4: %v %v", active, err)
}
}