Files
90CNServer/pvf/services/gui_compatibility_real_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

245 lines
6.4 KiB
Go

package services
import (
"encoding/json"
"os"
"path"
"strings"
"testing"
"pvfine/internal/pvf"
)
// Use a separate opt-in so ordinary real-archive tests do not also scan every
// GUI document. No source archive or user settings are changed.
func TestGUIRealArchiveCompatibility(t *testing.T) {
filename := os.Getenv("PVF_GUI_TESTFILE")
if filename == "" {
t.Skip("PVF_GUI_TESTFILE required")
}
a, err := pvf.Open(filename)
if err != nil {
t.Fatal(err)
}
defer a.Release()
c := newCore()
// Readers need the archive and relation engine, not a full explorer/search
// index for the millions of unrelated files in a retail archive.
c.archive = a
service := NewFileGUIService(c)
type questFixture struct {
Text string `json:"text"`
}
var quests []questFixture
type stats struct {
Files, Errors, Issues, Empty int
}
counts := map[string]*stats{}
messages := map[string]int{}
for i := int32(0); i < a.FileCount(); i++ {
p := strings.ToLower(a.Path(i))
kind := ""
switch {
case strings.HasSuffix(p, ".shp"):
kind = "shop"
case strings.HasPrefix(p, "skill/") && strings.HasSuffix(p, ".skl"):
kind = "skill"
case strings.HasSuffix(p, ".qst"):
kind = "quest"
case p == worldDropPath || p == regionalWorldDropPath:
kind = "world-drop"
case isUpgradePath(p):
kind = "upgrade"
case p == "etc/upgrade_separate.etc":
kind = "forge"
}
if kind == "" {
continue
}
if counts[kind] == nil {
counts[kind] = &stats{}
}
s := counts[kind]
s.Files++
text, readErr := a.Text(i)
issues, empty := 0, false
if readErr == nil {
switch kind {
case "shop":
doc, e := service.ReadShop(i, text)
readErr = e
if e == nil {
issues, empty = len(doc.Issues), len(doc.Tabs) == 0
for _, issue := range doc.Issues {
messages[kind+": section="+issue.Section]++
}
}
case "skill":
doc, e := service.ReadSkill(i, text)
readErr = e
if e == nil {
empty = len(doc.Modes) == 0
for _, mode := range doc.Modes {
issues += len(mode.Issues)
for _, issue := range mode.Issues {
messages[kind+": "+issue]++
}
}
}
case "world-drop":
doc, e := service.ReadWorldDrop(i, text)
readErr = e
if e == nil {
empty = len(doc.Levels) == 0
}
case "upgrade":
doc, e := service.ReadUpgrade(i, text)
readErr = e
if e == nil {
empty = len(doc.Rows) == 0
if doc.SafeIssue != "" {
issues++
messages[kind+": "+doc.SafeIssue]++
}
}
case "forge":
doc, e := service.ReadForge(i, text)
readErr = e
if e == nil {
empty = len(doc.Rows) == 0
}
case "quest":
if os.Getenv("PVF_GUI_QUEST_CORPUS") != "" {
quests = append(quests, questFixture{Text: text})
}
}
}
if readErr != nil {
s.Errors++
messages[kind+": "+readErr.Error()]++
}
if issues > 0 {
s.Issues++
}
if empty {
s.Empty++
}
}
t.Logf("client=%s format=%s files=%d", a.ClientVersion(), a.Format(), a.FileCount())
for _, kind := range []string{"shop", "skill", "quest", "world-drop", "upgrade", "forge"} {
s := counts[kind]
if s == nil {
t.Errorf("%s: no matching files", kind)
continue
}
t.Logf("%s: files=%d read-errors=%d with-issues=%d empty=%d", kind, s.Files, s.Errors, s.Issues, s.Empty)
if s.Errors > 0 {
t.Errorf("%s: %d GUI read failures", kind, s.Errors)
}
}
for message, count := range messages {
t.Logf("diagnostic count=%d: %s", count, message)
}
for _, spec := range dropRateFileSpecs {
index, ok := a.Find(spec.path)
if !ok {
t.Errorf("drop %s: missing", spec.key)
continue
}
raw, err := a.RawBytes(index)
if err != nil {
t.Fatal(err)
}
doc, err := a.ParseScriptDocument(raw)
if err != nil {
t.Fatal(err)
}
section, _ := doc.Section([]string{dropRateSectionName}, 0)
if section == nil {
t.Errorf("drop %s: missing rarity section", spec.key)
continue
}
values, err := scriptIntegerValues(section.Values(), path.Base(spec.path))
if err != nil {
t.Fatal(err)
}
width, err := dropRateGroupWidth(values, spec)
t.Logf("drop %s: group-width=%d error=%v", spec.key, width, err)
if err != nil {
t.Error(err)
}
}
if doc, err := NewDropService(c).Read(); err != nil {
t.Errorf("drop structure: %v", err)
} else {
t.Logf("drop structure: %d sections; version allowed=%t", len(doc.Sections), supportedDropRateVersion(a.ClientVersion()))
for _, section := range doc.Sections {
section.Groups[0].Rates = []int{1000, 2000, 3000, 4000, 0}
}
replacements, err := prepareDropRateReplacements(a, doc.Sections)
if err != nil {
t.Fatal(err)
}
if len(replacements) != len(dropRateFileSpecs) {
t.Fatal("drop edit did not prepare all four files")
}
for i, replacement := range replacements {
original, err := a.RawBytes(replacement.index)
if err != nil {
t.Fatal(err)
}
before, err := a.ParseScriptDocument(original)
if err != nil {
t.Fatal(err)
}
after, err := a.ParseScriptDocument(replacement.raw)
if err != nil {
t.Fatal(err)
}
oldSection, _ := before.Section([]string{dropRateSectionName}, 0)
newSection, _ := after.Section([]string{dropRateSectionName}, 0)
oldValues, err := scriptIntegerValues(oldSection.Values(), "")
if err != nil {
t.Fatal(err)
}
newValues, err := scriptIntegerValues(newSection.Values(), "")
if err != nil {
t.Fatal(err)
}
spec := dropRateFileSpecs[i]
if len(newValues) != len(oldValues) || len(original) != len(replacement.raw) {
t.Fatal("drop edit changed the source layout")
}
for j, value := range oldValues {
if j >= spec.offset && j < spec.offset+4 {
continue
}
if newValues[j] != value {
t.Fatal("drop edit changed an unrelated threshold")
}
}
width, err := dropRateGroupWidth(newValues, spec)
if err != nil {
t.Fatal(err)
}
rates, err := decodeDropRateGroup(newValues[spec.offset : spec.offset+width])
if err != nil || !equalDropRates(rates, []int{1000, 2000, 3000, 4000, 0}) {
t.Fatal("drop edit did not round trip through the encoder")
}
}
t.Log("drop edits: four replacement payloads round tripped; extra thresholds preserved")
}
if output := os.Getenv("PVF_GUI_QUEST_CORPUS"); output != "" {
data, err := json.Marshal(quests)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(output, data, 0o600); err != nil {
t.Fatal(err)
}
}
if a.ModifiedCount() != 0 {
t.Fatal("GUI readers modified the archive")
}
}