Files
90CNServer/server/internal/services/dnfbridge/equipment_regeneration_pool_audit_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

131 lines
3.8 KiB
Go

package dnfbridge
import (
"fmt"
"os"
"sort"
"testing"
platformpvf "longheng.io/server/internal/platform/pvf"
)
// TestEquipmentRegenerationRealPVFPoolsNeverCollapse walks every regeneration
// source the active PVF accepts and reports the size of each pool op407 can
// draw from. A pool of one whose only member is the source itself is the live
// bug: the player pays the soul cost and gets their own piece back.
func TestEquipmentRegenerationRealPVFPoolsNeverCollapse(t *testing.T) {
pvfPath := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
if pvfPath == "" {
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE to audit op407 pools against the active PVF")
}
archive, err := platformpvf.Open(pvfPath)
if err != nil {
t.Fatal(err)
}
items, err := newPVFDungeonDropCatalog(archive)
if err != nil {
t.Fatal(err)
}
catalog, err := loadCurrentEquipmentRegenerationCatalog(items)
if err != nil {
t.Fatal(err)
}
groupPart := make(map[string]uint16, 128)
partNames := make([]uint16, 0, len(catalog.parts))
for part, groups := range catalog.parts {
partNames = append(partNames, part)
for group := range groups {
groupPart[group] = part
}
}
sort.Slice(partNames, func(i, j int) bool { return partNames[i] < partNames[j] })
// The live filter splits on legacy too: a 传承 piece only ever draws other
// 传承 pieces, and vice versa. Auditing without that dimension overstates
// every pool.
type key struct {
part uint16
rarity int64
level int64
legacy bool
}
sizes := make(map[key]int)
members := make(map[key][]int64)
for _, candidate := range catalog.byItemID {
part, ok := groupPart[candidate.itemGroup]
if !ok || candidate.attachType != "sealing" {
continue
}
id := key{part: part, rarity: candidate.rarity, level: candidate.minimumLevel, legacy: candidate.legacy}
sizes[id]++
if len(members[id]) < 4 {
members[id] = append(members[id], candidate.itemID)
}
}
keys := make([]key, 0, len(sizes))
for k := range sizes {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool {
if keys[i].part != keys[j].part {
return keys[i].part < keys[j].part
}
if keys[i].rarity != keys[j].rarity {
return keys[i].rarity < keys[j].rarity
}
if keys[i].level != keys[j].level {
return keys[i].level < keys[j].level
}
return !keys[i].legacy && keys[j].legacy
})
collapsed := make([]string, 0, 8)
for _, k := range keys {
size := sizes[k]
if size <= 1 {
collapsed = append(collapsed, fmt.Sprintf("part=%d rarity=%d level=%d legacy=%t size=%d members=%v", k.part, k.rarity, k.level, k.legacy, size, members[k]))
}
}
t.Logf("pool keys=%d", len(keys))
for _, k := range keys {
t.Logf(" part=%d rarity=%d level=%-3d legacy=%-5t size=%d", k.part, k.rarity, k.level, k.legacy, sizes[k])
}
if len(collapsed) > 0 {
t.Logf("collapsed pools (%d):", len(collapsed))
for _, line := range collapsed {
t.Logf(" %s", line)
}
}
// The guarantee: no source may be charged for a draw that can only return
// itself. Either its band holds another candidate, or the resolver refuses
// the attempt outright.
for _, k := range keys {
if sizes[k] > 1 {
continue
}
sole := members[k][0]
resolution, resolveErr := catalog.resolve(sole, 0, k.part)
if resolveErr != nil {
t.Fatalf("resolve sole candidate %d: %v", sole, resolveErr)
}
if resolution.Eligible {
t.Fatalf("item %d (part=%d rarity=%d level=%d legacy=%t) is the only candidate in its band yet regeneration was accepted: %+v",
sole, k.part, k.rarity, k.level, k.legacy, resolution)
}
}
// Weapons are the reported case: every band a player can actually reach
// has to offer a real draw.
for _, k := range keys {
if k.part != 1 || sizes[k] <= 1 {
continue
}
if sizes[k] < 3 {
t.Fatalf("weapon band part=%d rarity=%d level=%d legacy=%t has only %d candidates", k.part, k.rarity, k.level, k.legacy, sizes[k])
}
}
}