服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
305 lines
9.4 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|