Files
90CNServer/pvf/services/item_effects_test.go
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

213 lines
8.1 KiB
Go

package services
import (
"fmt"
"strings"
"testing"
"pvfine/internal/pvf"
)
const effectTestANI = "[FRAME MAX]\n1\n[FRAME000]\n[IMAGE]\n`effect/test.img` 3\n[IMAGE POS]\n-16 -32\n[DELAY]\n80"
func effectFixture(t *testing.T, modern bool) (*core, int32, map[string]int32) {
t.Helper()
a := pvf.New()
indexes := map[string]int32{}
listPath, entry := "passiveobject/passiveobject.lst", "common/test.obj"
if modern {
listPath, entry = "list/passiveobject.lst", "passiveobject/common/test.obj"
}
files := map[string]string{
"equipment/test.equ": "[name]\n`测试装备`",
listPath: "12 `" + entry + "`",
"passiveobject/common/test.obj": "[basic motion]\n`animation/base.ani`\n[basic action]\n`action/base.act`\n[string data]\n`particle/own.ptl`\n[/string data]\n[add particles]\n[particle]\n[file]\n`particle/added.ptl`\n[/particle]\n[/add particles]",
"passiveobject/common/animation/base.ani": effectTestANI,
"passiveobject/common/action/base.act": "[BASE ANI]\n`../animation/action.ani`",
"passiveobject/common/animation/action.ani": effectTestANI,
"passiveobject/common/particle/own.ptl": "[life time]\n1000\n[creation frequency]\n25.0\n[maximum create number]\n5\n[gravity]\n10\n[transparent value]\n100 0\n[x axis]\n[move variable 1]\n20 5\n[/x axis]\n[object]\n`../animation/particle.ani` 1\n[/object]",
"passiveobject/common/particle/added.ptl": "[life time]\n500\n[object]\n`../animation/particle.ani`\n[/object]",
"passiveobject/common/animation/particle.ani": effectTestANI,
"equipment/character/particle/straight.ptl": "[life time]\n500\n[object]\n`../../../passiveobject/common/animation/particle.ani`\n[/object]",
}
for p, text := range files {
i, err := a.AddFileText(p, text, pvf.TypeScript)
if err != nil {
t.Fatal(err)
}
indexes[p] = i
}
c := NewCore()
if err := c.setArchive(a); err != nil {
t.Fatal(err)
}
t.Cleanup(c.closeArchive)
return c, indexes["equipment/test.equ"], indexes
}
func TestItemEffectPreviewCurrentArchiveAndRelativePaths(t *testing.T) {
for _, modern := range []bool{false, true} {
t.Run(fmt.Sprintf("modern=%v", modern), func(t *testing.T) {
c, index, _ := effectFixture(t, modern)
doc, err := NewFileGUIService(c).ReadItemEffectPreview(index, "12", "straight.ptl", nil)
if err != nil || doc == nil {
t.Fatal(err)
}
if len(doc.Issues) != 0 || doc.ObjectPath != "passiveobject/common/test.obj" || len(doc.Layers) != 5 {
t.Fatalf("preview=%+v", doc)
}
particles := 0
for _, layer := range doc.Layers {
if layer.Animation == nil || !layer.Animation.Valid || len(layer.Animation.Frames) != 1 || layer.Animation.Frames[0].Layers[0].Image.Index != 3 {
t.Fatalf("layer=%+v", layer)
}
if layer.Particle != nil {
particles++
if layer.Particle.Maximum > 64 {
t.Fatal("unbounded particles")
}
}
}
if particles != 3 {
t.Fatalf("particles=%d", particles)
}
})
}
}
func TestItemEffectPreviewDraftsAndMissingResources(t *testing.T) {
c, index, indexes := effectFixture(t, false)
s := NewFileGUIService(c)
p := "passiveobject/common/animation/base.ani"
doc, err := s.ReadItemEffectPreview(index, "12", "missing.ptl", []ShopDraft{{FileIndex: indexes[p], Path: p, Text: strings.Replace(effectTestANI, "80", "240", 1)}})
if err != nil || doc.Layers[0].Animation.Frames[0].Delay != 240 || len(doc.Issues) != 1 || !strings.Contains(doc.Issues[0], "无法读取粒子") {
t.Fatalf("draft=%+v %v", doc, err)
}
original, _ := c.archive.Text(indexes[p])
if strings.Contains(original, "240") {
t.Fatal("preview mutated archive")
}
doc, err = s.ReadItemEffectPreview(index, "12", "", []ShopDraft{{FileIndex: indexes[p], Path: "wrong/path.ani", Text: "invalid"}})
if err != nil || doc.Layers[0].Animation.Frames[0].Delay != 80 {
t.Fatal("mismatched draft applied")
}
for _, id := range []string{"-1", "nan", "2147483648"} {
if _, err := s.ReadItemEffectPreview(index, id, "", nil); err == nil {
t.Fatal("invalid object accepted")
}
}
if _, err := s.ReadItemEffectPreview(-1, "12", "", nil); err == nil {
t.Fatal("invalid index accepted")
}
doc, err = s.ReadItemEffectPreview(index, "99", "", nil)
if err != nil || len(doc.Layers) != 0 || len(doc.Issues) == 0 {
t.Fatal("missing object not reported")
}
}
func TestItemEffectParticleMetadataAndPlaceholders(t *testing.T) {
meta := parseEffectParticle(pvf.ParseScriptView("[life time]\n4000\n[creation frequency]\n25.0\n[transparent value]\n75\n[x axis]\n[move variable 1]\n20 -2\n[/x axis]\n[y axis]\n[move variable 1]\n30 4\n[/y axis]"))
if meta.Life != 4000 || meta.Maximum != 64 || meta.Alpha[1] != 75 || meta.Velocity[0] != 20 || meta.Acceleration[1] != 4 {
t.Fatalf("meta=%+v", meta)
}
c, index, _ := effectFixture(t, false)
_, err := c.archive.AddFileText("equipment/empty.ptl", "[life time]\n1000", pvf.TypeScript)
if err != nil {
t.Fatal(err)
}
doc, err := NewFileGUIService(c).ReadItemEffectPreview(index, "", "empty.ptl", nil)
if err != nil || len(doc.Layers) != 0 || len(doc.Issues) != 1 || !strings.Contains(doc.Issues[0], "没有可绘制") {
t.Fatalf("placeholder=%+v %v", doc, err)
}
}
func TestEffectResourceSearchScopesBeforePagination(t *testing.T) {
for _, disk := range []bool{false, true} {
t.Run(fmt.Sprintf("disk=%v", disk), func(t *testing.T) {
c, _, _ := effectFixture(t, false)
if _, err := c.archive.AddFileText("passiveobject/unregistered.obj", "[name]\n`未登记对象`", pvf.TypeScript); err != nil {
t.Fatal(err)
}
if err := c.setArchive(c.archive); err != nil {
t.Fatal(err)
}
if disk {
index, _, err := openSQLiteArchiveIndex(c.archive)
if err != nil {
t.Fatal(err)
}
c.mu.Lock()
c.diskIndex = index
c.installDiskArchiveIndexesLocked(c.archive)
c.mu.Unlock()
}
c.startSearchIndex()
waitForSearchIndex(t, c)
s := NewArchiveService(c)
result, err := s.SearchEffectResources("object", "12", 0, 1)
if err != nil || len(result.Hits) != 1 || result.Hits[0].ID != "12" {
t.Fatalf("object=%+v %v", result, err)
}
cursor, count := 0, 0
seen := map[string]bool{}
for page := 0; page < 10; page++ {
result, err = s.SearchEffectResources("particle", ".PTL", cursor, 1)
if err != nil {
t.Fatal(err)
}
for _, hit := range result.Hits {
if seen[hit.Path] || !strings.HasSuffix(hit.Path, ".ptl") {
t.Fatalf("bad particle=%+v", hit)
}
seen[hit.Path] = true
count++
}
if result.NextCursor < 0 {
break
}
if result.NextCursor <= cursor {
t.Fatal("cursor did not advance")
}
cursor = result.NextCursor
}
if count != 3 {
t.Fatalf("particles=%d", count)
}
if _, err := s.SearchEffectResources("invalid", "", 0, 30); err == nil {
t.Fatal("invalid resource kind accepted")
}
})
}
}
func TestEffectResourceAPCAndEmptyQuery(t *testing.T) {
c, _, _ := effectFixture(t, true)
for p, text := range map[string]string{
"list/aicharacter.lst": "101 `aicharacter/sub/a.chr` 102 `aicharacter/sub/b.chr`",
"aicharacter/sub/a.chr": "[name]\n`测试人偶甲`",
"aicharacter/sub/b.chr": "[name]\n`测试人偶乙`",
} {
if _, err := c.archive.AddFileText(p, text, pvf.TypeScript); err != nil {
t.Fatal(err)
}
}
if err := c.setArchive(c.archive); err != nil {
t.Fatal(err)
}
c.startSearchIndex()
waitForSearchIndex(t, c)
s := NewArchiveService(c)
first, err := s.SearchEffectResources("apc", "测试人偶", 0, 1)
if err != nil || len(first.Hits) != 1 || first.Hits[0].ID != "101" || first.NextCursor < 0 {
t.Fatalf("APC=%+v %v", first, err)
}
second, err := s.SearchEffectResources("apc", "测试人偶", first.NextCursor, 1)
if err != nil || len(second.Hits) != 1 || second.Hits[0].ID != "102" {
t.Fatalf("APC page2=%+v %v", second, err)
}
all, err := s.SearchEffectResources("object", "", 0, 30)
if err != nil || len(all.Hits) != 1 || all.Hits[0].ID != "12" {
t.Fatalf("empty query=%+v %v", all, err)
}
empty, err := s.SearchEffectResources("particle", "", 1<<40, 30)
if err != nil || len(empty.Hits) != 0 {
t.Fatalf("oversized cursor=%+v %v", empty, err)
}
}