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

305 lines
9.4 KiB
Go

package services
import (
"bytes"
"context"
"os"
"path/filepath"
"testing"
"pvfine/internal/pvf"
)
// writeRarityFixture writes an archive whose registered items cover the three
// states the explorer has to tell apart: a real rarity, the 普通 value 0 (which
// must survive as 0 rather than degrade into "unknown") and a file without any
// [rarity] section.
func writeRarityFixture(t *testing.T, name string) string {
t.Helper()
a := pvf.New()
for _, file := range []struct{ path, text string }{
{"equipment/equipment.lst", "2001 `character/common/amulet/epic.equ` 2002 `character/common/amulet/normal.equ` 2003 `character/common/amulet/plain.equ`"},
{"stackable/stackable.lst", "3001 `consumable/potion.stk`"},
{"equipment/character/common/amulet/epic.equ", "[name]\n`史诗项链`\n[rarity]\n4\n[usable job]\n`[all]`"},
{"equipment/character/common/amulet/normal.equ", "[name]\n`普通项链`\n[rarity]\n0\n[usable job]\n`[all]`"},
{"equipment/character/common/amulet/plain.equ", "[name]\n`无稀有度项链`\n[usable job]\n`[all]`"},
{"stackable/consumable/potion.stk", "[name]\n`稀有回复药`\n[rarity]\n2"},
} {
if _, err := a.AddFileText(file.path, file.text, pvf.TypeScript); err != nil {
t.Fatal(err)
}
}
output := filepath.Join(t.TempDir(), name)
var out bytes.Buffer
if err := a.SaveTo(&out); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(output, out.Bytes(), 0o644); err != nil {
t.Fatal(err)
}
return output
}
func openRarityFixture(t *testing.T, name string) (*core, *ArchiveService) {
t.Helper()
c := NewCore()
service := NewArchiveService(c)
if _, err := service.Open(writeRarityFixture(t, name)); err != nil {
t.Fatal(err)
}
t.Cleanup(c.closeArchive)
waitForSearchIndex(t, c)
return c, service
}
func TestSearchHitsCarryRarity(t *testing.T) {
_, service := openRarityFixture(t, "rarity-search.pvf")
found := make(map[string]int32)
for _, query := range []string{"项链", "回复药"} {
result, err := service.Search(query, 0, 20)
if err != nil {
t.Fatal(err)
}
for _, hit := range result.Hits {
if hit.Category == SearchCategoryFile {
continue
}
found[hit.Path] = hit.Rarity
}
}
for path, want := range map[string]int32{
"equipment/character/common/amulet/epic.equ": 4,
"equipment/character/common/amulet/normal.equ": 0,
"equipment/character/common/amulet/plain.equ": pvf.RarityUnknown,
"stackable/consumable/potion.stk": 2,
} {
got, ok := found[path]
if !ok {
t.Fatalf("%s missing from search results: %#v", path, found)
}
if got != want {
t.Fatalf("%s rarity = %d, want %d", path, got, want)
}
}
}
func TestExplorerTagsCarryRarity(t *testing.T) {
c, service := openRarityFixture(t, "rarity-tags.pvf")
nodes, err := service.ListChildren("equipment/character/common/amulet")
if err != nil {
t.Fatal(err)
}
tagsByPath := make(map[string]int32)
for _, node := range nodes {
if node.IsDir || len(node.Tags) == 0 {
continue
}
tagsByPath[node.Path] = node.Tags[0].Rarity
}
for path, want := range map[string]int32{
"equipment/character/common/amulet/epic.equ": 4,
"equipment/character/common/amulet/normal.equ": 0,
"equipment/character/common/amulet/plain.equ": pvf.RarityUnknown,
} {
got, ok := tagsByPath[path]
if !ok {
t.Fatalf("%s has no indexed tag: %#v", path, tagsByPath)
}
if got != want {
t.Fatalf("%s tag rarity = %d, want %d", path, got, want)
}
}
// A file that declares no rarity keeps the unknown sentinel instead of a
// color that would claim a rarity it never had.
resolved, err := service.ResolveFiles([]string{"equipment/equipment.lst"})
if err != nil || len(resolved) != 1 {
t.Fatalf("resolve = %#v, err = %v", resolved, err)
}
if visuals := c.fileVisualsLocked(resolved[0].FileIndex); visuals.rarity != pvf.RarityUnknown {
t.Fatalf("unregistered visuals rarity = %d, want %d", visuals.rarity, pvf.RarityUnknown)
}
}
func TestRarityOnlyEditRefreshesExplorerTags(t *testing.T) {
c, service := openRarityFixture(t, "rarity-edit.pvf")
target, ok := c.archive.Find("equipment/character/common/amulet/plain.equ")
if !ok {
t.Fatal("registered target missing")
}
// Only the [rarity] section changes: the index must still refresh, because
// the explorer colors the tag from this value.
if err := NewEditorService(c).SetText(target, "[name]\n`无稀有度项链`\n[rarity]\n6\n[usable job]\n`[all]`"); err != nil {
t.Fatal(err)
}
waitForSearchRefresh(t, c)
nodes, err := service.ListChildren("equipment/character/common/amulet")
if err != nil {
t.Fatal(err)
}
for _, node := range nodes {
if node.Path != "equipment/character/common/amulet/plain.equ" {
continue
}
if len(node.Tags) == 0 || node.Tags[0].Rarity != 6 {
t.Fatalf("refreshed tag = %#v, want rarity 6", node.Tags)
}
result, err := service.Search("无稀有度项链", 0, 5)
if err != nil || len(result.Hits) != 1 || result.Hits[0].Rarity != 6 {
t.Fatalf("refreshed search = %#v, err = %v", result.Hits, err)
}
return
}
t.Fatal("edited file missing from the explorer listing")
}
func TestShopItemsCarryRarity(t *testing.T) {
c, _ := openRarityFixture(t, "rarity-shop.pvf")
shopText := "[sell info]\n[tab]\n`测试`\n[item list]\n2001 2003\n[/item list]\n[/tab]\n[/sell info]"
itemIndex, err := c.archive.AddFileText("itemshop/test.shp", shopText, pvf.TypeScript)
if err != nil {
t.Fatal(err)
}
shop, err := NewFileGUIService(c).ReadShop(itemIndex, shopText)
if err != nil {
t.Fatal(err)
}
if len(shop.Tabs) != 1 || len(shop.Tabs[0].Groups) != 1 || len(shop.Tabs[0].Groups[0].Items) != 2 {
t.Fatalf("shop shape = %#v", shop.Tabs)
}
items := shop.Tabs[0].Groups[0].Items
if items[0].Item.Rarity != 4 || items[1].Item.Rarity != pvf.RarityUnknown {
t.Fatalf("shop rarities = %d, %d, want 4 and %d", items[0].Item.Rarity, items[1].Item.Rarity, pvf.RarityUnknown)
}
}
// TestRealRarityMatchesPreview checks, on a real archive, that the value the
// index exposes is exactly what the equipment tooltip shows: the explorer tag
// and the tooltip must never disagree about an item's rarity.
func TestRealRarityMatchesPreview(t *testing.T) {
path := os.Getenv("PVF_TESTFILE")
if path == "" {
t.Skip("PVF_TESTFILE 未设置")
}
a, err := pvf.Open(path)
if err != nil {
t.Fatal(err)
}
preview := NewPreviewService()
checked := 0
for _, sample := range []string{
"equipment/character/common/amulet/100300001.equ",
"equipment/character/common/amulet/100300002.equ",
"stackable/10000001/10000039.stk",
} {
index, ok := a.Find(sample)
if !ok {
continue
}
metadata, err := a.ScriptMetadata(index)
if err != nil {
t.Fatal(err)
}
text, err := a.Text(index)
if err != nil {
t.Fatal(err)
}
document, err := preview.ParseEQU(index, text)
if err != nil {
t.Fatal(err)
}
if got := metadata.RarityValue(); got != document.Rarity {
t.Fatalf("%s index rarity = %d, preview rarity = %d", sample, got, document.Rarity)
}
checked++
}
if checked == 0 {
t.Skip("真实 PVF 中没有稀有度样本")
}
}
// TestSQLiteIndexCarriesRarity covers the disk-backed index used by large
// archives, where the tags are joined against the per-file visuals row.
func TestSQLiteIndexCarriesRarity(t *testing.T) {
a := pvf.New()
files := []struct{ path, text string }{
{"equipment/equipment.lst", "2001 `character/epic.equ` 2003 `character/plain.equ`"},
{"equipment/character/epic.equ", "[name]\n`史诗项链`\n[rarity]\n5"},
{"equipment/character/plain.equ", "[name]\n`无稀有度项链`"},
}
for _, file := range files {
if _, err := a.AddFileText(file.path, file.text, pvf.TypeScript); err != nil {
t.Fatal(err)
}
}
index, _, err := openSQLiteArchiveIndex(a)
if err != nil {
t.Fatal(err)
}
defer index.close()
c := NewCore()
c.mu.Lock()
c.archive = a
c.diskIndex = index
c.indexGen = 1
c.mu.Unlock()
specs := []searchableListSpec{{listPath: "equipment/equipment.lst", category: SearchCategoryEquipment}}
if _, _, err := index.buildSemantic(context.Background(), c, a, specs, 1); err != nil {
t.Fatal(err)
}
for path, want := range map[string]int32{
"equipment/character/epic.equ": 5,
"equipment/character/plain.equ": pvf.RarityUnknown,
} {
fileIndex, ok := a.Find(path)
if !ok {
t.Fatalf("%s missing from the archive", path)
}
tags, err := index.tags(fileIndex)
if err != nil || len(tags) != 1 {
t.Fatalf("%s tags = %#v, err = %v", path, tags, err)
}
if tags[0].Rarity != want {
t.Fatalf("%s tag rarity = %d, want %d", path, tags[0].Rarity, want)
}
if visuals := index.visuals(fileIndex); visuals.rarity != want {
t.Fatalf("%s visuals rarity = %d, want %d", path, visuals.rarity, want)
}
}
}
func TestEditorItemAnnotationsCarryRarity(t *testing.T) {
c, _ := openRarityFixture(t, "rarity-annotations.pvf")
editor := NewEditorService(c)
for _, sample := range []struct {
path string
name string
want int32
}{
{"equipment/equipment.lst", "史诗项链", 4},
{"equipment/equipment.lst", "普通项链", 0},
{"equipment/equipment.lst", "无稀有度项链", pvf.RarityUnknown},
{"stackable/stackable.lst", "稀有回复药", 2},
} {
index, ok := c.archive.Find(sample.path)
if !ok {
t.Fatalf("missing fixture list: %s", sample.path)
}
annotations, err := editor.GetAnnotations(index)
if err != nil {
t.Fatal(err)
}
annotation := findEditorAnnotation(annotations, sample.name)
if annotation == nil || annotation.Rarity != sample.want {
t.Fatalf("%s annotation = %#v, want rarity %d", sample.name, annotation, sample.want)
}
}
}