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,479 @@
|
||||
package preview
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// IssueSeverity describes whether a preview issue prevents the document from
|
||||
// being used as a new playback document.
|
||||
type IssueSeverity string
|
||||
|
||||
const (
|
||||
IssueWarning IssueSeverity = "warning"
|
||||
IssueError IssueSeverity = "error"
|
||||
)
|
||||
|
||||
// Issue is a recoverable ANI parsing problem. Line is one-based and points to
|
||||
// the source line that introduced the problem when possible.
|
||||
type Issue struct {
|
||||
Severity IssueSeverity
|
||||
Line int
|
||||
Section string
|
||||
Message string
|
||||
}
|
||||
|
||||
// Layer is one image layer in an ANI frame.
|
||||
type Layer struct {
|
||||
Path string
|
||||
Index int32
|
||||
X int32
|
||||
Y int32
|
||||
}
|
||||
|
||||
// Frame is one decoded ANI frame.
|
||||
type Frame struct {
|
||||
Index int32
|
||||
Delay int32
|
||||
Layers []Layer
|
||||
}
|
||||
|
||||
// Document is the structured preview model produced from ANI text.
|
||||
type Document struct {
|
||||
Valid bool
|
||||
Loop bool
|
||||
Shadow bool
|
||||
FrameMax int32
|
||||
Frames []Frame
|
||||
Issues []Issue
|
||||
}
|
||||
|
||||
type token struct {
|
||||
value string
|
||||
line int
|
||||
}
|
||||
|
||||
type section struct {
|
||||
indented bool
|
||||
name string
|
||||
line int
|
||||
values []token
|
||||
}
|
||||
|
||||
type frameBlock struct {
|
||||
section section
|
||||
parts []section
|
||||
}
|
||||
|
||||
// ParseANI parses the decompiled TypeScript text used by an .ani file. The
|
||||
// parser intentionally works on text rather than archive tokens so preview
|
||||
// results can follow unsaved editor content.
|
||||
func ParseANI(text string) Document {
|
||||
sections := parseSections(text)
|
||||
document := Document{FrameMax: -1, Frames: make([]Frame, 0)}
|
||||
issues := make([]Issue, 0)
|
||||
|
||||
global := make(map[string][]section)
|
||||
blocks := make([]frameBlock, 0)
|
||||
var current *frameBlock
|
||||
for _, item := range sections {
|
||||
if _, ok := parseFrameName(item.name); ok {
|
||||
block := frameBlock{section: item, parts: make([]section, 0)}
|
||||
block.section.name = item.name
|
||||
blocks = append(blocks, block)
|
||||
current = &blocks[len(blocks)-1]
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(item.name, "frame") && item.name != "frame max" {
|
||||
issues = append(issues, Issue{Severity: IssueError, Line: item.line, Section: item.name, Message: "帧 section 名称必须是 FRAME 加数字"})
|
||||
current = nil
|
||||
continue
|
||||
}
|
||||
if current != nil {
|
||||
if isFramePart(item.name) || (item.indented && (item.name == "loop" || item.name == "shadow")) {
|
||||
current.parts = append(current.parts, item)
|
||||
continue
|
||||
}
|
||||
// A new top-level section ends the current frame. Keep known
|
||||
// global sections in the global map for files that place them
|
||||
// after the frame list.
|
||||
if isGlobalSection(item.name) {
|
||||
global[item.name] = append(global[item.name], item)
|
||||
current = nil
|
||||
continue
|
||||
}
|
||||
current.parts = append(current.parts, item)
|
||||
continue
|
||||
}
|
||||
if isGlobalSection(item.name) {
|
||||
global[item.name] = append(global[item.name], item)
|
||||
}
|
||||
}
|
||||
|
||||
parseBooleanField(&document.Loop, global["loop"], "LOOP", &issues)
|
||||
parseBooleanField(&document.Shadow, global["shadow"], "SHADOW", &issues)
|
||||
if values := global["frame max"]; len(values) == 0 {
|
||||
issues = append(issues, Issue{Severity: IssueError, Line: 1, Section: "FRAME MAX", Message: "缺少 [FRAME MAX]"})
|
||||
} else {
|
||||
document.FrameMax = parseNonNegativeInt(values[len(values)-1], "FRAME MAX", &issues, -1)
|
||||
if len(values) > 1 {
|
||||
issues = append(issues, Issue{Severity: IssueWarning, Line: values[len(values)-1].line, Section: "FRAME MAX", Message: "重复的 [FRAME MAX],使用最后一个值"})
|
||||
}
|
||||
}
|
||||
|
||||
var imagePaths []token
|
||||
for _, paths := range global["image path"] {
|
||||
imagePaths = append(imagePaths, paths.values...)
|
||||
}
|
||||
|
||||
frames := make([]Frame, 0, len(blocks))
|
||||
seenFrames := make(map[int32]struct{}, len(blocks))
|
||||
for _, block := range blocks {
|
||||
frameIndex, _ := parseFrameName(block.section.name)
|
||||
if _, exists := seenFrames[frameIndex]; exists {
|
||||
issues = append(issues, Issue{Severity: IssueError, Line: block.section.line, Section: block.section.name, Message: fmt.Sprintf("重复的帧编号 %d", frameIndex)})
|
||||
continue
|
||||
}
|
||||
seenFrames[frameIndex] = struct{}{}
|
||||
frame := Frame{Index: frameIndex, Delay: 100, Layers: make([]Layer, 0)}
|
||||
frameLine := block.section.line
|
||||
var pendingLayer *Layer
|
||||
posSet := false
|
||||
delaySeen := false
|
||||
|
||||
flushLayerPosition := func(line int) {
|
||||
if pendingLayer != nil && !posSet {
|
||||
issues = append(issues, Issue{Severity: IssueWarning, Line: line, Section: block.section.name, Message: "图片缺少 [IMAGE POS],使用坐标 0,0"})
|
||||
}
|
||||
}
|
||||
for _, part := range block.parts {
|
||||
switch part.name {
|
||||
case "image", "image ex":
|
||||
flushLayerPosition(part.line)
|
||||
var layer Layer
|
||||
var ok bool
|
||||
if part.name == "image ex" {
|
||||
layer, ok = parseImageEx(part, imagePaths, &issues)
|
||||
} else {
|
||||
layer, ok = parseImage(part, &issues, block.section.name)
|
||||
}
|
||||
if !ok {
|
||||
pendingLayer = nil
|
||||
posSet = false
|
||||
continue
|
||||
}
|
||||
frame.Layers = append(frame.Layers, layer)
|
||||
pendingLayer = &frame.Layers[len(frame.Layers)-1]
|
||||
posSet = false
|
||||
case "image pos":
|
||||
if pendingLayer == nil {
|
||||
issues = append(issues, Issue{Severity: IssueWarning, Line: part.line, Section: block.section.name, Message: "[IMAGE POS] 没有对应的 [IMAGE] 或 [IMAGE EX],已忽略"})
|
||||
continue
|
||||
}
|
||||
if len(part.values) < 2 {
|
||||
issues = append(issues, Issue{Severity: IssueError, Line: part.line, Section: "IMAGE POS", Message: "[IMAGE POS] 需要两个整数"})
|
||||
continue
|
||||
}
|
||||
x, xOK := parseInt(part.values[0])
|
||||
y, yOK := parseInt(part.values[1])
|
||||
if !xOK || !yOK {
|
||||
issues = append(issues, Issue{Severity: IssueError, Line: part.line, Section: "IMAGE POS", Message: "[IMAGE POS] 坐标必须是整数"})
|
||||
continue
|
||||
}
|
||||
pendingLayer.X, pendingLayer.Y = x, y
|
||||
posSet = true
|
||||
case "delay":
|
||||
if delaySeen {
|
||||
issues = append(issues, Issue{Severity: IssueWarning, Line: part.line, Section: block.section.name, Message: "重复的 [DELAY],使用最后一个值"})
|
||||
}
|
||||
delaySeen = true
|
||||
if len(part.values) == 0 {
|
||||
issues = append(issues, Issue{Severity: IssueWarning, Line: part.line, Section: "DELAY", Message: "[DELAY] 缺少数值,使用 100ms"})
|
||||
frame.Delay = 100
|
||||
continue
|
||||
}
|
||||
delay, ok := parseInt(part.values[0])
|
||||
if !ok || delay <= 0 {
|
||||
issues = append(issues, Issue{Severity: IssueWarning, Line: part.line, Section: "DELAY", Message: "[DELAY] 必须是正整数,使用 100ms"})
|
||||
frame.Delay = 100
|
||||
continue
|
||||
}
|
||||
frame.Delay = delay
|
||||
}
|
||||
}
|
||||
flushLayerPosition(frameLine)
|
||||
if !delaySeen {
|
||||
issues = append(issues, Issue{Severity: IssueWarning, Line: frameLine, Section: block.section.name, Message: "帧缺少 [DELAY],使用 100ms"})
|
||||
}
|
||||
if len(frame.Layers) == 0 {
|
||||
issues = append(issues, Issue{Severity: IssueError, Line: frameLine, Section: block.section.name, Message: "帧没有有效的 [IMAGE] 或 [IMAGE EX]"})
|
||||
}
|
||||
frames = append(frames, frame)
|
||||
}
|
||||
|
||||
sort.SliceStable(frames, func(i, j int) bool { return frames[i].Index < frames[j].Index })
|
||||
document.Frames = frames
|
||||
if document.FrameMax < 0 {
|
||||
// Keep the value useful for an invalid document without allowing a
|
||||
// consumer to mistake it for a valid declared count.
|
||||
document.FrameMax = int32(len(frames))
|
||||
}
|
||||
if document.FrameMax == 0 && len(frames) == 0 {
|
||||
issues = append(issues, Issue{Severity: IssueError, Line: 1, Section: "FRAME MAX", Message: "动画没有帧"})
|
||||
}
|
||||
if document.FrameMax != int32(len(frames)) {
|
||||
issues = append(issues, Issue{Severity: IssueError, Line: frameMaxLine(global["frame max"]), Section: "FRAME MAX", Message: fmt.Sprintf("声明帧数为 %d,实际解析到 %d 帧", document.FrameMax, len(frames))})
|
||||
}
|
||||
for index, frame := range frames {
|
||||
if frame.Index != int32(index) {
|
||||
issues = append(issues, Issue{Severity: IssueError, Line: frameLine(blocks, frame.Index), Section: fmt.Sprintf("FRAME%03d", frame.Index), Message: fmt.Sprintf("帧编号不连续,期望 %d,实际 %d", index, frame.Index)})
|
||||
}
|
||||
}
|
||||
|
||||
document.Issues = issues
|
||||
document.Valid = !hasErrors(issues) && len(frames) > 0
|
||||
return document
|
||||
}
|
||||
|
||||
func parseSections(text string) []section {
|
||||
lines := strings.Split(text, "\n")
|
||||
sections := make([]section, 0)
|
||||
var current *section
|
||||
for lineNumber, raw := range lines {
|
||||
line := stripComment(strings.TrimSuffix(raw, "\r"))
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "[") {
|
||||
end := strings.IndexByte(trimmed, ']')
|
||||
if end >= 0 {
|
||||
name := normalizeSection(trimmed[1:end])
|
||||
if strings.HasPrefix(name, "/") {
|
||||
current = nil
|
||||
continue
|
||||
}
|
||||
sections = append(sections, section{indented: len(line) > 0 && (line[0] == ' ' || line[0] == '\t'), name: name, line: lineNumber + 1, values: make([]token, 0)})
|
||||
current = §ions[len(sections)-1]
|
||||
rest := strings.TrimSpace(trimmed[end+1:])
|
||||
if rest != "" {
|
||||
current.values = append(current.values, tokenize(rest, lineNumber+1)...)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
if current != nil {
|
||||
current.values = append(current.values, tokenize(trimmed, lineNumber+1)...)
|
||||
}
|
||||
}
|
||||
return sections
|
||||
}
|
||||
|
||||
func tokenize(value string, line int) []token {
|
||||
result := make([]token, 0, 2)
|
||||
for index := 0; index < len(value); {
|
||||
for index < len(value) && isSpace(value[index]) {
|
||||
index++
|
||||
}
|
||||
if index >= len(value) {
|
||||
break
|
||||
}
|
||||
if value[index] == '`' {
|
||||
index++
|
||||
var builder strings.Builder
|
||||
for index < len(value) {
|
||||
if value[index] != '`' {
|
||||
builder.WriteByte(value[index])
|
||||
index++
|
||||
continue
|
||||
}
|
||||
if index+1 < len(value) && value[index+1] == '`' {
|
||||
builder.WriteByte('`')
|
||||
index += 2
|
||||
continue
|
||||
}
|
||||
index++
|
||||
break
|
||||
}
|
||||
result = append(result, token{value: builder.String(), line: line})
|
||||
continue
|
||||
}
|
||||
start := index
|
||||
for index < len(value) && !isSpace(value[index]) {
|
||||
index++
|
||||
}
|
||||
result = append(result, token{value: value[start:index], line: line})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func stripComment(value string) string {
|
||||
inBacktick := false
|
||||
for index := 0; index < len(value); index++ {
|
||||
switch value[index] {
|
||||
case '`':
|
||||
if inBacktick && index+1 < len(value) && value[index+1] == '`' {
|
||||
index++
|
||||
continue
|
||||
}
|
||||
inBacktick = !inBacktick
|
||||
case '#':
|
||||
if !inBacktick {
|
||||
return value[:index]
|
||||
}
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func normalizeSection(value string) string {
|
||||
return strings.ToLower(strings.Join(strings.Fields(strings.TrimSpace(value)), " "))
|
||||
}
|
||||
|
||||
func isGlobalSection(name string) bool {
|
||||
switch name {
|
||||
case "loop", "shadow", "frame max", "image path":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isFramePart(name string) bool {
|
||||
switch name {
|
||||
case "image", "image pos", "delay", "image ex":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func parseFrameName(name string) (int32, bool) {
|
||||
if !strings.HasPrefix(name, "frame") || name == "frame max" {
|
||||
return 0, false
|
||||
}
|
||||
number := strings.TrimPrefix(name, "frame")
|
||||
if number == "" {
|
||||
return 0, false
|
||||
}
|
||||
value, err := strconv.ParseInt(number, 10, 32)
|
||||
return int32(value), err == nil && value >= 0
|
||||
}
|
||||
|
||||
func parseBooleanField(target *bool, values []section, name string, issues *[]Issue) {
|
||||
if len(values) == 0 {
|
||||
return
|
||||
}
|
||||
field := values[len(values)-1]
|
||||
if len(field.values) == 0 {
|
||||
*issues = append(*issues, Issue{Severity: IssueWarning, Line: field.line, Section: name, Message: fmt.Sprintf("[%s] 缺少数值,使用 0", name)})
|
||||
return
|
||||
}
|
||||
value, ok := parseInt(field.values[0])
|
||||
if !ok {
|
||||
*issues = append(*issues, Issue{Severity: IssueWarning, Line: field.values[0].line, Section: name, Message: fmt.Sprintf("[%s] 必须是整数,使用 0", name)})
|
||||
return
|
||||
}
|
||||
*target = value != 0
|
||||
if value != 0 && value != 1 {
|
||||
*issues = append(*issues, Issue{Severity: IssueWarning, Line: field.values[0].line, Section: name, Message: fmt.Sprintf("[%s] 只建议使用 0 或 1", name)})
|
||||
}
|
||||
if len(values) > 1 {
|
||||
*issues = append(*issues, Issue{Severity: IssueWarning, Line: field.line, Section: name, Message: fmt.Sprintf("重复的 [%s],使用最后一个值", name)})
|
||||
}
|
||||
}
|
||||
|
||||
func parseNonNegativeInt(value section, name string, issues *[]Issue, fallback int32) int32 {
|
||||
if len(value.values) == 0 {
|
||||
*issues = append(*issues, Issue{Severity: IssueError, Line: value.line, Section: name, Message: fmt.Sprintf("[%s] 缺少数值", name)})
|
||||
return fallback
|
||||
}
|
||||
parsed, ok := parseInt(value.values[0])
|
||||
if !ok || parsed < 0 {
|
||||
*issues = append(*issues, Issue{Severity: IssueError, Line: value.values[0].line, Section: name, Message: fmt.Sprintf("[%s] 必须是非负整数", name)})
|
||||
return fallback
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
func parseImage(value section, issues *[]Issue, frameName string) (Layer, bool) {
|
||||
if len(value.values) == 0 || strings.TrimSpace(value.values[0].value) == "" {
|
||||
*issues = append(*issues, Issue{Severity: IssueWarning, Line: value.line, Section: frameName, Message: "[IMAGE] 图片引用为空,此帧按空图片处理"})
|
||||
return Layer{Index: -1}, true
|
||||
}
|
||||
if len(value.values) < 2 {
|
||||
*issues = append(*issues, Issue{Severity: IssueError, Line: value.line, Section: frameName, Message: "[IMAGE] 需要图片路径和索引"})
|
||||
return Layer{}, false
|
||||
}
|
||||
index, ok := parseInt(value.values[1])
|
||||
if !ok || index < 0 {
|
||||
*issues = append(*issues, Issue{Severity: IssueError, Line: value.values[1].line, Section: "IMAGE", Message: "图片索引必须是非负整数"})
|
||||
return Layer{}, false
|
||||
}
|
||||
return Layer{Path: value.values[0].value, Index: index}, true
|
||||
}
|
||||
|
||||
func parseImageEx(value section, paths []token, issues *[]Issue) (Layer, bool) {
|
||||
if len(value.values) < 2 {
|
||||
*issues = append(*issues, Issue{Severity: IssueError, Line: value.line, Section: "IMAGE EX", Message: "[IMAGE EX] 需要路径索引和图片索引"})
|
||||
return Layer{}, false
|
||||
}
|
||||
pathIndex, ok := parseInt(value.values[0])
|
||||
// -1 marks a frame with no image, matching the binary ANI decoder.
|
||||
// Keep a layer so IMAGE POS and DELAY still apply to this frame.
|
||||
if ok && pathIndex == -1 {
|
||||
return Layer{Index: -1}, true
|
||||
}
|
||||
if !ok || pathIndex < 0 || int64(pathIndex) >= int64(len(paths)) {
|
||||
*issues = append(*issues, Issue{Severity: IssueError, Line: value.values[0].line, Section: "IMAGE EX", Message: "[IMAGE EX] 路径索引必须为 -1(空图片)或 [IMAGE PATH] 范围内的非负整数"})
|
||||
return Layer{}, false
|
||||
}
|
||||
path := paths[pathIndex].value
|
||||
if strings.TrimSpace(path) == "" {
|
||||
*issues = append(*issues, Issue{Severity: IssueError, Line: paths[pathIndex].line, Section: "IMAGE PATH", Message: "[IMAGE PATH] 图片路径不能为空"})
|
||||
return Layer{}, false
|
||||
}
|
||||
index, ok := parseInt(value.values[1])
|
||||
if !ok || index < 0 {
|
||||
*issues = append(*issues, Issue{Severity: IssueError, Line: value.values[1].line, Section: "IMAGE EX", Message: "图片索引必须是非负整数"})
|
||||
return Layer{}, false
|
||||
}
|
||||
return Layer{Path: path, Index: index}, true
|
||||
}
|
||||
|
||||
func parseInt(value token) (int32, bool) {
|
||||
parsed, err := strconv.ParseInt(strings.TrimSpace(value.value), 10, 32)
|
||||
return int32(parsed), err == nil
|
||||
}
|
||||
|
||||
func frameMaxLine(values []section) int {
|
||||
if len(values) == 0 {
|
||||
return 1
|
||||
}
|
||||
return values[len(values)-1].line
|
||||
}
|
||||
|
||||
func frameLine(blocks []frameBlock, index int32) int {
|
||||
for _, block := range blocks {
|
||||
frameIndex, ok := parseFrameName(block.section.name)
|
||||
if ok && frameIndex == index {
|
||||
return block.section.line
|
||||
}
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
func hasErrors(issues []Issue) bool {
|
||||
for _, issue := range issues {
|
||||
if issue.Severity == IssueError {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isSpace(value byte) bool {
|
||||
return value == ' ' || value == '\t' || value == '\r' || value == '\n' || value == '\v' || value == '\f'
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user