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

145 lines
4.9 KiB
Go

package dnfbridge
import (
"encoding/binary"
"testing"
"time"
"longheng.io/server/internal/modules/dnf/raid"
)
// op1460's per-row handler (0x009AF190) rejects any key outside 81..125
// (`lea ecx,[eax-0x51]` / `cmp ecx,0x2c` / `ja default`), and those ids are the
// Fiend War state symbols from `etc/raid/fiendwar/fiendwar.etc.symbol`:
// 81 radar / 82 battery / 83..88 outposts and 兵站本营 / 92..96 boss state,
// mechanism digits and boss HP / 110,111 the BOSS display HP.
//
// Before this the board only routed `key >= 121` to 1460, so 81..111 travelled
// on op583 and the client read none of them: the radar/battery effects, the boss
// state and the boss HP bar were all blank.
func TestFiendWarBoardRoutesTheStateSymbolsOnOp1460(t *testing.T) {
now := time.Unix(1700000000, 0)
const (
areaStateKey = 79 // last [n AREA STATE] symbol -- stays on op583
radarStateKey = 81 // 雷达
batteryKey = 82 // 炮塔
commandCenter = 88 // 南部兵站本营
patternState = 94 // BOSS 机制位
bossViewHPKey = 110 // TOP BOSS VIEW HP
movieCenterKey = 125 // MOVIE COMMAND CENTER ON
outOfRangeKey = 126 // outside the client's window, for either opcode
)
b := &raid.FiendWarBattle{
Config: &raid.FiendWarBattleConfig{WaitingArea: 2, Areas: map[int]raid.FiendWarArea{}},
Areas: map[int]raid.FiendWarAreaState{},
GroupAreas: map[byte]int{1: 14},
MemberAreas: map[uint16]int{1: 14},
RecoveryUntil: map[uint16]time.Time{},
Symbols: map[uint32]int{
areaStateKey: 1,
radarStateKey: 1,
batteryKey: 1,
commandCenter: 1,
patternState: 1111,
bossViewHPKey: 100,
outOfRangeKey: 7,
},
}
state := runtimeRaidState{
RaidKey: 1,
FrozenMembers: []runtimeRaidMemberState{{CharID: 1, GroupIndex: 1}},
Lifecycle: runtimeRaidLifecycleState{FiendWar: b},
}
nativeRows := func(packets []runtimeRaidWireNotification) map[uint32]uint32 {
for _, packet := range packets {
if packet.msgID != 1460 {
continue
}
rows := make(map[uint32]uint32)
for offset := 1; offset+8 <= len(packet.body); offset += 8 {
rows[binary.LittleEndian.Uint32(packet.body[offset:])] = binary.LittleEndian.Uint32(packet.body[offset+4:])
}
return rows
}
return nil
}
symbolRows := func(packets []runtimeRaidWireNotification) map[uint32]uint32 {
for _, packet := range packets {
if packet.msgID != raid.RaidSymbolMsgID {
continue
}
rows := make(map[uint32]uint32)
for offset := 1; offset+8 <= len(packet.body); offset += 8 {
rows[binary.LittleEndian.Uint32(packet.body[offset:])] = binary.LittleEndian.Uint32(packet.body[offset+4:])
}
return rows
}
return nil
}
// Frame 1: the whole board is new, so the state symbols must be present.
first, err := runtimeFiendWarBoardPackets(state, runtimeRaidState{}, 0, now)
if err != nil {
t.Fatal(err)
}
native := nativeRows(first)
if native == nil {
t.Fatal("no op1460 packet was built")
}
for key, want := range map[uint32]uint32{
radarStateKey: 1,
batteryKey: 1,
commandCenter: 1,
patternState: 1111,
bossViewHPKey: 100,
} {
got, ok := native[key]
if !ok {
t.Errorf("symbol %d was not routed to op1460 (the client drops it on op583)", key)
continue
}
if got != want {
t.Errorf("symbol %d on op1460 = %d, want %d", key, got, want)
}
}
for _, key := range []uint32{areaStateKey, outOfRangeKey} {
if _, ok := native[key]; ok {
t.Errorf("symbol %d must stay off op1460 (outside the client's 81..125 window)", key)
}
}
// The legacy op583 route is untouched, and this change is deliberately
// ADDITIVE: 81..120 ride both opcodes for now, so nothing that used to work
// can regress while the new route is being proven on the live client.
// Trimming op583 to `key < 81` is a later, separate experiment.
symbols := symbolRows(first)
if symbols == nil {
t.Fatal("no op583 symbol table was built")
}
if _, ok := symbols[areaStateKey]; !ok {
t.Errorf("symbol %d must still be carried by op583", areaStateKey)
}
if _, ok := symbols[radarStateKey]; !ok {
t.Errorf("symbol %d stays on op583 while the op1460 route is unproven", radarStateKey)
}
// 121..125 were never on op583 and must not become op583 traffic.
if _, ok := symbols[movieCenterKey]; ok {
t.Errorf("symbol %d is op1460-only traffic and must not ride op583", movieCenterKey)
}
// Frame 2: the 121..125 one-shots keep their "first frame, non-zero only"
// rule, so they appear when they change and not before.
previous := cloneRuntimeRaidState(state)
b.Symbols[movieCenterKey] = 1
second, err := runtimeFiendWarBoardPackets(state, previous, 0, now)
if err != nil {
t.Fatal(err)
}
if got := nativeRows(second)[movieCenterKey]; got != 1 {
t.Errorf("symbol %d after its change = %d, want 1 routed to op1460", movieCenterKey, got)
}
if _, ok := nativeRows(first)[movieCenterKey]; ok {
t.Errorf("symbol %d must not be replayed on the first frame while zero", movieCenterKey)
}
}