Files
90CNServer/pvf/internal/preview/ani_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

230 lines
9.0 KiB
Go

package preview
import (
"fmt"
"strings"
"testing"
)
func TestParseANIStandardAnimation(t *testing.T) {
var builder strings.Builder
builder.WriteString("[LOOP]\n\t1\n\n[SHADOW]\n\t0\n\n[FRAME MAX]\n\t105\n")
for index := 0; index < 105; index++ {
fmt.Fprintf(&builder, "\n[FRAME%03d]\n\n[IMAGE]\n\t`Item/Title/15_summer_2.img`\t%d\n\n[IMAGE POS]\n\t0\t0\n\n[DELAY]\n\t100\n", index, index)
}
document := ParseANI(builder.String())
if !document.Valid {
t.Fatalf("document invalid: %#v", document.Issues)
}
if !document.Loop || document.Shadow || document.FrameMax != 105 || len(document.Frames) != 105 {
t.Fatalf("document metadata = %#v", document)
}
for index, frame := range document.Frames {
if frame.Index != int32(index) || frame.Delay != 100 || len(frame.Layers) != 1 {
t.Fatalf("frame %d = %#v", index, frame)
}
layer := frame.Layers[0]
if layer.Path != "Item/Title/15_summer_2.img" || layer.Index != int32(index) || layer.X != 0 || layer.Y != 0 {
t.Fatalf("frame %d layer = %#v", index, layer)
}
}
}
func TestParseANIMultipleLayersAndQuotedComments(t *testing.T) {
text := "[LOOP] # play once\n" +
" 0\n[SHADOW]\n 1\n[FRAME MAX]\n 1\n[FRAME000]\n" +
"[IMAGE]\n `Item/a``b.img` 2 # first layer\n[IMAGE POS]\n" +
" -3 4\n[IMAGE]\n `Item/second.img` 5\n[IMAGE POS]\n" +
" 6 -7\n[DELAY]\n 80\n"
document := ParseANI(text)
if !document.Valid || document.Loop || !document.Shadow {
t.Fatalf("document = %#v", document)
}
if len(document.Frames) != 1 || len(document.Frames[0].Layers) != 2 {
t.Fatalf("layers = %#v", document.Frames)
}
first, second := document.Frames[0].Layers[0], document.Frames[0].Layers[1]
if first.Path != "Item/a`b.img" || first.Index != 2 || first.X != -3 || first.Y != 4 {
t.Fatalf("first layer = %#v", first)
}
if second.Path != "Item/second.img" || second.Index != 5 || second.X != 6 || second.Y != -7 {
t.Fatalf("second layer = %#v", second)
}
}
func TestParseANIRecoverableDefaultsAndStructuralErrors(t *testing.T) {
text := "[FRAME MAX]\n 2\n[FRAME000]\n[IMAGE]\n `Item/a.img` 0\n" +
"[FRAME002]\n[DELAY]\n 0\n"
document := ParseANI(text)
if document.Valid {
t.Fatal("malformed document should not be valid")
}
if len(document.Frames) != 2 || document.Frames[0].Layers[0].X != 0 || document.Frames[0].Layers[0].Y != 0 {
t.Fatalf("best effort frames = %#v", document.Frames)
}
if document.Frames[0].Delay != 100 || document.Frames[1].Delay != 100 {
t.Fatalf("default delays = %#v", document.Frames)
}
assertIssue(t, document.Issues, IssueWarning, "图片缺少 [IMAGE POS]")
assertIssue(t, document.Issues, IssueWarning, "帧缺少 [DELAY]")
assertIssue(t, document.Issues, IssueWarning, "必须是正整数")
assertIssue(t, document.Issues, IssueError, "帧编号不连续")
}
func TestParseANIDuplicateFrameAndMissingImage(t *testing.T) {
text := "[FRAME MAX]\n 2\n[FRAME000]\n[FRAME000]\n[FRAME001]\n" +
"[DELAY]\n 100\n"
document := ParseANI(text)
if document.Valid {
t.Fatal("document with duplicate/missing frames should not be valid")
}
assertIssue(t, document.Issues, IssueError, "重复的帧编号")
assertIssue(t, document.Issues, IssueError, "帧没有有效的 [IMAGE]")
}
func TestParseANIRejectsInvalidFrameNameAndImageIndex(t *testing.T) {
text := "[FRAME MAX]\n 1\n[FRAMEoops]\n[IMAGE]\n `Item/test.img` -1\n"
document := ParseANI(text)
if document.Valid {
t.Fatal("document with invalid frame syntax should not be valid")
}
assertIssue(t, document.Issues, IssueError, "帧 section 名称必须是 FRAME 加数字")
assertIssue(t, document.Issues, IssueError, "声明帧数为 1,实际解析到 0 帧")
invalidIndex := ParseANI("[FRAME MAX]\n 1\n[FRAME000]\n[IMAGE]\n `Item/test.img` -1\n")
if invalidIndex.Valid {
t.Fatal("document with invalid image index should not be valid")
}
assertIssue(t, invalidIndex.Issues, IssueError, "图片索引必须是非负整数")
}
func TestParseANIAllowsEmptyImageFrame(t *testing.T) {
document := ParseANI("[FRAME MAX]\n 1\n[FRAME000]\n[IMAGE]\n[IMAGE POS]\n 3 4\n[DELAY]\n 50\n")
if !document.Valid || len(document.Frames) != 1 || len(document.Frames[0].Layers) != 1 {
t.Fatalf("empty image document = %#v", document)
}
layer := document.Frames[0].Layers[0]
if layer.Path != "" || layer.Index != -1 || layer.X != 3 || layer.Y != 4 {
t.Fatalf("empty image layer = %#v", layer)
}
assertIssue(t, document.Issues, IssueWarning, "图片引用为空")
}
func assertIssue(t *testing.T, issues []Issue, severity IssueSeverity, message string) {
t.Helper()
for _, issue := range issues {
if issue.Severity == severity && strings.Contains(issue.Message, message) {
return
}
}
t.Fatalf("missing %s issue containing %q: %#v", severity, message, issues)
}
func TestParseANIFrameFlagsStayInFrame(t *testing.T) {
text := "[LOOP]\n1\n[SHADOW]\n1\n[FRAME MAX]\n1\n[FRAME000]\n\t[IMAGE]\n\t\t`test.img` 0\n\t[LOOP]\n\t\t0\n\t[SHADOW]\n\t\t0\n\t[DELAY]\n\t\t75\n"
doc := ParseANI(text)
if !doc.Valid || !doc.Loop || !doc.Shadow || len(doc.Frames) != 1 || doc.Frames[0].Delay != 75 {
t.Fatalf("frame flags escaped into global scope: %+v", doc)
}
// Unindented global sections after frames retain the existing behavior.
after := ParseANI("[FRAME MAX]\n1\n[FRAME000]\n[IMAGE]\n`test.img` 0\n[DELAY]\n75\n[LOOP]\n1\n")
if !after.Valid || !after.Loop {
t.Fatal("global section after frame stopped working")
}
}
func TestParseANISharedImagePath(t *testing.T) {
var builder strings.Builder
builder.WriteString("[LOOP]\n1\n[FRAME MAX]\n50\n[IMAGE PATH]\n`Item/Title/2012dnf_festival_yellow.img`\n[/IMAGE PATH]\n")
for index := 0; index < 50; index++ {
fmt.Fprintf(&builder, "[FRAME%03d]\n[IMAGE EX]\n0 %d\n[DELAY]\n80\n", index, index)
}
document := ParseANI(builder.String())
if !document.Valid || !document.Loop || len(document.Frames) != 50 {
t.Fatalf("shared image path document = %#v", document)
}
for index, frame := range document.Frames {
if frame.Delay != 80 || len(frame.Layers) != 1 {
t.Fatalf("frame %d = %#v", index, frame)
}
want := Layer{Path: "Item/Title/2012dnf_festival_yellow.img", Index: int32(index)}
if frame.Layers[0] != want {
t.Fatalf("frame %d layer = %#v, want %#v", index, frame.Layers[0], want)
}
}
}
func TestParseANISharedPathsAndMixedLayers(t *testing.T) {
document := ParseANI("[FRAME MAX]\n1\n[FRAME000]\n" +
"[IMAGE EX]\n1 7\n[IMAGE POS]\n-3 4\n" +
"[IMAGE]\n`Item/direct.img` 2\n[IMAGE POS]\n5 -6\n" +
"[IMAGE EX]\n0 9\n[IMAGE POS]\n1 2\n[DELAY]\n80\n" +
"[IMAGE PATH]\n`Item/first.img`\n`Item/second.img`\n[/IMAGE PATH]\n")
if !document.Valid || len(document.Issues) != 0 || len(document.Frames) != 1 {
t.Fatalf("mixed document = %#v", document)
}
want := []Layer{
{Path: "Item/second.img", Index: 7, X: -3, Y: 4},
{Path: "Item/direct.img", Index: 2, X: 5, Y: -6},
{Path: "Item/first.img", Index: 9, X: 1, Y: 2},
}
got := document.Frames[0].Layers
if len(got) != len(want) {
t.Fatalf("layers = %#v", got)
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("layer %d = %#v, want %#v", i, got[i], want[i])
}
}
}
func TestParseANIRejectsInvalidImageEx(t *testing.T) {
for _, test := range []struct {
name, paths, reference, message string
}{
{"missing table", "", "0 1", "路径索引必须"},
{"missing index", "`a.img`", "0", "需要路径索引和图片索引"},
{"negative path", "`a.img`", "-2 0", "路径索引必须"},
{"out of range", "`a.img`", "1 0", "路径索引必须"},
{"invalid path", "`a.img`", "x 0", "路径索引必须"},
{"overflow path", "`a.img`", "2147483648 0", "路径索引必须"},
{"empty path", "``", "0 0", "图片路径不能为空"},
{"negative image", "`a.img`", "0 -1", "图片索引必须"},
{"invalid image", "`a.img`", "0 x", "图片索引必须"},
} {
t.Run(test.name, func(t *testing.T) {
document := ParseANI("[FRAME MAX]\n1\n[IMAGE PATH]\n" + test.paths +
"\n[/IMAGE PATH]\n[FRAME000]\n[IMAGE EX]\n" + test.reference + "\n[DELAY]\n80\n")
if document.Valid || len(document.Frames) != 1 || len(document.Frames[0].Layers) != 0 {
t.Fatalf("invalid reference document = %#v", document)
}
assertIssue(t, document.Issues, IssueError, test.message)
})
}
}
func TestParseANIImageExEmptyFrame(t *testing.T) {
for _, paths := range []string{"", "[IMAGE PATH]\n`Effect/WaveAuraEff.img`\n[/IMAGE PATH]\n"} {
document := ParseANI("[FRAME MAX]\n2\n" + paths +
"[FRAME000]\n[IMAGE EX]\n-1 0\n[IMAGE POS]\n12 0\n[IMAGE RATE]\n0.4124 0.4124\n[DELAY]\n260\n" +
"[FRAME001]\n[IMAGE]\n`Effect/WaveAuraEff.img` 16\n[IMAGE POS]\n-507 -631\n[DELAY]\n72\n")
if !document.Valid || len(document.Issues) != 0 || len(document.Frames) != 2 {
t.Fatalf("empty frame document = %#v", document)
}
first := document.Frames[0]
if first.Delay != 260 || len(first.Layers) != 1 || first.Layers[0] != (Layer{Index: -1, X: 12}) {
t.Fatalf("empty frame = %#v", first)
}
second := document.Frames[1]
if second.Delay != 72 || len(second.Layers) != 1 || second.Layers[0] != (Layer{Path: "Effect/WaveAuraEff.img", Index: 16, X: -507, Y: -631}) {
t.Fatalf("following frame = %#v", second)
}
}
}