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,113 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/channelcatalog"
|
||||
)
|
||||
|
||||
// 现场 2026-09-26(raid 1507329):名册里那一位(wire field 66)过去填的是"你是不是团长"
|
||||
// (state.LeaderID),所以团长自己单独回城、队长顺延给队友之后,客户端仍然把团长画成队长:
|
||||
// 被顺延的人没有队长标志、战况表没有按钮,回城的人两样都还在(packet_log 15:03:14.782
|
||||
// 解出来的名册:char 1 group 0 LEADER=1;char 20 group 1 slot 1 LEADER=0)。
|
||||
//
|
||||
// 客户端侧证据(tools/ida_work/batch3.txt + 本机反汇编):sub_17B6EF0 把该 u32 读成
|
||||
// `value == 1` 的布尔存到记录 +0x42;客户端问"我是不是队长"就是拿自己的记录读这一位
|
||||
// (0x51b2748 → 0x593cf0 取自己的 key → 0x13b29c0 取自己的记录 → cmp byte ptr [rec+42h],0)。
|
||||
func TestRuntimeRaidMemberLeadsSquadRule(t *testing.T) {
|
||||
started := func(members []runtimeRaidMemberState, leaders map[byte]uint16) runtimeRaidState {
|
||||
return runtimeRaidState{LeaderID: 99, Started: true, Members: members, SquadPartyLeaders: leaders}
|
||||
}
|
||||
members := []runtimeRaidMemberState{
|
||||
{CharID: 20, GroupIndex: 1, SlotOrder: 1},
|
||||
{CharID: 22, GroupIndex: 1, SlotOrder: 3},
|
||||
{CharID: 30, GroupIndex: 2, SlotOrder: 2},
|
||||
{CharID: 99, GroupIndex: 0, SlotOrder: 0},
|
||||
}
|
||||
flag := func(state runtimeRaidState, charID uint16) bool {
|
||||
index := runtimeRaidMemberIndex(state, charID)
|
||||
if index < 0 {
|
||||
t.Fatalf("char %d missing", charID)
|
||||
}
|
||||
return runtimeRaidMemberLeadsSquad(state, state.Members[index])
|
||||
}
|
||||
|
||||
// 开团前:没有小队、也没有普通队可领,"是团长"就是旧行为,一个字不改。
|
||||
beforeStart := runtimeRaidState{LeaderID: 99, Members: members}
|
||||
if !flag(beforeStart, 99) || flag(beforeStart, 20) {
|
||||
t.Fatal("before the raid starts the flag must stay \"is the 团长\"")
|
||||
}
|
||||
// 单人攻坚:没有小队(applyRuntimeRaidParties 直接返回),同样维持旧行为。
|
||||
single := runtimeRaidState{LeaderID: 99, Started: true, TailFlag: 1, Members: members}
|
||||
if !flag(single, 99) || flag(single, 20) {
|
||||
t.Fatal("a single raid keeps \"is the 团长\"")
|
||||
}
|
||||
|
||||
// 运行中的多人团:1 队的队长是普通队队长(20),2 队没有记录 → 落到最低槽位(30)。
|
||||
state := started(members, map[byte]uint16{1: 20})
|
||||
if !flag(state, 20) || flag(state, 22) {
|
||||
t.Fatal("squad 1 must be led by the ordinary party's leader")
|
||||
}
|
||||
if !flag(state, 30) {
|
||||
t.Fatal("a squad with no recorded leader falls back to its lowest occupied slot")
|
||||
}
|
||||
if flag(state, 99) {
|
||||
t.Fatal("a member in no squad is nobody's squad leader")
|
||||
}
|
||||
|
||||
// 记录里的队长已经不在那一队了(比如他自己退了队):当作没有记录,落回最低槽位。
|
||||
stale := started(members, map[byte]uint16{1: 99})
|
||||
if !flag(stale, 20) || flag(stale, 99) {
|
||||
t.Fatal("a recorded leader outside its squad must not keep the flag")
|
||||
}
|
||||
}
|
||||
|
||||
// 普通队伍每次发布都会把队长镜像进它所在的小队:这就是"队长顺延"后客户端立刻拿到新队长的
|
||||
// 唯一途径(同步那条路在副本里会被跳过,而顺延恰恰发生在副本里)。
|
||||
func TestRaidSquadLeaderFollowsOrdinaryPartyPublication(t *testing.T) {
|
||||
service := &Service{
|
||||
options: options{gameUpperBodyCodec: gameUpperBodyCodecPlain},
|
||||
gameSessions: make(map[uint16]*gameSession),
|
||||
raids: make(map[uint32]*runtimeRaidState),
|
||||
}
|
||||
channel := channelcatalog.Channel{ID: 200, Type: 23, Name: "anton", Port: 10200}
|
||||
sessions := make(map[uint16]*gameSession, 2)
|
||||
for _, characterID := range []uint16{20, 22} {
|
||||
session := &gameSession{conn: &bufferConn{}, channel: channel, selectedCharacterID: characterID}
|
||||
service.bindGameSessionCharacter(session, characterID)
|
||||
sessions[characterID] = session
|
||||
}
|
||||
raidKey := uint32(0x00c80600)
|
||||
service.raidMu.Lock()
|
||||
service.raids[raidKey] = &runtimeRaidState{
|
||||
RaidKey: raidKey, LeaderID: 20, TailFlag: 5, NameBytes: []byte("anton"),
|
||||
Started: true,
|
||||
Members: []runtimeRaidMemberState{
|
||||
{CharID: 20, GroupIndex: 0, UserState: 1, HPPercent: 100, MPPercent: 100},
|
||||
{CharID: 22, GroupIndex: 0, UserState: 1, HPPercent: 100, MPPercent: 100},
|
||||
},
|
||||
Lifecycle: runtimeRaidLifecycleState{
|
||||
Status: runtimeRaidLifecyclePhaseActive, Phase: runtimeRaidDungeonPhaseOne,
|
||||
},
|
||||
}
|
||||
service.raidMu.Unlock()
|
||||
|
||||
// 22 建队、20 加入:发布时两人被同步进 1 队,队长 22 必须被记到那一队上。
|
||||
if _, result, joined := service.createOrJoinManagedRuntimeParty(sessions[20], sessions[22]); !joined || !result.OK {
|
||||
t.Fatalf("fixture party result=%+v", result)
|
||||
}
|
||||
state := raidStateForTest(t, service, raidKey)
|
||||
if leader := state.SquadPartyLeaders[1]; leader != 22 {
|
||||
t.Fatalf("squad 1 leader = %d, want the party leader 22 (table=%v)", leader, state.SquadPartyLeaders)
|
||||
}
|
||||
record := func(charID uint16) uint32 {
|
||||
index := runtimeRaidMemberIndex(state, charID)
|
||||
if index < 0 {
|
||||
t.Fatalf("char %d missing from the roster", charID)
|
||||
}
|
||||
return runtimeRaidMemberRecord(state, state.Members[index]).Field66BoolSource
|
||||
}
|
||||
if record(22) != 1 || record(20) != 0 {
|
||||
t.Fatalf("leader flag = 22:%d 20:%d, want 1/0", record(22), record(20))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user