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,250 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/channelcatalog"
|
||||
"longheng.io/server/internal/modules/dnf/dnfenum"
|
||||
dnfproto "longheng.io/server/internal/modules/dnf/protocol"
|
||||
"longheng.io/server/internal/modules/dnf/raid"
|
||||
)
|
||||
|
||||
func TestRaidCreateAfterCharacterSelectionUsesBoundOriginalAccountCharacter(t *testing.T) {
|
||||
service, session, _ := newTownMoveTest(t)
|
||||
service.options.accountID = defaultAccountPrefix + "1"
|
||||
channel := channelcatalog.Channel{ServerID: 1, ID: 23, Type: 23, Name: "anton", Port: 10023}
|
||||
session.channel = channel
|
||||
session.residentChannel = channel
|
||||
session.selectedCharacterID = 0
|
||||
session.townSceneReadyCharacterID = 0
|
||||
connection := session.conn.(*bufferConn)
|
||||
connection.write.Reset()
|
||||
|
||||
selectRequest := make([]byte, 16)
|
||||
binary.LittleEndian.PutUint32(selectRequest[:4], 0)
|
||||
if err := service.sendUpperCSharpSelectInit(session, selectRequest); err != nil {
|
||||
t.Fatalf("select character: %v", err)
|
||||
}
|
||||
if session.selectedCharacterID != 29 {
|
||||
t.Fatalf("selected character = %d, want 29", session.selectedCharacterID)
|
||||
}
|
||||
if bound, ok := service.onlineGameSession(29); !ok || bound != session {
|
||||
t.Fatalf("selected character 29 was not bound to the game session")
|
||||
}
|
||||
|
||||
connection.write.Reset()
|
||||
handled, err := service.handleAlignedGameCommand(
|
||||
session,
|
||||
byte(dnfenum.GameCmdCommand),
|
||||
uint16(dnfenum.CmdPacketCreateRaid),
|
||||
buildRaidInfoRequestForBridgeTest(1, []byte("anton"), 5),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("create raid: %v", err)
|
||||
}
|
||||
if !handled {
|
||||
t.Fatal("create raid was not handled")
|
||||
}
|
||||
|
||||
wantRaidKey := uint32(23)<<16 | 29
|
||||
state := service.raids[wantRaidKey]
|
||||
if state == nil || state.LeaderID != 29 || len(state.Members) != 1 || state.Members[0].CharID != 29 {
|
||||
t.Fatalf("runtime raid state = %+v, want character 29 as the sole leader", state)
|
||||
}
|
||||
|
||||
object, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
|
||||
wantObjectBody, err := raid.BuildRaidObjectMode0Body(runtimeRaidObjectMode0(*state))
|
||||
if err != nil {
|
||||
t.Fatalf("build expected raid object: %v", err)
|
||||
}
|
||||
if object.Header.Classification != 0 ||
|
||||
object.Header.MsgID != raid.RaidObjectMsgID ||
|
||||
!bytes.Equal(object.Body, wantObjectBody) {
|
||||
t.Fatalf("raid object = class %d msg %#x body=% X", object.Header.Classification, object.Header.MsgID, object.Body)
|
||||
}
|
||||
|
||||
buff, rest := splitGameServerUpperPacket(t, rest)
|
||||
wantBuffBody := mustRuntimeRaidBuffRegistrationSnapshotBody(t, *state)
|
||||
if buff.Header.Classification != 0 ||
|
||||
buff.Header.MsgID != raid.RaidBuffMsgID ||
|
||||
!bytes.Equal(buff.Body, wantBuffBody) {
|
||||
t.Fatalf("raid buff = class %d msg %#x body=% X", buff.Header.Classification, buff.Header.MsgID, buff.Body)
|
||||
}
|
||||
|
||||
ack, rest := splitGameServerUpperPacket(t, rest)
|
||||
if ack.Header.Classification != dnfproto.DefaultChannelClassification ||
|
||||
ack.Header.MsgID != uint16(dnfenum.CmdPacketCreateRaid) ||
|
||||
!bytes.Equal(ack.Body, raid.BuildCreateRaidResultBody(wantRaidKey)) {
|
||||
t.Fatalf("raid create ACK = class %d msg %#x body=% X", ack.Header.Classification, ack.Header.MsgID, ack.Body)
|
||||
}
|
||||
refresh, rest := splitGameServerUpperPacket(t, rest)
|
||||
wantRefreshBody, err := raid.BuildMemberRefreshMode3Body(runtimeRaidMemberRefresh(*state))
|
||||
if err != nil {
|
||||
t.Fatalf("build expected raid member refresh: %v", err)
|
||||
}
|
||||
if refresh.Header.Classification != 0 ||
|
||||
refresh.Header.MsgID != raid.RaidMemberRefreshMsgID ||
|
||||
!bytes.Equal(refresh.Body, wantRefreshBody) {
|
||||
t.Fatalf("raid member refresh = class %d msg %#x body=% X", refresh.Header.Classification, refresh.Header.MsgID, refresh.Body)
|
||||
}
|
||||
if len(rest) != 0 {
|
||||
t.Fatalf("raid create trailing bytes = %d", len(rest))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRaidCreateWithoutCharacterBoundForConfiguredAccountReturnsReason3(t *testing.T) {
|
||||
service, session, _ := newTownMoveTest(t)
|
||||
service.options.accountID = "dnf:missing-account"
|
||||
channel := channelcatalog.Channel{ServerID: 1, ID: 23, Type: 23, Name: "anton", Port: 10023}
|
||||
session.channel = channel
|
||||
session.residentChannel = channel
|
||||
session.selectedCharacterID = 0
|
||||
connection := session.conn.(*bufferConn)
|
||||
connection.write.Reset()
|
||||
|
||||
selectRequest := make([]byte, 16)
|
||||
if err := service.sendUpperCSharpSelectInit(session, selectRequest); err != nil {
|
||||
t.Fatalf("select character with mismatched account: %v", err)
|
||||
}
|
||||
if session.selectedCharacterID != 0 {
|
||||
t.Fatalf("mismatched account selected character = %d, want 0", session.selectedCharacterID)
|
||||
}
|
||||
if connection.write.Len() != 0 {
|
||||
t.Fatalf("mismatched account wrote %d select bytes, want 0", connection.write.Len())
|
||||
}
|
||||
|
||||
handled, err := service.handleAlignedGameCommand(
|
||||
session,
|
||||
byte(dnfenum.GameCmdCommand),
|
||||
uint16(dnfenum.CmdPacketCreateRaid),
|
||||
buildRaidInfoRequestForBridgeTest(1, []byte("anton"), 5),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("create raid without bound character: %v", err)
|
||||
}
|
||||
if !handled {
|
||||
t.Fatal("create raid without bound character was not handled")
|
||||
}
|
||||
|
||||
ack, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
|
||||
if ack.Header.Classification != dnfproto.DefaultChannelClassification ||
|
||||
ack.Header.MsgID != uint16(dnfenum.CmdPacketCreateRaid) ||
|
||||
!bytes.Equal(ack.Body, []byte{0, runtimeRaidFailureCode}) {
|
||||
t.Fatalf("unbound raid create ACK = class %d msg %#x body=% X", ack.Header.Classification, ack.Header.MsgID, ack.Body)
|
||||
}
|
||||
if len(rest) != 0 {
|
||||
t.Fatalf("unbound raid create trailing bytes = %d", len(rest))
|
||||
}
|
||||
if len(service.raids) != 0 {
|
||||
t.Fatalf("unbound raid create produced %d runtime raids", len(service.raids))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRaidCreateModeMismatchRecoversExistingUnstartedSingleRaid(t *testing.T) {
|
||||
const (
|
||||
characterID = uint16(1001)
|
||||
raidKey = uint32(0x00170001)
|
||||
)
|
||||
service := &Service{
|
||||
options: options{gameUpperBodyCodec: gameUpperBodyCodecPlain},
|
||||
raids: make(map[uint32]*runtimeRaidState),
|
||||
}
|
||||
connection := &bufferConn{}
|
||||
session := &gameSession{
|
||||
conn: connection,
|
||||
connID: "raid-create-recover-existing",
|
||||
channel: channelcatalog.Channel{ServerID: 1, ID: 23, Type: dnfenum.RaidChannelType, Name: "anton", Port: 10023},
|
||||
}
|
||||
service.bindGameSessionCharacter(session, characterID)
|
||||
existing := &runtimeRaidState{
|
||||
RaidKey: raidKey,
|
||||
LeaderID: characterID,
|
||||
RouteOrRaidType: 0,
|
||||
NameBytes: []byte("fff"),
|
||||
TailFlag: 1,
|
||||
SingleRaidType: 0,
|
||||
Members: []runtimeRaidMemberState{{
|
||||
CharID: characterID, Name: "leader", GroupIndex: 1, SlotOrder: 0,
|
||||
UserState: 1, HPPercent: 100, MPPercent: 100,
|
||||
}},
|
||||
Lifecycle: runtimeRaidLifecycleState{Status: runtimeRaidLifecycleCreated, Revision: 1},
|
||||
}
|
||||
service.raids[raidKey] = existing
|
||||
|
||||
err := service.handleOnlineCreateRaid(
|
||||
session,
|
||||
uint16(dnfenum.CmdPacketCreateRaid),
|
||||
buildRaidInfoRequestForBridgeTest(1, []byte("different-normal-name"), 0),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("recover existing raid on mismatched create: %v", err)
|
||||
}
|
||||
if len(service.raids) != 1 {
|
||||
t.Fatalf("mismatched create changed raid count: got=%d want=1", len(service.raids))
|
||||
}
|
||||
current := service.raids[raidKey]
|
||||
if current == nil || current.TailFlag != 1 || current.RouteOrRaidType != 0 || !bytes.Equal(current.NameBytes, []byte("fff")) {
|
||||
t.Fatalf("mismatched create mutated existing single raid: %+v", current)
|
||||
}
|
||||
|
||||
object, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
|
||||
wantObjectBody, err := raid.BuildRaidObjectMode0Body(runtimeRaidObjectMode0(*existing))
|
||||
if err != nil {
|
||||
t.Fatalf("build existing raid object: %v", err)
|
||||
}
|
||||
if object.Header.Classification != 0 || object.Header.MsgID != raid.RaidObjectMsgID || !bytes.Equal(object.Body, wantObjectBody) {
|
||||
t.Fatalf("recovered raid object = class %d msg %#x body=% X", object.Header.Classification, object.Header.MsgID, object.Body)
|
||||
}
|
||||
buff, rest := splitGameServerUpperPacket(t, rest)
|
||||
wantBuffBody := mustRuntimeRaidBuffRegistrationSnapshotBody(t, *existing)
|
||||
if buff.Header.Classification != 0 || buff.Header.MsgID != raid.RaidBuffMsgID || !bytes.Equal(buff.Body, wantBuffBody) {
|
||||
t.Fatalf("recovered raid buff = class %d msg %#x body=% X", buff.Header.Classification, buff.Header.MsgID, buff.Body)
|
||||
}
|
||||
ack, rest := splitGameServerUpperPacket(t, rest)
|
||||
if ack.Header.Classification != dnfproto.DefaultChannelClassification ||
|
||||
ack.Header.MsgID != uint16(dnfenum.CmdPacketCreateRaid) ||
|
||||
!bytes.Equal(ack.Body, raid.BuildCreateRaidResultBody(raidKey)) {
|
||||
t.Fatalf("recovered raid ACK = class %d msg %#x body=% X", ack.Header.Classification, ack.Header.MsgID, ack.Body)
|
||||
}
|
||||
refresh, rest := splitGameServerUpperPacket(t, rest)
|
||||
wantRefreshBody, err := raid.BuildMemberRefreshMode3Body(runtimeRaidMemberRefresh(*existing))
|
||||
if err != nil {
|
||||
t.Fatalf("build existing raid refresh: %v", err)
|
||||
}
|
||||
if refresh.Header.Classification != 0 || refresh.Header.MsgID != raid.RaidMemberRefreshMsgID || !bytes.Equal(refresh.Body, wantRefreshBody) {
|
||||
t.Fatalf("recovered raid refresh = class %d msg %#x body=% X", refresh.Header.Classification, refresh.Header.MsgID, refresh.Body)
|
||||
}
|
||||
if len(rest) != 0 {
|
||||
t.Fatalf("recovered raid trailing bytes = %d", len(rest))
|
||||
}
|
||||
if party := runtimePartyStateSnapshot(session); party.PartyID > 0 || len(party.Members) != 0 {
|
||||
t.Fatalf("unstarted single recovery created an ordinary party: %+v", party)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRaidCreateDoesNotRecoverAnUnstartedRaidOwnedByAnotherRaidChannel(t *testing.T) {
|
||||
const (
|
||||
characterID = uint16(1001)
|
||||
raidKey = uint32(0x00170001)
|
||||
)
|
||||
service := &Service{raids: map[uint32]*runtimeRaidState{
|
||||
raidKey: {
|
||||
RaidKey: raidKey, LeaderID: characterID, RaidMode: runtimeRaidOwnerAnton,
|
||||
TailFlag: 1, NameBytes: []byte("single"),
|
||||
Members: []runtimeRaidMemberState{{CharID: characterID}},
|
||||
},
|
||||
}}
|
||||
session := &gameSession{channel: channelcatalog.Channel{Type: dnfenum.HiddenRaidType}}
|
||||
service.bindGameSessionCharacter(session, characterID)
|
||||
|
||||
if state, created := service.createRuntimeRaid(session, characterID, raid.InfoRequest{
|
||||
NameBytes: []byte("single"), TailFlag: 1,
|
||||
}); created || state.RaidKey != 0 {
|
||||
t.Fatalf("Luke channel recovered Anton state: created=%t state=%+v", created, state)
|
||||
}
|
||||
if got := service.raids[raidKey]; got == nil || got.RaidMode != runtimeRaidOwnerAnton {
|
||||
t.Fatalf("cross-owner create mutated existing state: %+v", got)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user