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,185 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/channelcatalog"
|
||||
"longheng.io/server/internal/modules/dnf/dungeoncmd"
|
||||
"longheng.io/server/internal/modules/dnf/raid"
|
||||
)
|
||||
|
||||
// 真机 2026-10-02 12:34~12:38(raid 12058625,分片 012)的**原样复刻**:
|
||||
//
|
||||
// 04:34:58 char 21 走进 306000460(下侧 BOSS 房)→ bot boss state = 4
|
||||
// 04:35:02 char 20 走进 306000463(上侧 BOSS 房)→ top = 4,且 bot 已是 4
|
||||
// ⇒ PVF rule 131/132 触发 boss pattern init ⇒ 两侧各分到一颗球、pattern = 1111
|
||||
// 04:35:35 起 char 1(上侧队长)与 char 22(下侧队长)各自反复报 kind=1 value=5
|
||||
// 服务端:rejected 0、dropped 0 —— 但机制**不推进**,球永远是同一颗。
|
||||
//
|
||||
// 本用例用**真 PVF 配置**把这段摆出来,然后要求:两队各自打掉**自己手上那颗球**
|
||||
// 之后,机制条按颗数掉位(1111 → 111 → 11),而且打自己那颗不影响对面。
|
||||
func TestRealPVFFiendWarRoshanPatternAdvancesFromBothSidesReports(t *testing.T) {
|
||||
path := os.Getenv("DNF_FIENDWAR_REAL_PVF")
|
||||
if path == "" {
|
||||
t.Skip("DNF_FIENDWAR_REAL_PVF not set")
|
||||
}
|
||||
service := &Service{gameSessions: map[uint16]*gameSession{}}
|
||||
service.options.pvfPath = path
|
||||
config, err := service.runtimeRaidFiendWarConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("真 PVF 配置: %v", err)
|
||||
}
|
||||
battle, err := raid.NewFiendWarBattle(config.Battle)
|
||||
if err != nil {
|
||||
t.Fatalf("战场: %v", err)
|
||||
}
|
||||
// 真机顺序:先下侧进 BOSS 房,再上侧。
|
||||
for _, mapID := range []int{306000460, 306000463} {
|
||||
if err := battle.Apply(raid.FiendWarEvent{Kind: "on enter map", ID: mapID}, time.Now().UTC()); err != nil {
|
||||
t.Fatalf("on enter map %d: %v", mapID, err)
|
||||
}
|
||||
}
|
||||
topClone := battle.Value("top boss state")
|
||||
botClone := battle.Value("bot boss state")
|
||||
// 客户端 0x9D80A0 那张表:5=宝石/虚无 6=火 7=冰 8=风。
|
||||
if topClone < 5 || topClone > 8 {
|
||||
t.Fatalf("上侧 state=%d,want 5..8(两侧都进 BOSS 房之后应当 pattern init 分球)", topClone)
|
||||
}
|
||||
if botClone < 5 || botClone > 8 || botClone == topClone {
|
||||
t.Fatalf("两侧应当拿到两颗**不同**的球:top=%d bot=%d", topClone, botClone)
|
||||
}
|
||||
if got := battle.Value("boss pattern state"); got != 1111 {
|
||||
t.Fatalf("机制条=%d,want 1111", got)
|
||||
}
|
||||
// 真机上这两个区域此刻是"被占"的(Enter 成功过)。dnfbridge 侧没有 set,
|
||||
// 所以直接按战场语义摆好 —— BossReport 的第一道门就是这一条。
|
||||
battle.Areas[30] = raid.FiendWarAreaState{Group: 1}
|
||||
battle.Areas[31] = raid.FiendWarAreaState{Group: 2}
|
||||
|
||||
const raidKey = uint32(0x00c80440)
|
||||
topSeat := runtimeRaidDungeonSeat{Group: 1}
|
||||
botSeat := runtimeRaidDungeonSeat{Group: 2}
|
||||
members := []runtimeRaidMemberState{
|
||||
{CharID: 1, Name: "top-leader", GroupIndex: 1, SlotOrder: 0},
|
||||
{CharID: 20, Name: "top-member", GroupIndex: 2, SlotOrder: 0},
|
||||
{CharID: 22, Name: "bot-leader", GroupIndex: 3, SlotOrder: 0},
|
||||
{CharID: 21, Name: "bot-member", GroupIndex: 4, SlotOrder: 0},
|
||||
}
|
||||
// 真机:上侧的 run 是 char 20 开的(token=11),下侧是 char 21 开的(token=13);
|
||||
// 两个队长(char 1 / char 22)都是 adopt 进来的。
|
||||
topProgress := runtimeRaidDungeonProgress{
|
||||
Phase: 1, MapID: 4978, Seat: topSeat, CharacterID: 20,
|
||||
LifecycleToken: 11, Status: runtimeRaidDungeonProgressLoaded,
|
||||
}
|
||||
botProgress := runtimeRaidDungeonProgress{
|
||||
Phase: 1, MapID: 4953, Seat: botSeat, CharacterID: 21,
|
||||
LifecycleToken: 13, Status: runtimeRaidDungeonProgressLoaded,
|
||||
}
|
||||
state := &runtimeRaidState{
|
||||
RaidKey: raidKey, LeaderID: 1, Started: true, EntryCostPaid: true,
|
||||
RaidMode: runtimeRaidOwnerFiendWar,
|
||||
Members: append([]runtimeRaidMemberState(nil), members...),
|
||||
FrozenMembers: append([]runtimeRaidMemberState(nil), members...),
|
||||
Lifecycle: runtimeRaidLifecycleState{
|
||||
Status: runtimeRaidLifecyclePhaseActive, Phase: 1, FiendWar: battle,
|
||||
GroupOccupancy: map[runtimeRaidDungeonSeat]runtimeRaidDungeonBinding{
|
||||
topSeat: {RaidKey: raidKey, Phase: 1, MapID: 4978, LifecycleToken: 11},
|
||||
botSeat: {RaidKey: raidKey, Phase: 1, MapID: 4953, LifecycleToken: 13},
|
||||
},
|
||||
Dungeons: map[runtimeRaidDungeonRunKey]runtimeRaidDungeonProgress{
|
||||
{MapID: 4978, Seat: topSeat}: topProgress,
|
||||
{MapID: 4953, Seat: botSeat}: botProgress,
|
||||
},
|
||||
},
|
||||
}
|
||||
service.raids = map[uint32]*runtimeRaidState{raidKey: state}
|
||||
|
||||
// 两个队长各带一个队员;队长是 char 1(上)/ char 22(下)。
|
||||
newLeaderSession := func(charID, memberID uint16, dungeonID int, localToken, runToken uint64) *gameSession {
|
||||
session := &gameSession{
|
||||
conn: &bufferConn{},
|
||||
connID: "roschan-report-" + strconv.Itoa(int(charID)),
|
||||
channel: channelcatalog.Channel{ID: 184, Type: 42},
|
||||
selectedCharacterID: charID,
|
||||
characterGeneration: 1,
|
||||
}
|
||||
session.dungeon.runtime = &runtimeDungeonState{
|
||||
Request: dungeoncmd.SelectDungeonRequest{DungeonID: uint32(dungeonID)},
|
||||
lifecycleToken: localToken,
|
||||
raidDungeon: runtimeRaidDungeonBinding{
|
||||
RaidKey: raidKey, Phase: 1, MapID: dungeonID,
|
||||
LifecycleToken: runToken,
|
||||
// **真机就是这个形状**:adopt 进来的成员,本地 loadedNotified 是 false
|
||||
// (notifyRuntimeRaidDungeonLifecycleLocked 对"加入者"跳过 loaded,
|
||||
// 只有 completed 才在本地留一笔)。
|
||||
// 真机 12:34~12:38 的两个队长(char 1 / char 22)全都是 false,
|
||||
// 而 kind=1 机制上报只有他们在发 ⇒ 每一条都被静默丢掉。
|
||||
loadedNotified: false,
|
||||
},
|
||||
partyDungeonOwner: &runtimePartyDungeonOwner{
|
||||
partyID: uint16(100 + dungeonID%100),
|
||||
leaderID: charID,
|
||||
members: []runtimePartyDungeonMember{
|
||||
{slot: 0, userID: charID, generation: 1},
|
||||
{slot: 1, userID: memberID, generation: 1},
|
||||
},
|
||||
},
|
||||
}
|
||||
service.gameSessions[charID] = session
|
||||
return session
|
||||
}
|
||||
topLeader := newLeaderSession(1, 20, 4978, 9, 11)
|
||||
botLeader := newLeaderSession(22, 21, 4953, 12, 13)
|
||||
|
||||
body := func(kind, value int) []byte {
|
||||
payload := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint32(payload[:4], uint32(kind))
|
||||
binary.LittleEndian.PutUint32(payload[4:], uint32(value))
|
||||
return payload
|
||||
}
|
||||
|
||||
// 客户端 0x9D80A0 那张表:5=宝石/虚无 6=火 7=冰 8=风;球池 boss pattern state
|
||||
// 的四个十进制位就是这四颗球(1=还在)。真 PVF 的分球是随机的,所以期望值
|
||||
// 必须按"这一侧实际拿到哪颗球"算出来,不能写死 111/11。
|
||||
cloneWeight := func(clone int) int { return []int{1000, 100, 10, 1}[clone-5] }
|
||||
|
||||
// 上侧先报:只清掉**它自己**那颗球那一位,并给它补一颗新的;下侧一点不动。
|
||||
if err := service.handleRuntimeFiendWarBoss(topLeader, body(1, topClone)); err != nil {
|
||||
t.Fatalf("上侧机制上报: %v", err)
|
||||
}
|
||||
if got, want := state.Lifecycle.FiendWar.Value("boss pattern state"), 1111-cloneWeight(topClone); got != want {
|
||||
t.Fatalf("上侧打掉自己那颗球(%d)之后机制条 = %d,want %d", topClone, got, want)
|
||||
}
|
||||
if got := state.Lifecycle.FiendWar.Value("bot boss state"); got != botClone {
|
||||
t.Fatalf("上侧打自己的球却动了对面:bot boss state=%d,want %d", got, botClone)
|
||||
}
|
||||
topSecond := state.Lifecycle.FiendWar.Value("top boss state")
|
||||
if topSecond < 5 || topSecond > 8 || topSecond == botClone {
|
||||
t.Fatalf("上侧补到的球 = %d,want 5..8 且不等于对面那颗 %d", topSecond, botClone)
|
||||
}
|
||||
// 下侧报它手上那颗:池子里只剩上侧没拿的那一颗。
|
||||
if err := service.handleRuntimeFiendWarBoss(botLeader, body(1, botClone)); err != nil {
|
||||
t.Fatalf("下侧机制上报: %v", err)
|
||||
}
|
||||
wantAfterBot := 1111 - cloneWeight(topClone) - cloneWeight(botClone)
|
||||
if got := state.Lifecycle.FiendWar.Value("boss pattern state"); got != wantAfterBot {
|
||||
t.Fatalf("下侧打掉自己那颗球(%d)之后机制条 = %d,want %d\n"+
|
||||
"(真机 12:35~12:38:两侧都在报 kind=1 value=5,rejected 0 / dropped 0,"+
|
||||
"但球一直停在第一颗)", botClone, got, wantAfterBot)
|
||||
}
|
||||
botSecond := state.Lifecycle.FiendWar.Value("bot boss state")
|
||||
if botSecond < 5 || botSecond > 8 || botSecond == topSecond {
|
||||
t.Fatalf("下侧补到的球 = %d,want 5..8 且不等于上侧那颗 %d", botSecond, topSecond)
|
||||
}
|
||||
// 现在各自手上那颗也必须能被报掉 —— 这就是"球会切换"。
|
||||
if err := service.handleRuntimeFiendWarBoss(topLeader, body(1, topSecond)); err != nil {
|
||||
t.Fatalf("上侧第二颗球的机制上报: %v", err)
|
||||
}
|
||||
if got, want := state.Lifecycle.FiendWar.Value("boss pattern state"), wantAfterBot-cloneWeight(topSecond); got != want {
|
||||
t.Fatalf("上侧第二颗球(%d)之后机制条 = %d,want %d", topSecond, got, want)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user