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,678 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"pvfine/internal/pvf"
|
||||
)
|
||||
|
||||
func openRealPaged110(t *testing.T) (*pvf.Archive, string) {
|
||||
t.Helper()
|
||||
archive := os.Getenv("PVF_TESTFILE")
|
||||
if archive == "" {
|
||||
t.Skip("PVF_TESTFILE not set; skipping Paged110 integration test")
|
||||
}
|
||||
if _, err := os.Stat(archive); err != nil {
|
||||
t.Skipf("stat %s: %v", archive, err)
|
||||
}
|
||||
sealedPath := filepath.Join(filepath.Dir(archive), "sk.dat")
|
||||
if _, err := os.Stat(sealedPath); err != nil {
|
||||
t.Skipf("stat %s: %v", sealedPath, err)
|
||||
}
|
||||
a, err := pvf.Open(archive)
|
||||
if err != nil {
|
||||
t.Fatalf("open %s: %v", archive, err)
|
||||
}
|
||||
return a, archive
|
||||
}
|
||||
|
||||
// paged110LayoutFixture builds the 110US list layout: every list lives under
|
||||
// `list/`, entries are archive-root-relative, and item names are stored as
|
||||
// `<table::key>` placeholders resolved through `list/n_string.lst`.
|
||||
func paged110LayoutFixture(t *testing.T) (*core, *pvf.Archive, int32, int32) {
|
||||
t.Helper()
|
||||
a := pvf.New()
|
||||
encode := func(s string) []byte {
|
||||
out := make([]byte, 0, len(s)*2)
|
||||
for _, r := range s {
|
||||
out = append(out, byte(r), byte(r>>8))
|
||||
}
|
||||
return out
|
||||
}
|
||||
if _, err := a.AddFileText("list/n_string.lst", "3 `String/Equipment.uv.str`", pvf.TypeScript); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := a.AddFileText("list/n_string_kor.lst", "3 `String/Equipment.kor.str`", pvf.TypeScript); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// name_514530375 is localized; name_514530376 is blank in the base table and
|
||||
// only the Korean overlay has it.
|
||||
a.AddFile("String/Equipment.uv.str",
|
||||
encode("name_514530375>白色兽语腰带 [A款]\r\nname_514530376=\r\n"), pvf.TypeScript)
|
||||
a.AddFile("String/Equipment.kor.str", encode("name_514530376>포니 비즈 뱅글[A타입]\r\n"), pvf.TypeScript)
|
||||
if _, err := a.AddFileText("list/equipment.lst",
|
||||
"514530375 `equipment/character/x/514530375.equ` 514530376 `equipment/character/x/514530376.equ`",
|
||||
pvf.TypeScript); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
itemIndex, err := a.AddFileText("equipment/character/x/514530375.equ",
|
||||
"[name]\n{8=`<3::name_514530375>`}\n[rarity]\n4", pvf.TypeScript)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := a.AddFileText("equipment/character/x/514530376.equ",
|
||||
"[name]\n{8=`<3::name_514530376>`}", pvf.TypeScript); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stringTableIndex, ok := a.Find("String/Equipment.uv.str")
|
||||
if !ok {
|
||||
t.Fatal("string table missing")
|
||||
}
|
||||
|
||||
c := NewCore()
|
||||
if err := c.setArchive(a); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(c.closeArchive)
|
||||
return c, a, itemIndex, stringTableIndex
|
||||
}
|
||||
|
||||
// TestSearchIndexResolvesPaged110Names searches the display text of an item
|
||||
// whose name is a placeholder in the 110US list layout.
|
||||
func TestSearchIndexResolvesPaged110Names(t *testing.T) {
|
||||
c, _, itemIndex, _ := paged110LayoutFixture(t)
|
||||
c.startSearchIndex()
|
||||
waitForSearchIndex(t, c)
|
||||
|
||||
result, err := NewArchiveService(c).Search("白色兽语腰带", 0, 20)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(result.Hits) != 1 {
|
||||
t.Fatalf("hits = %#v", result.Hits)
|
||||
}
|
||||
hit := result.Hits[0]
|
||||
if hit.Category != SearchCategoryEquipment || hit.FileIndex != itemIndex || hit.Name != "白色兽语腰带 [A款]" {
|
||||
t.Fatalf("hit = %#v", hit)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaged110ItemAnnotationRelations(t *testing.T) {
|
||||
c, a, itemIndex, _ := paged110LayoutFixture(t)
|
||||
sourceIndex, err := a.AddFileText("stackable/source.stk", "[input item]\n514530375 1", pvf.TypeScript)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := c.setArchive(a); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c.mu.Lock()
|
||||
reference, found := c.resolveAnnotationReferenceLocked("物品", "514530375")
|
||||
c.mu.Unlock()
|
||||
if !found || reference.FileIndex != itemIndex || reference.Name != "白色兽语腰带 [A款]" {
|
||||
t.Fatalf("item relation = %#v, found = %v", reference, found)
|
||||
}
|
||||
source, err := NewEditorService(c).GetFile(sourceIndex)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
linkedSource := false
|
||||
for _, annotation := range source.Annotations {
|
||||
if annotation.TargetFileIndex == itemIndex && annotation.Title == "白色兽语腰带 [A款]" {
|
||||
linkedSource = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !linkedSource {
|
||||
t.Fatalf("item reference annotation does not link to item %d: %#v", itemIndex, source.Annotations)
|
||||
}
|
||||
|
||||
listIndex, ok := a.Find("list/equipment.lst")
|
||||
if !ok {
|
||||
t.Fatal("equipment list missing")
|
||||
}
|
||||
file, err := NewEditorService(c).GetFile(listIndex)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
linked := false
|
||||
for _, annotation := range file.Annotations {
|
||||
if annotation.TargetFileIndex == itemIndex && annotation.Title == "白色兽语腰带 [A款]" {
|
||||
linked = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !linked {
|
||||
t.Fatalf("list annotations do not link to item %d: %#v", itemIndex, file.Annotations)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSearchIndexMarksOverlayFallbackNames covers the marker the explorer, the
|
||||
// search results and the editor tags add when a name only the language overlay
|
||||
// could answer.
|
||||
func TestSearchIndexMarksOverlayFallbackNames(t *testing.T) {
|
||||
c, a, _, _ := paged110LayoutFixture(t)
|
||||
c.startSearchIndex()
|
||||
waitForSearchIndex(t, c)
|
||||
|
||||
const want = "포니 비즈 뱅글[A타입](未翻译)"
|
||||
result, err := NewArchiveService(c).Search("포니 비즈 뱅글", 0, 10)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(result.Hits) != 1 {
|
||||
t.Fatalf("hits = %#v", result.Hits)
|
||||
}
|
||||
if result.Hits[0].Name != want {
|
||||
t.Errorf("search name = %q, want %q", result.Hits[0].Name, want)
|
||||
}
|
||||
|
||||
fallbackIndex, ok := a.Find("equipment/character/x/514530376.equ")
|
||||
if !ok {
|
||||
t.Fatal("fallback item missing")
|
||||
}
|
||||
file, err := NewEditorService(c).GetFile(fallbackIndex)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, tag := range file.Tags {
|
||||
if tag.Name == want {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("tree tags = %#v", file.Tags)
|
||||
}
|
||||
|
||||
// TestEditorAnnotationsShowResolvedPlaceholder checks the editor keeps the
|
||||
// placeholder in the document while showing the resolved text as a tag.
|
||||
func TestEditorAnnotationsShowResolvedPlaceholder(t *testing.T) {
|
||||
c, _, itemIndex, stringTableIndex := paged110LayoutFixture(t)
|
||||
service := NewEditorService(c)
|
||||
|
||||
file, err := service.GetFile(itemIndex)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(file.Text, "<3::name_514530375>") {
|
||||
t.Fatalf("editor text lost the placeholder: %q", file.Text)
|
||||
}
|
||||
|
||||
var resolved *EditorAnnotation
|
||||
for _, annotation := range file.Annotations {
|
||||
if annotation.Type == "placeholder" {
|
||||
copy := annotation
|
||||
resolved = ©
|
||||
break
|
||||
}
|
||||
}
|
||||
if resolved == nil {
|
||||
t.Fatalf("no placeholder annotation in %#v", file.Annotations)
|
||||
}
|
||||
if resolved.Title != "白色兽语腰带 [A款]" {
|
||||
t.Errorf("title = %q", resolved.Title)
|
||||
}
|
||||
if resolved.TargetFileIndex != stringTableIndex {
|
||||
t.Errorf("target = %d, want %d", resolved.TargetFileIndex, stringTableIndex)
|
||||
}
|
||||
if !strings.Contains(resolved.Content, "String/Equipment.uv.str") {
|
||||
t.Errorf("content = %q", resolved.Content)
|
||||
}
|
||||
}
|
||||
|
||||
// TestEditorAnnotationsMarkOverlayFallback covers the marker the preview and
|
||||
// the editor add when only a language overlay can answer the placeholder.
|
||||
func TestEditorAnnotationsMarkOverlayFallback(t *testing.T) {
|
||||
a := pvf.New()
|
||||
encode := func(s string) []byte {
|
||||
out := make([]byte, 0, len(s)*2)
|
||||
for _, r := range s {
|
||||
out = append(out, byte(r), byte(r>>8))
|
||||
}
|
||||
return out
|
||||
}
|
||||
if _, err := a.AddFileText("list/n_string.lst", "3 `String/Equipment.uv.str`", pvf.TypeScript); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := a.AddFileText("list/n_string_kor.lst", "3 `String/Equipment.kor.str`", pvf.TypeScript); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.AddFile("String/Equipment.uv.str", encode("name_1=\r\n"), pvf.TypeScript)
|
||||
a.AddFile("String/Equipment.kor.str", encode("name_1>포니 비즈 뱅글[A타입]\r\n"), pvf.TypeScript)
|
||||
itemIndex, err := a.AddFileText("equipment/a.equ", "[name]\n{8=`<3::name_1>`}", pvf.TypeScript)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c := NewCore()
|
||||
if err := c.setArchive(a); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(c.closeArchive)
|
||||
|
||||
annotations, err := NewEditorService(c).GetAnnotations(itemIndex)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, annotation := range annotations {
|
||||
if annotation.Type != "placeholder" {
|
||||
continue
|
||||
}
|
||||
if annotation.Title != "포니 비즈 뱅글[A타입](未翻译)" {
|
||||
t.Fatalf("title = %q", annotation.Title)
|
||||
}
|
||||
return
|
||||
}
|
||||
t.Fatalf("no placeholder annotation in %#v", annotations)
|
||||
}
|
||||
|
||||
// TestListTargetCandidates covers both list entry layouts.
|
||||
// TestSetPlaceholderTextEditsStringTableOnly rewrites the text behind a
|
||||
// placeholder: the script keeps its placeholder, the `.str` entry changes, and
|
||||
// the editor tag, tree name and search index all show the new text.
|
||||
func TestSetPlaceholderTextEditsStringTableOnly(t *testing.T) {
|
||||
c, a, itemIndex, _ := paged110LayoutFixture(t)
|
||||
c.startSearchIndex()
|
||||
waitForSearchIndex(t, c)
|
||||
service := NewEditorService(c)
|
||||
|
||||
before, err := service.GetFile(itemIndex)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := service.SetPlaceholderText(itemIndex, 3, "name_514530375", "改过的腰带"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
waitForSearchRefresh(t, c)
|
||||
|
||||
// The stored script is untouched: only the string table changed.
|
||||
if got, ok := a.LookupStringTable(3, "name_514530375"); !ok || got != "改过的腰带" {
|
||||
t.Errorf("string table = %q, %v", got, ok)
|
||||
}
|
||||
after, err := service.GetFile(itemIndex)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if after.Text != before.Text || !strings.Contains(after.Text, "<3::name_514530375>") {
|
||||
t.Errorf("script text changed: %q", after.Text)
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, annotation := range after.Annotations {
|
||||
if annotation.Type != "placeholder" || annotation.Placeholder == nil {
|
||||
continue
|
||||
}
|
||||
if annotation.Placeholder.TableIndex != 3 || annotation.Placeholder.Key != "name_514530375" {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
if annotation.Title != "改过的腰带" {
|
||||
t.Errorf("annotation title = %q", annotation.Title)
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("placeholder annotation missing: %#v", after.Annotations)
|
||||
}
|
||||
|
||||
tagFound := false
|
||||
for _, tag := range after.Tags {
|
||||
if tag.Name == "改过的腰带" {
|
||||
tagFound = true
|
||||
}
|
||||
}
|
||||
if !tagFound {
|
||||
t.Errorf("tree tags = %#v", after.Tags)
|
||||
}
|
||||
|
||||
result, err := NewArchiveService(c).Search("改过的腰带", 0, 10)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, hit := range result.Hits {
|
||||
if hit.FileIndex == itemIndex && hit.Name == "改过的腰带" {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("search hits = %#v", result.Hits)
|
||||
}
|
||||
|
||||
// TestSetPlaceholderTextEditsOverlayEntry covers the fallback case: the base
|
||||
// table has the key with an empty value, so the edit lands in the Korean overlay
|
||||
// and the name stays flagged as untranslated.
|
||||
func TestSetPlaceholderTextEditsOverlayEntry(t *testing.T) {
|
||||
c, a, _, _ := paged110LayoutFixture(t)
|
||||
fallbackIndex, ok := a.Find("equipment/character/x/514530376.equ")
|
||||
if !ok {
|
||||
t.Fatal("fallback item missing")
|
||||
}
|
||||
service := NewEditorService(c)
|
||||
if err := service.SetPlaceholderText(fallbackIndex, 3, "name_514530376", "改过的韩文"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, ok := a.LookupStringTable(3, "name_514530376"); !ok || got != "改过的韩文" {
|
||||
t.Errorf("overlay entry = %q, %v", got, ok)
|
||||
}
|
||||
// The base table must stay empty, so the value is still a fallback.
|
||||
resolution, ok := a.ResolveStringTable(3, "name_514530376")
|
||||
if !ok || resolution.Source == "" || !resolution.Fallback {
|
||||
t.Errorf("resolution = %#v, %v", resolution, ok)
|
||||
}
|
||||
if !strings.Contains(strings.ToLower(resolution.Source), "kor") {
|
||||
t.Errorf("edit did not land in the overlay: %s", resolution.Source)
|
||||
}
|
||||
annotations, err := service.GetAnnotations(fallbackIndex)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, annotation := range annotations {
|
||||
if annotation.Type != "placeholder" || annotation.Placeholder == nil {
|
||||
continue
|
||||
}
|
||||
if !annotation.Placeholder.Fallback {
|
||||
t.Error("annotation is no longer marked as a fallback")
|
||||
}
|
||||
if annotation.Title != "改过的韩文(未翻译)" {
|
||||
t.Errorf("title = %q", annotation.Title)
|
||||
}
|
||||
return
|
||||
}
|
||||
t.Fatalf("placeholder annotation missing: %#v", annotations)
|
||||
}
|
||||
|
||||
// TestPlaceholderCreationForNewFile covers the new-file workflow: a file that
|
||||
// references a key no table has yet gets a "create" annotation, and filling it
|
||||
// in appends the entry to the base table.
|
||||
func TestPlaceholderCreationForNewFile(t *testing.T) {
|
||||
a := pvf.New()
|
||||
encode := func(s string) []byte {
|
||||
out := make([]byte, 0, len(s)*2)
|
||||
for _, r := range s {
|
||||
out = append(out, byte(r), byte(r>>8))
|
||||
}
|
||||
return out
|
||||
}
|
||||
if _, err := a.AddFileText("list/n_string.lst", "3 `String/Equipment.uv.str`", pvf.TypeScript); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.AddFile("String/Equipment.uv.str", encode("name_514530375>白色兽语腰带 [A款]\r\n"), pvf.TypeScript)
|
||||
// A brand-new entry registered in the equipment list, referencing a key the
|
||||
// tables do not know.
|
||||
newIndex, err := a.AddFileText("equipment/character/x/new_item.equ",
|
||||
"[name]\n{8=`<3::name_new_item>`}\n[rarity]\n4", pvf.TypeScript)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := a.AddFileText("list/equipment.lst",
|
||||
"900000001 `equipment/character/x/new_item.equ`", pvf.TypeScript); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c := NewCore()
|
||||
if err := c.setArchive(a); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(c.closeArchive)
|
||||
c.startSearchIndex()
|
||||
waitForSearchIndex(t, c)
|
||||
service := NewEditorService(c)
|
||||
|
||||
file, err := service.GetFile(newIndex)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var missing *EditorAnnotation
|
||||
for _, annotation := range file.Annotations {
|
||||
if annotation.Type == "placeholder-missing" {
|
||||
copy := annotation
|
||||
missing = ©
|
||||
}
|
||||
}
|
||||
if missing == nil {
|
||||
t.Fatalf("no create annotation in %#v", file.Annotations)
|
||||
}
|
||||
if missing.Placeholder == nil || !missing.Placeholder.Missing || missing.Placeholder.Key != "name_new_item" {
|
||||
t.Fatalf("create annotation = %#v", missing)
|
||||
}
|
||||
if missing.Title != missingPlaceholderLabel {
|
||||
t.Errorf("title = %q", missing.Title)
|
||||
}
|
||||
|
||||
// Create the entry the way the dialog does.
|
||||
if err := service.SetPlaceholderText(newIndex, 3, "name_new_item", "新装备名"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
waitForSearchRefresh(t, c)
|
||||
if got, ok := a.LookupStringTable(3, "name_new_item"); !ok || got != "新装备名" {
|
||||
t.Errorf("created entry = %q, %v", got, ok)
|
||||
}
|
||||
after, err := service.GetFile(newIndex)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resolved := false
|
||||
for _, annotation := range after.Annotations {
|
||||
if annotation.Type != "placeholder" || annotation.Placeholder == nil {
|
||||
continue
|
||||
}
|
||||
resolved = true
|
||||
if annotation.Title != "新装备名" {
|
||||
t.Errorf("title after creation = %q", annotation.Title)
|
||||
}
|
||||
}
|
||||
if !resolved {
|
||||
t.Errorf("annotation stayed unresolved: %#v", after.Annotations)
|
||||
}
|
||||
tagged := false
|
||||
for _, tag := range after.Tags {
|
||||
if tag.Name == "新装备名" {
|
||||
tagged = true
|
||||
}
|
||||
}
|
||||
if !tagged {
|
||||
t.Errorf("tree tags = %#v", after.Tags)
|
||||
}
|
||||
result, err := NewArchiveService(c).Search("新装备名", 0, 10)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, hit := range result.Hits {
|
||||
if hit.FileIndex == newIndex && hit.Name == "新装备名" {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("new entry not searchable: %#v", result.Hits)
|
||||
}
|
||||
|
||||
func TestListTargetCandidates(t *testing.T) {
|
||||
candidates, ok := listPathCandidates("equipment/equipment.lst", "character/a.equ")
|
||||
if !ok {
|
||||
t.Fatal("list-dir-relative entry rejected")
|
||||
}
|
||||
if candidates[0] != "equipment/character/a.equ" {
|
||||
t.Errorf("first candidate = %q", candidates[0])
|
||||
}
|
||||
found := false
|
||||
for _, candidate := range candidates {
|
||||
if candidate == "character/a.equ" {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("root-relative candidate missing: %v", candidates)
|
||||
}
|
||||
|
||||
// 110US: a configured 90US list path must still resolve root-relative
|
||||
// entries stored under `list/`.
|
||||
candidates, ok = listPathCandidates("list/equipment.lst", "equipment/character/a.equ")
|
||||
if !ok {
|
||||
t.Fatal("root-relative entry rejected")
|
||||
}
|
||||
found = false
|
||||
for _, candidate := range candidates {
|
||||
if candidate == "equipment/character/a.equ" {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("root-relative candidate missing for list/ layout: %v", candidates)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSearchIndexRealPaged110 names the retail archive end to end: the 110US
|
||||
// list layout resolves, item names become display text, and searching the
|
||||
// Chinese name finds the item. Opening the 543MB archive plus indexing its
|
||||
// 638k list entries takes roughly half a minute, so the test skips when the
|
||||
// fixture is absent.
|
||||
func TestSearchIndexRealPaged110(t *testing.T) {
|
||||
a, _ := openRealPaged110(t)
|
||||
listIndex, ok := a.FindList("equipment/equipment.lst")
|
||||
if !ok {
|
||||
t.Fatal("equipment list not found")
|
||||
}
|
||||
if got := a.Path(listIndex); got != "list/equipment.lst" {
|
||||
t.Fatalf("equipment list = %s", got)
|
||||
}
|
||||
const itemPath = "character/demoniclancer/avatar/belt/514530375.equ"
|
||||
itemIndex, ok := a.Find(itemPath)
|
||||
if !ok {
|
||||
t.Fatalf("%s not found", itemPath)
|
||||
}
|
||||
metadata, err := a.ScriptMetadata(itemIndex)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !metadata.HasName || metadata.Name != "白色兽语腰带 [A款]" {
|
||||
t.Fatalf("%s metadata = %#v", itemPath, metadata)
|
||||
}
|
||||
|
||||
c := NewCore()
|
||||
if err := c.setArchive(a); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(c.closeArchive)
|
||||
c.startSearchIndex()
|
||||
waitForSearchIndexWithin(t, c, 15*time.Minute)
|
||||
|
||||
result, err := NewArchiveService(c).Search("白色兽语腰带", 0, 20)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
found := false
|
||||
for _, hit := range result.Hits {
|
||||
if hit.Category != SearchCategoryEquipment || !strings.HasPrefix(hit.Name, "白色兽语腰带") {
|
||||
continue
|
||||
}
|
||||
// The indexed name is the display text, not the placeholder.
|
||||
metadata, err := a.ScriptMetadata(hit.FileIndex)
|
||||
if err != nil || metadata.Name != hit.Name {
|
||||
t.Fatalf("hit %s name %q, metadata %#v err=%v", hit.Path, hit.Name, metadata, err)
|
||||
}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
if !found {
|
||||
for _, hit := range result.Hits {
|
||||
t.Logf("hit [%s] %s | %s", hit.Category, hit.Name, hit.Path)
|
||||
}
|
||||
t.Fatalf("no equipment hit for the Chinese name among %d hits", len(result.Hits))
|
||||
}
|
||||
|
||||
// A name this localization left blank is answered by the Korean overlay and
|
||||
// is flagged in the explorer and the search results.
|
||||
fallbackIndex, ok := a.Find("equipment/character/archer/avatar/belt/117530006.equ")
|
||||
if !ok {
|
||||
t.Fatal("fallback item not found")
|
||||
}
|
||||
metadata, err = a.ScriptMetadata(fallbackIndex)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !metadata.NameFallback {
|
||||
t.Fatalf("%s is not an overlay fallback: %#v", a.Path(fallbackIndex), metadata)
|
||||
}
|
||||
result, err = NewArchiveService(c).Search("포니 비즈 뱅글", 0, 10)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
flagged := false
|
||||
for _, hit := range result.Hits {
|
||||
if hit.FileIndex == fallbackIndex && hit.Name == metadata.Name+untranslatedMark {
|
||||
flagged = true
|
||||
}
|
||||
}
|
||||
if !flagged {
|
||||
for _, hit := range result.Hits {
|
||||
t.Logf("hit [%s] %s | %s", hit.Category, hit.Name, hit.Path)
|
||||
}
|
||||
t.Fatalf("fallback name not flagged in the search results")
|
||||
}
|
||||
|
||||
// Editing the text behind the placeholder changes the `.str` entry only and
|
||||
// is visible through the editor tag, the tree and the search index. This
|
||||
// item is registered in the equipment list, so it has index records.
|
||||
editIndex, ok := a.Find("equipment/character/archer/avatar/belt/117530002.equ")
|
||||
if !ok {
|
||||
t.Fatal("editable item not found")
|
||||
}
|
||||
const (
|
||||
editKey = "name_117530002"
|
||||
editValue = "改过的稀有腰带"
|
||||
)
|
||||
editorService := NewEditorService(c)
|
||||
before, err := editorService.GetFile(editIndex)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := editorService.SetPlaceholderText(editIndex, 3, editKey, editValue); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
waitForSearchRefresh(t, c)
|
||||
after, err := editorService.GetFile(editIndex)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if after.Text != before.Text || !strings.Contains(after.Text, "<3::"+editKey+">") {
|
||||
t.Errorf("script text changed: %q", after.Text)
|
||||
}
|
||||
if got, ok := a.LookupStringTable(3, editKey); !ok || got != editValue {
|
||||
t.Errorf("string table = %q, %v", got, ok)
|
||||
}
|
||||
tagged := false
|
||||
for _, tag := range after.Tags {
|
||||
if tag.Name == editValue {
|
||||
tagged = true
|
||||
}
|
||||
}
|
||||
if !tagged {
|
||||
t.Errorf("tree tags = %#v", after.Tags)
|
||||
}
|
||||
result, err = NewArchiveService(c).Search(editValue, 0, 20)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, hit := range result.Hits {
|
||||
if hit.FileIndex == editIndex && hit.Name == editValue {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("edited name not searchable: %#v", result.Hits)
|
||||
}
|
||||
|
||||
func waitForSearchIndexWithin(t *testing.T, c *core, limit time.Duration) IndexStatus {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(limit)
|
||||
svc := NewArchiveService(c)
|
||||
for time.Now().Before(deadline) {
|
||||
status := svc.IndexStatus()
|
||||
if status.State == IndexStateReady || status.State == IndexStateError {
|
||||
if status.State == IndexStateError {
|
||||
t.Fatalf("search index failed: %s", status.Error)
|
||||
}
|
||||
return status
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("search index did not finish: %#v", svc.IndexStatus())
|
||||
return IndexStatus{}
|
||||
}
|
||||
Reference in new issue
Block a user