DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
hanfuit committed 2026-11-04 14:31:55 +08:00
commit 645b64fe21
4388 files changed
+1405457

No files matched your search

@@ -0,0 +1,111 @@
package dnfbridge
import (
"testing"
)
// 现场 2026-10-03(服主报「塔罗牌用不了」,角色 1 礼白666):连点 6 次,6 次的拒绝
// 原因**逐字相同**,都是奖池的第一项:
//
// reason=random reward item requires a complete runtime-PVF outcome table:
// item=108040415 kind="equipment" target_list=0 slots=9..64
//
// 抽 6 次全是同一项,概率上不可能是运气(1/4 的表,6 次全中同一项的几率是 1/4096)。
// 追下去发现 stackable_lottery_pvf.go 里:
//
// roll := int64(0)
// if s.dungeonChoice != nil { ... } // 只在测试里被赋值
//
// —— `dungeonChoice` 在全仓**非测试代码里没有任何一处赋值**,线上它恒为 nil,于是
// roll 永远停在 0,`dungeonDropLotteryPick(entries, 0)` 永远返回第一项。这张卡从来
// 没有真正抽过。
//
// 本用例就是这条的回归锁:**不注入随机源**(线上就是这样)时也必须真的掷骰子。
func TestTarotLotteryRollsWithoutAnInjectedRandomSource(t *testing.T) {
catalog := mustCurrentTarotLotteryTestCatalog(t)
definition, err := catalog.ResolveItem(tarotLotteryTestCardItemID)
if err != nil {
t.Fatal(err)
}
if len(definition.LotteryEntries) != 2 {
t.Fatalf("夹具奖池=%d 项 want 2(这条用例的前提)", len(definition.LotteryEntries))
}
// 刻意**不**设 service.dungeonChoice:这就是线上的形状。
service := &Service{pvfItemCatalog: catalog}
const draws = 200
first, second := definition.LotteryEntries[0].ItemID, definition.LotteryEntries[1].ItemID
seen := map[int64]int{}
sawRollOutsideRange := false
for i := 0; i < draws; i++ {
resolution, resolveErr := service.resolveCurrentTarotLotteryCard(definition)
if resolveErr != nil {
t.Fatalf("第 %d 次抽取失败: %v", i, resolveErr)
}
if resolution == nil || resolution.Drawn == nil {
t.Fatalf("第 %d 次抽取没有结果(missed=%t)", i, resolution != nil && resolution.Missed)
}
if resolution.Roll < 0 || resolution.Roll >= resolution.TotalWeight {
sawRollOutsideRange = true
}
seen[resolution.Drawn.Reward.ItemID]++
}
if sawRollOutsideRange {
t.Fatalf("%d 次抽取里有 roll 落在 [0,total) 之外", draws)
}
// 权重 10000:10000,200 次里两项都该出现。钉死"不止一项"就够 ——
// 旧代码(roll 恒 0)在这里必红:它只可能产出第一项。
if seen[second] == 0 {
t.Fatalf("抽了 %d 次,第二项 %d 一次都没出现(命中分布=%v)——"+
"roll 又被钉死了", draws, second, seen)
}
if seen[first] == 0 {
t.Fatalf("抽了 %d 次,第一项 %d 一次都没出现(命中分布=%v)", draws, first, seen)
}
t.Logf("dungeonChoice=nil(线上形状)下 %d 次抽取分布=%v", draws, seen)
}
// 注入源在时必须仍然优先,这样既有测试与"可复现抽样"的能力不被这次兜底破坏。
func TestTarotLotteryPrefersTheInjectedRandomSource(t *testing.T) {
catalog := mustCurrentTarotLotteryTestCatalog(t)
definition, err := catalog.ResolveItem(tarotLotteryTestCardItemID)
if err != nil {
t.Fatal(err)
}
service := &Service{pvfItemCatalog: catalog}
second := definition.LotteryEntries[1]
boundary := definition.LotteryEntries[0].Weight // 第二项的起始 roll
service.dungeonChoice = func(int) (int, error) { return int(boundary), nil }
resolution, err := service.resolveCurrentTarotLotteryCard(definition)
if err != nil {
t.Fatal(err)
}
if resolution == nil || resolution.Drawn == nil {
t.Fatal("注入源下没有抽到结果")
}
if got := resolution.Drawn.Reward.ItemID; got != second.ItemID {
t.Fatalf("注入 roll=%d 抽到 %d want %d(第二项)", boundary, got, second.ItemID)
}
if resolution.Roll != boundary {
t.Fatalf("resolution.Roll=%d want %d", resolution.Roll, boundary)
}
}
// 注入源越界必须拒绝,而不是被静默夹到边界上:宁可这次不生效,也不能给出错误的奖。
func TestTarotLotteryRejectsAnOutOfRangeInjectedRoll(t *testing.T) {
catalog := mustCurrentTarotLotteryTestCatalog(t)
definition, err := catalog.ResolveItem(tarotLotteryTestCardItemID)
if err != nil {
t.Fatal(err)
}
service := &Service{pvfItemCatalog: catalog}
total := dungeonDropLotteryTotalWeight(definition.LotteryEntries)
if total <= 0 {
t.Fatal("夹具奖池总权重必须为正")
}
service.dungeonChoice = func(int) (int, error) { return int(total), nil } // total 已在区间外
if _, err := service.resolveCurrentTarotLotteryCard(definition); err == nil {
t.Fatal("越界的注入 roll 必须报错,不能被当成合法抽取")
}
}