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,160 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
dnfguild "longheng.io/server/internal/modules/dnf/guild"
|
||||
platformpvf "longheng.io/server/internal/platform/pvf"
|
||||
)
|
||||
|
||||
// The catalog drives entry permission, so it has to survive the real archive:
|
||||
// the style list names the layout with mixed case, the layout nests blocks, and
|
||||
// a stage read wrong silently locks or unlocks the wrong dungeon.
|
||||
func TestRealPVFGuildDungeonCatalogMatchesTheLiveLayout(t *testing.T) {
|
||||
pvfPath := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
|
||||
if pvfPath == "" {
|
||||
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE to parse the live guild dungeon layout")
|
||||
}
|
||||
archive, err := platformpvf.Open(pvfPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
catalog, err := dnfguild.LoadDungeonCatalog(archive)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if catalog.FinalDungeon != 8508 {
|
||||
t.Fatalf("final dungeon=%d want 8508", catalog.FinalDungeon)
|
||||
}
|
||||
for _, want := range []struct {
|
||||
index uint32
|
||||
gaugeOnClear uint32
|
||||
ratio uint32
|
||||
required []uint32
|
||||
}{
|
||||
{index: 8500, gaugeOnClear: 150, ratio: 50},
|
||||
{index: 8504, gaugeOnClear: 75, ratio: 50, required: []uint32{8500}},
|
||||
{index: 8507, gaugeOnClear: 50, ratio: 50, required: []uint32{8500}},
|
||||
{index: 8505, gaugeOnClear: 50, ratio: 50, required: []uint32{8504}},
|
||||
{index: 8503, gaugeOnClear: 75, ratio: 50, required: []uint32{8507}},
|
||||
{index: 8508, gaugeOnClear: 30, ratio: 100, required: []uint32{8503, 8505}},
|
||||
} {
|
||||
stage, found := catalog.Stage(want.index)
|
||||
if !found {
|
||||
t.Fatalf("stage %d missing", want.index)
|
||||
}
|
||||
if stage.GaugeLimit != 10000 || stage.GaugeOnClear != want.gaugeOnClear ||
|
||||
stage.ConquestGaugeRatio != want.ratio {
|
||||
t.Fatalf("stage %d=%+v want clear=%d ratio=%d",
|
||||
want.index, stage, want.gaugeOnClear, want.ratio)
|
||||
}
|
||||
if len(stage.RequiredConquest) != len(want.required) {
|
||||
t.Fatalf("stage %d prerequisites=%v want %v",
|
||||
want.index, stage.RequiredConquest, want.required)
|
||||
}
|
||||
for i, required := range want.required {
|
||||
if stage.RequiredConquest[i] != required {
|
||||
t.Fatalf("stage %d prerequisites=%v want %v",
|
||||
want.index, stage.RequiredConquest, want.required)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A guild that has never cleared anything sees exactly 伤城, which is what
|
||||
// the operator observed when the table was first seeded with empty gauges.
|
||||
empty := map[uint32]uint32{}
|
||||
for _, stage := range catalog.Stages {
|
||||
unlocked := catalog.Unlocked(empty, stage.Index)
|
||||
if (stage.Index == 8500) != unlocked {
|
||||
t.Fatalf("with no progress, stage %d unlocked=%t", stage.Index, unlocked)
|
||||
}
|
||||
}
|
||||
|
||||
// The wire carries clears, not the gauge: the client multiplies by
|
||||
// [gauge on clear] itself. 9000 of 8500's 10000 is 60 clears of 150, which
|
||||
// is the 90% the operator asked for; sending 9000 rendered 100% because the
|
||||
// client read it as nine thousand clears.
|
||||
if got := currentGuildDungeonClears(mustStage(t, catalog, 8500), 9000); got != 60 {
|
||||
t.Fatalf("8500 gauge 9000 -> %d clears, want 60", got)
|
||||
}
|
||||
if got := currentGuildDungeonClears(mustStage(t, catalog, 8500), 150); got != 1 {
|
||||
t.Fatalf("8500 gauge 150 -> %d clears, want 1", got)
|
||||
}
|
||||
// A gauge past the limit must not render past a full bar.
|
||||
if got := currentGuildDungeonClears(mustStage(t, catalog, 8500), 99_999); got != 10000/150 {
|
||||
t.Fatalf("8500 over-credited -> %d clears, want %d", got, 10000/150)
|
||||
}
|
||||
|
||||
// Wire shape: u8 flag + 3 u32 + u32 count + count*(u32,u32).
|
||||
body := buildCurrentGuildDungeonUnlockBody(catalog, map[uint32]uint32{8500: 9000})
|
||||
if want := 17 + 8*len(catalog.Stages); len(body) != want {
|
||||
t.Fatalf("body len=%d want %d", len(body), want)
|
||||
}
|
||||
if body[0] != currentGuildDungeonUnlockFlag {
|
||||
t.Fatalf("flag=%d", body[0])
|
||||
}
|
||||
if got := binary.LittleEndian.Uint32(body[13:17]); got != uint32(len(catalog.Stages)) {
|
||||
t.Fatalf("count=%d want %d", got, len(catalog.Stages))
|
||||
}
|
||||
for index, stage := range catalog.Stages {
|
||||
offset := 17 + 8*index
|
||||
if got := binary.LittleEndian.Uint32(body[offset : offset+4]); got != stage.Index {
|
||||
t.Fatalf("pair %d index=%d want %d", index, got, stage.Index)
|
||||
}
|
||||
want := uint32(0)
|
||||
if stage.Index == 8500 {
|
||||
want = 60
|
||||
}
|
||||
if got := binary.LittleEndian.Uint32(body[offset+4 : offset+8]); got != want {
|
||||
t.Fatalf("pair %d clears=%d want %d", index, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mustStage(t *testing.T, catalog dnfguild.DungeonCatalog, index uint32) dnfguild.DungeonStage {
|
||||
t.Helper()
|
||||
stage, found := catalog.Stage(index)
|
||||
if !found {
|
||||
t.Fatalf("stage %d missing", index)
|
||||
}
|
||||
return stage
|
||||
}
|
||||
|
||||
// The per-stage reward counts differ (3 through 7 公会贡献证书), so a parser that
|
||||
// dropped the [reward] blocks or shared one block across stages would show up
|
||||
// here rather than as a silent under-payment in production.
|
||||
func TestRealPVFGuildDungeonRewardsParsePerStage(t *testing.T) {
|
||||
pvfPath := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
|
||||
if pvfPath == "" {
|
||||
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE to parse the live guild dungeon rewards")
|
||||
}
|
||||
archive, err := platformpvf.Open(pvfPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
catalog, err := dnfguild.LoadDungeonCatalog(archive)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, stage := range catalog.Stages {
|
||||
if len(stage.Rewards) != 2 {
|
||||
t.Fatalf("stage %d has %d reward blocks, want 2 (99%% and 100%%)",
|
||||
stage.Index, len(stage.Rewards))
|
||||
}
|
||||
for _, reward := range stage.Rewards {
|
||||
if reward.GaugeRatio != 99 && reward.GaugeRatio != 100 {
|
||||
t.Fatalf("stage %d reward ratio=%d", stage.Index, reward.GaugeRatio)
|
||||
}
|
||||
// 公会贡献证书; the count is what varies per stage.
|
||||
if reward.ItemID != 10154439 || reward.ItemCount == 0 || reward.Contribute != 20 {
|
||||
t.Fatalf("stage %d reward=%+v", stage.Index, reward)
|
||||
}
|
||||
}
|
||||
}
|
||||
first := mustStage(t, catalog, 8500)
|
||||
if first.Rewards[0].ItemCount != 3 {
|
||||
t.Fatalf("8500 reward count=%d want 3", first.Rewards[0].ItemCount)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user