Files
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

114 lines
5.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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))
}
}