Files
90CNServer/server/internal/services/dnfbridge/party_transport_identity_test.go
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

153 lines
6.4 KiB
Go

package dnfbridge
import (
"encoding/binary"
"testing"
"longheng.io/server/internal/modules/dnf/alignedcmd"
"longheng.io/server/internal/modules/dnf/channelcatalog"
)
func registerPartyPeerEndpointForTest(session *gameSession, host byte, port uint16) {
if session == nil {
return
}
session.partyPeerMu.Lock()
session.partyPeer = currentPartyPeerEndpointRegistration{
InnerIPv4: [4]byte{192, 168, 5, host},
OuterIPv4: [4]byte{192, 168, 5, host},
Port: port,
MTU: currentPartyPeerDefaultPathMTU,
}
session.partyPeerMu.Unlock()
}
func TestPartyLoginPublishesUniqueNativeRoutingKey(t *testing.T) {
service := &Service{options: options{serverIP: "127.0.0.1", gameOuterToken: 0x11223344}}
channel := channelcatalog.Channel{ID: 19, Type: 1, Port: 10019}
first := &gameSession{channel: channel}
second := &gameSession{channel: channel}
firstBody := service.buildLoginSuccessForSession(first)
secondBody := service.buildLoginSuccessForSession(second)
firstKey := binary.LittleEndian.Uint32(firstBody[4:8])
secondKey := binary.LittleEndian.Uint32(secondBody[4:8])
if firstKey == 0 || secondKey == 0 || firstKey == secondKey {
t.Fatalf("login routing keys first=%#x second=%#x, want distinct nonzero keys", firstKey, secondKey)
}
if firstKey != first.partyRoutingKey || secondKey != second.partyRoutingKey {
t.Fatalf("login keys first=%#x second=%#x do not match session identities first=%#x second=%#x", firstKey, secondKey, first.partyRoutingKey, second.partyRoutingKey)
}
if firstKey == service.options.gameOuterToken || secondKey == service.options.gameOuterToken {
t.Fatalf("party routing key reused outer compatibility token %#x", service.options.gameOuterToken)
}
if first.partyPathMTU != currentPartyPeerDefaultPathMTU || second.partyPathMTU != currentPartyPeerDefaultPathMTU {
t.Fatalf("default path MTU first=%d second=%d", first.partyPathMTU, second.partyPathMTU)
}
}
func TestRuntimePartyPeerEndpointPublishesRemoteNativeIdentity(t *testing.T) {
service := &Service{options: options{gameOuterToken: 0x11223344}}
channel := channelcatalog.Channel{ID: 38, Type: 1, Port: 10038}
first := &gameSession{conn: &bufferConn{}, channel: channel, selectedCharacterID: 1001, partyRoutingKey: 111, partyPathMTU: 512}
second := &gameSession{conn: &bufferConn{}, channel: channel, selectedCharacterID: 1002, partyRoutingKey: 222, partyPathMTU: 1472}
second.partyPeer = currentPartyPeerEndpointRegistration{
InnerIPv4: [4]byte{192, 168, 5, 3}, OuterIPv4: [4]byte{192, 168, 5, 3},
Port: 60359, MTU: 1472,
}
service.bindGameSessionCharacter(first, 1001)
service.bindGameSessionCharacter(second, 1002)
state := alignedcmd.PartyState{
PartyID: 2, UserID: 1001, MaxMembers: 4,
Members: []alignedcmd.PartyMemberState{
{UserID: 1001, Slot: 0, UserState: 100},
{UserID: 1002, Slot: 1, UserState: 100},
},
}
body := service.buildRuntimePartyPeerEndpointInfoForReceiver(first, state)
if len(body) != 23 || body[0] != 1 || binary.LittleEndian.Uint16(body[1:3]) != 1002 {
t.Fatalf("remote-only op11 body=% X", body)
}
if got := binary.LittleEndian.Uint32(body[13:17]); got != second.partyRoutingKey {
t.Fatalf("op11 routing key=%#x want remote identity=%#x body=% X", got, second.partyRoutingKey, body)
}
if body[17] != 1 {
t.Fatalf("op11 NAT type=%d want direct=1 body=% X", body[17], body)
}
if got := binary.LittleEndian.Uint32(body[18:22]); got != 1472 {
t.Fatalf("op11 path MTU=%d want=1472 body=% X", got, body)
}
if body[22] != 100 {
t.Fatalf("op11 connection percent=%d want=100 body=% X", body[22], body)
}
if !service.runtimePartyNativePeerEndpointsReady(first, state) {
t.Fatal("complete native endpoint table was not admitted")
}
// The legacy relay can remain constructed as an explicit compatibility
// option, but it must not replace the same-version op2 endpoint table.
service.partyUDPRelay = &currentPartyUDPRelay{enabled: true, advertise: [4]byte{10, 0, 0, 1}}
withRelay := service.buildRuntimePartyPeerEndpointInfoForReceiver(first, state)
if string(withRelay) != string(body) {
t.Fatalf("enabled compatibility relay changed native op11 row\n got=% X\nwant=% X", withRelay, body)
}
second.partyPeer.Port = 0
if service.runtimePartyNativePeerEndpointsReady(first, state) {
t.Fatal("zero-port native endpoint table was admitted")
}
}
func TestCurrentPartyNetworkReportUpdatesNativePathMTU(t *testing.T) {
service := &Service{}
session := &gameSession{}
body := make([]byte, currentPartyNetworkReportSize)
binary.LittleEndian.PutUint32(body[0:4], 1024)
binary.LittleEndian.PutUint32(body[10:14], currentPartyNetworkVersion)
if err := service.handleCurrentPartyNetworkReport(session, body); err != nil {
t.Fatalf("handle network report: %v", err)
}
if session.partyRoutingKey == 0 || session.partyPathMTU != 1024 {
t.Fatalf("transport identity key=%d mtu=%d", session.partyRoutingKey, session.partyPathMTU)
}
binary.LittleEndian.PutUint32(body[0:4], 1500)
if err := service.handleCurrentPartyNetworkReport(session, body); err == nil {
t.Fatal("accepted non-native 1500-byte path MTU")
}
}
func TestCurrentPartyTransportTelemetryMatchesSameVersionLayouts(t *testing.T) {
peerState := []byte{1, 0x34, 0x12}
parsedPeer, err := parseCurrentPartyPeerStateReport(peerState)
if err != nil || parsedPeer.InternalStateWasZero != 1 || parsedPeer.PeerActor != 0x1234 {
t.Fatalf("peer-state parsed=%+v err=%v", parsedPeer, err)
}
holePunch := make([]byte, 23)
holePunch[0] = 2
binary.LittleEndian.PutUint32(holePunch[1:5], 3)
binary.LittleEndian.PutUint32(holePunch[5:9], 12000)
binary.LittleEndian.PutUint16(holePunch[21:23], 0x4321)
parsedHolePunch, err := parseCurrentPartyHolePunchReport(holePunch)
if err != nil || parsedHolePunch.ObservationWindowMS != 12000 || parsedHolePunch.PeerActor != 0x4321 {
t.Fatalf("hole-punch parsed=%+v err=%v", parsedHolePunch, err)
}
averagePing := make([]byte, 10)
binary.LittleEndian.PutUint16(averagePing[0:2], 3)
binary.LittleEndian.PutUint32(averagePing[2:6], 0x11223344)
binary.LittleEndian.PutUint32(averagePing[6:10], 27)
parsedPing, err := parseCurrentPartyAveragePingReport(averagePing)
if err != nil || parsedPing.Slot != 3 || parsedPing.PeerKey != 0x11223344 || parsedPing.AveragePingMS != 27 {
t.Fatalf("average-ping parsed=%+v err=%v", parsedPing, err)
}
averagePing[0] = 8
averagePing[1] = 0
if _, err := parseCurrentPartyAveragePingReport(averagePing); err == nil {
t.Fatal("accepted out-of-range party slot")
}
}