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,130 @@
|
||||
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])
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user