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