Files
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

229 lines
5.7 KiB
Go

package dnfbridge
import (
"context"
"fmt"
"sort"
"strings"
"testing"
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
platformpvf "longheng.io/server/internal/platform/pvf"
)
const probePVFPath = `DNF_REAL_PVF`
type probeItem struct {
path string
doc *dnfpvf.Document
text string
}
func loadProbeEqu(t *testing.T) (*platformpvf.Archive, []probeItem) {
t.Helper()
archive, err := platformpvf.Open(probePVFPath)
if err != nil {
t.Fatal(err)
}
items := []probeItem{}
archive.RangePaths(func(p string) bool {
low := strings.ToLower(p)
if !strings.HasSuffix(low, ".equ") {
return true
}
text, err := archive.ReadText(p)
if err != nil {
return true
}
if !strings.Contains(text, "[skill levelup]") && !strings.Contains(text, "[all skill item]") {
return true
}
doc, perr := dnfpvf.Parse(p, text)
if perr != nil {
return true
}
items = append(items, probeItem{path: p, doc: doc, text: text})
return true
})
return archive, items
}
func TestZZProbeRealBuffCoverage(t *testing.T) {
archive, items := loadProbeEqu(t)
ctx := context.Background()
table, err := buildSkillCatalogFromSource(ctx, archive)
if err != nil {
t.Fatal(err)
}
character := dnfrepo.CharacterRecord{Job: "4", Stats: map[string]int64{"grow_type": 1}}
candidates, err := skillSwitchSupportSkillsByPVFSource(archive, character)
if err != nil {
t.Fatal(err)
}
jobTag := "priest"
stage := byte(1)
fmt.Printf("PROBE items scanned=%d priest(4) grow=1 candidates=%v\n", len(items), candidates)
for _, cand := range candidates {
def, ok := table.Find(4, cand)
req, active, name := 0, false, ""
if ok {
req, active, name = def.RequiredLevel, def.Active, def.Name
}
exact, ranged, both, totalNonZero := 0, 0, 0, 0
best := ""
for _, it := range items {
bonus := skillSwitchDocumentBuffLevelBonus(it.doc, jobTag, cand, req, active, stage)
_ = best
// split contributions: exact-only by re-running with range disabled is
// not exposed; approximate by checking block presence.
if bonus == 0 {
continue
}
totalNonZero++
hasLevelup := strings.Contains(it.text, "[skill levelup]")
hasAllSkill := strings.Contains(it.text, "[all skill item]")
switch {
case hasLevelup && hasAllSkill:
both++
case hasLevelup:
exact++
case hasAllSkill:
ranged++
}
}
fmt.Printf("PROBE cand=%d name=%q requiredLevel=%d active=%v -> itemsWithBonus=%d (exactOnly=%d rangeOnly=%d mixed=%d)\n",
cand, name, req, active, totalNonZero, exact, ranged, both)
}
// Sample the levelup rows for a couple of concrete items to inspect tag layout.
samples := 0
for _, it := range items {
if samples >= 3 {
break
}
for _, sec := range it.doc.Sections {
if sectionName(sec.Name) != "skill levelup" {
continue
}
if !strings.Contains(it.text, "[swordman]") {
continue
}
raw := ""
for _, tok := range it.doc.Tokens[sec.Start:sec.End] {
raw += tok.Raw + " "
}
fmt.Printf("PROBE sample levelup path=%s rows=%s\n", it.path, strings.TrimSpace(raw))
samples++
break
}
}
// Multi-tag same-skill rows: evidence for job-tag exclusivity.
dupSamples := 0
for _, it := range items {
if dupSamples >= 4 {
break
}
for _, sec := range it.doc.Sections {
if sectionName(sec.Name) != "skill levelup" {
continue
}
bySkill := map[int64][]string{}
toks := it.doc.Tokens[sec.Start:sec.End]
for i := 0; i+2 < len(toks); i += 3 {
if toks[i+1].Kind == dnfpvf.TokenInt {
bySkill[toks[i+1].Int] = append(bySkill[toks[i+1].Int], toks[i].Raw)
}
}
for skill, tags := range bySkill {
if len(tags) > 1 {
fmt.Printf("PROBE multiset path=%s skill=%d tags=%v\n", it.path, skill, tags)
dupSamples++
break
}
}
break
}
}
}
func TestZZProbeCommonTagAndSkillLevelTag(t *testing.T) {
archive, items := loadProbeEqu(t)
ctx := context.Background()
table, err := buildSkillCatalogFromSource(ctx, archive)
if err != nil {
t.Fatal(err)
}
commonSkills := map[int64]int{}
for _, it := range items {
for _, sec := range it.doc.Sections {
if sectionName(sec.Name) != "skill levelup" {
continue
}
toks := it.doc.Tokens[sec.Start:sec.End]
for i := 0; i+2 < len(toks); i += 3 {
if strings.ToLower(strings.Trim(toks[i].Raw, "`")) != "common" {
continue
}
if toks[i+1].Kind == dnfpvf.TokenInt {
commonSkills[toks[i+1].Int]++
}
}
}
}
keys := make([]int, 0, len(commonSkills))
for k := range commonSkills {
keys = append(keys, int(k))
}
sort.Ints(keys)
fmt.Printf("PROBE [common] rows distinctSkills=%d\n", len(keys))
shown := 0
for _, k := range keys {
name := ""
for job := byte(0); job < 16; job++ {
if def, ok := table.Find(job, uint16(k)); ok {
name = def.Name + " (job " + fmt.Sprint(job) + ")"
break
}
}
fmt.Printf("PROBE common skill=%d rows=%d name=%q\n", k, commonSkills[int64(k)], name)
shown++
if shown >= 20 {
break
}
}
// Which file types carry [SKILL_LEVEL]? Only the small data/config
// extensions are decompressed here to keep the probe cheap.
byExt := map[string]int{}
archive.RangePaths(func(p string) bool {
low := strings.ToLower(p)
want := false
for _, ext := range []string{".dat", ".etc", ".lst", ".skl", ".txt", ".ini"} {
if strings.HasSuffix(low, ext) {
want = true
break
}
}
if !want {
return true
}
text, err := archive.ReadText(p)
if err != nil {
return true
}
if strings.Contains(text, "[SKILL_LEVEL]") {
idx := strings.LastIndex(low, ".")
ext := low[idx:]
byExt[ext]++
if byExt[ext] <= 3 {
fmt.Printf("PROBE [SKILL_LEVEL] hit in %s\n", p)
}
}
return true
})
fmt.Printf("PROBE [SKILL_LEVEL] by extension: %v\n", byExt)
}