服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
131 lines
5.4 KiB
Go
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)
|
|
}
|
|
}
|