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