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,153 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/alignedcmd"
|
||||
"longheng.io/server/internal/modules/dnf/dnfenum"
|
||||
)
|
||||
|
||||
// The dungeon-selection page deliberately removes its opener from the TOWN
|
||||
// roster, and that decision is about town visibility only. The same retirement
|
||||
// also silenced the party-scoped actor fan-out, because
|
||||
// broadcastCurrentPartyActorProjection resolves peers through the roster - so the
|
||||
// opening player's teammates never received his scene actor and he never received
|
||||
// theirs. That is the client-side 队友不在附近 refusal the operator reports after a
|
||||
// return ("出副本有时候再进会提示队友不在附近"), and the server-side twin of that
|
||||
// check (game-party-dungeon-entry-copresence-refused) never fires for it.
|
||||
func TestSelectorPagePartyCoPresenceProjectsBothDirectionsForRetiredOwner(t *testing.T) {
|
||||
service, leader, follower := prepareCurrentPartySelectorEntryTest(t)
|
||||
leaderConn := leader.conn.(*bufferConn)
|
||||
followerConn := follower.conn.(*bufferConn)
|
||||
|
||||
leaderPresence, resolved := service.currentTownActorAreaDetachPresence(leader)
|
||||
if !resolved {
|
||||
t.Fatal("fixture leader has no town anchor")
|
||||
}
|
||||
// The completed-run route: the run is retired into the other-dungeon selector
|
||||
// page and the character is off the town roster, so its anchor is the memo.
|
||||
service.onlinePlayers.LeaveAreaSession(selectedCharacterID(leader), leader)
|
||||
if _, registered := service.onlinePlayers.PlayerForCharacter(selectedCharacterID(leader)); registered {
|
||||
t.Fatal("selector opener stayed on the town roster")
|
||||
}
|
||||
if !service.bindCompletedRunSelectorPresence(leader, currentTownPositionSnapshot{
|
||||
CharacterID: leaderPresence.CharacterID,
|
||||
TownID: leaderPresence.TownID,
|
||||
AreaID: leaderPresence.AreaID,
|
||||
PositionX: leaderPresence.PositionX,
|
||||
PositionY: leaderPresence.PositionY,
|
||||
MovementCode: leaderPresence.Direction,
|
||||
PositionValid: true,
|
||||
}, "test_completed_run") {
|
||||
t.Fatal("completed-run selector presence was not bound")
|
||||
}
|
||||
leaderConn.write.Reset()
|
||||
followerConn.write.Reset()
|
||||
|
||||
if projected := service.rebuildRuntimePartySelectorPageCoPresence(leader, "test_selector_page"); projected != 1 {
|
||||
t.Fatalf("selector co-presence projected=%d want=1", projected)
|
||||
}
|
||||
// The opener renders the teammate...
|
||||
assertSelectorPartyActorProjection(t, "selector opener", leaderConn.write.Bytes(), service, leader)
|
||||
// ...and the teammate renders the opener, who is no longer on the town roster.
|
||||
assertSelectorPartyActorProjection(t, "party member", followerConn.write.Bytes(), service, follower)
|
||||
if _, registered := service.onlinePlayers.PlayerForCharacter(selectedCharacterID(leader)); registered {
|
||||
t.Fatal("selector co-presence re-registered the opener in town")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectorPagePartyCoPresenceStaysQuietWithoutAPeerInTheSameArea(t *testing.T) {
|
||||
service, leader, follower := prepareCurrentPartySelectorEntryTest(t)
|
||||
leaderConn := leader.conn.(*bufferConn)
|
||||
followerConn := follower.conn.(*bufferConn)
|
||||
|
||||
// A solo party opening the page is a high-frequency action: it must not write a
|
||||
// packet or a log line.
|
||||
storeRuntimePartyState(leader, alignedcmd.PartyState{
|
||||
PartyID: 99, UserID: 99,
|
||||
Members: []alignedcmd.PartyMemberState{{UserID: 99, UserState: 1, HPPercent: 100, MPPercent: 100}},
|
||||
})
|
||||
service.leaveTownPresenceForDungeonSelector(leader, "test_selector_open_solo")
|
||||
leaderConn.write.Reset()
|
||||
followerConn.write.Reset()
|
||||
if projected := service.rebuildRuntimePartySelectorPageCoPresence(leader, "test_solo"); projected != 0 {
|
||||
t.Fatalf("solo selector page projected=%d want=0", projected)
|
||||
}
|
||||
if leaderConn.write.Len() != 0 || followerConn.write.Len() != 0 {
|
||||
t.Fatalf("solo selector page wrote packets: leader=%x member=%x",
|
||||
leaderConn.write.Bytes(), followerConn.write.Bytes())
|
||||
}
|
||||
|
||||
// A member standing in another area is genuinely not nearby. The projection must
|
||||
// not paper over that: the client's refusal is correct there, and the server's
|
||||
// entry preflight refuses it too.
|
||||
storeRuntimePartyState(leader, alignedcmd.PartyState{
|
||||
PartyID: 99, UserID: 99,
|
||||
Members: []alignedcmd.PartyMemberState{
|
||||
{UserID: 99, UserState: 1, HPPercent: 100, MPPercent: 100},
|
||||
{UserID: 100, UserState: 1, HPPercent: 100, MPPercent: 100},
|
||||
},
|
||||
})
|
||||
service.onlinePlayers.LeaveAreaSession(100, follower)
|
||||
service.onlinePlayers.EnterArea(&onlinePlayerInfo{
|
||||
CharacterID: 100, TownID: 38, AreaID: 4, Session: follower,
|
||||
})
|
||||
leaderConn.write.Reset()
|
||||
followerConn.write.Reset()
|
||||
if projected := service.rebuildRuntimePartySelectorPageCoPresence(leader, "test_split_areas"); projected != 0 {
|
||||
t.Fatalf("split-area selector page projected=%d want=0", projected)
|
||||
}
|
||||
if leaderConn.write.Len() != 0 || followerConn.write.Len() != 0 {
|
||||
t.Fatalf("split-area selector page wrote packets: leader=%x member=%x",
|
||||
leaderConn.write.Bytes(), followerConn.write.Bytes())
|
||||
}
|
||||
}
|
||||
|
||||
// assertSelectorPartyActorProjection checks one receiver's stream for the
|
||||
// party-scoped co-presence shape: remote mode0, the party display record and the
|
||||
// remote area notification -- and, just as importantly, no op11 and no mode1.
|
||||
//
|
||||
// op11 is a formation handshake: replaying it makes the client tear a healthy peer
|
||||
// link down and re-enter the connecting state, which is why a scene reconstruction
|
||||
// uses op9 + op153 only. mode1 is the receiver's own stat/equipment snapshot: it
|
||||
// has no consumer for a remote peer, the client applies it to the actor it
|
||||
// controls, and it is the expensive half of the projection (PVF attribute
|
||||
// recompute per body) -- see projectSelectorPartyActorPair.
|
||||
func assertSelectorPartyActorProjection(
|
||||
t *testing.T,
|
||||
label string,
|
||||
stream []byte,
|
||||
service *Service,
|
||||
receiver *gameSession,
|
||||
) {
|
||||
t.Helper()
|
||||
if len(stream) == 0 {
|
||||
t.Fatalf("%s received no selector co-presence packets", label)
|
||||
}
|
||||
packets := splitAllCurrentGameServerUpperPackets(t, stream)
|
||||
var mode0, mode1, roster, area bool
|
||||
for _, packet := range packets {
|
||||
if packet.Header.Classification != 0 {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case packet.Header.MsgID == 0x000b:
|
||||
t.Fatalf("%s received a formation op11 replay", label)
|
||||
case packet.Header.MsgID == uint16(dnfenum.CmdPacketSetUDPIPPort) && len(packet.Body) > 0 && packet.Body[0] == 0:
|
||||
mode0 = true
|
||||
case packet.Header.MsgID == uint16(dnfenum.CmdPacketSetUDPIPPort) && len(packet.Body) > 0 && packet.Body[0] == 1:
|
||||
mode1 = true
|
||||
case packet.Header.MsgID == uint16(dnfenum.CmdPacketRecoverStamina):
|
||||
roster = true
|
||||
case packet.Header.MsgID == currentTownUserAreaNotificationMsgID:
|
||||
area = true
|
||||
}
|
||||
}
|
||||
if !mode0 || !roster || !area {
|
||||
t.Fatalf("%s selector co-presence mode0=%t roster=%t area=%t count=%d char=%d",
|
||||
label, mode0, roster, area, len(packets), selectedCharacterID(receiver))
|
||||
}
|
||||
if mode1 {
|
||||
t.Fatalf("%s selector co-presence carried a mode1 binding", label)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user