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,265 @@
|
||||
// Package ani reads native binary animation resources independently of their
|
||||
// archive container. Format reference: pvfUtility BinaryAniCompiler.cs at
|
||||
// 53dc8e48bba0f7871ca96bcf10bfe6d2028d0582. This is a Go implementation of the
|
||||
// observed binary layout, not a copy of the recovered C# source.
|
||||
package ani
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var ErrBinary = errors.New("ani: invalid binary animation")
|
||||
|
||||
const maxDecodedBytes = 64 << 20
|
||||
|
||||
type binaryReader struct {
|
||||
data []byte
|
||||
pos int
|
||||
err error
|
||||
}
|
||||
|
||||
func (r *binaryReader) take(n int) []byte {
|
||||
if r.err != nil {
|
||||
return nil
|
||||
}
|
||||
if n < 0 || n > len(r.data)-r.pos {
|
||||
r.err = fmt.Errorf("%w: truncated at byte %d", ErrBinary, r.pos)
|
||||
return nil
|
||||
}
|
||||
b := r.data[r.pos : r.pos+n]
|
||||
r.pos += n
|
||||
return b
|
||||
}
|
||||
func (r *binaryReader) u8() byte {
|
||||
b := r.take(1)
|
||||
if b == nil {
|
||||
return 0
|
||||
}
|
||||
return b[0]
|
||||
}
|
||||
func (r *binaryReader) u16() uint16 {
|
||||
b := r.take(2)
|
||||
if b == nil {
|
||||
return 0
|
||||
}
|
||||
return binary.LittleEndian.Uint16(b)
|
||||
}
|
||||
func (r *binaryReader) i16() int16 { return int16(r.u16()) }
|
||||
func (r *binaryReader) i32() int32 {
|
||||
b := r.take(4)
|
||||
if b == nil {
|
||||
return 0
|
||||
}
|
||||
return int32(binary.LittleEndian.Uint32(b))
|
||||
}
|
||||
func (r *binaryReader) float() string {
|
||||
return strconv.FormatFloat(float64(math.Float32frombits(uint32(r.i32()))), 'g', -1, 32)
|
||||
}
|
||||
func (r *binaryReader) ascii() string {
|
||||
n := r.i32()
|
||||
if n > 1<<20 {
|
||||
r.err = fmt.Errorf("%w: resource string exceeds limit", ErrBinary)
|
||||
return ""
|
||||
}
|
||||
b := r.take(int(n))
|
||||
if b == nil {
|
||||
return ""
|
||||
}
|
||||
out := make([]byte, len(b))
|
||||
for i, v := range b {
|
||||
if v > 127 {
|
||||
v = '?'
|
||||
}
|
||||
out[i] = v
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
func (r *binaryReader) fail(context string, id uint16) {
|
||||
if r.err == nil {
|
||||
r.err = fmt.Errorf("%w: unknown %s field %d at byte %d", ErrBinary, context, id, r.pos-2)
|
||||
}
|
||||
}
|
||||
|
||||
var effects = []string{"NONE", "DODGE", "LINEARDODGE", "DARK", "XOR", "MONOCHROME", "SPACEDISTORT"}
|
||||
var damageTypes = []string{"NORMAL", "SUPERARMOR", "UNBREAKABLE"}
|
||||
var flips = []string{"0", "HORIZON", "VERTICAL", "ALL"}
|
||||
|
||||
func enumName(n uint16, names []string) string {
|
||||
if int(n) < len(names) {
|
||||
return names[n]
|
||||
}
|
||||
return strconv.Itoa(int(n))
|
||||
}
|
||||
func quote(s string) string { return "`" + strings.ReplaceAll(s, "`", "``") + "`" }
|
||||
func field(out *strings.Builder, name string, values ...any) {
|
||||
fmt.Fprintf(out, "[%s]\n", name)
|
||||
if len(values) > 0 {
|
||||
out.WriteByte('\t')
|
||||
for i, v := range values {
|
||||
if i > 0 {
|
||||
out.WriteByte('\t')
|
||||
}
|
||||
fmt.Fprint(out, v)
|
||||
}
|
||||
out.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
|
||||
// DecodeBinary returns source ANI text for the native frame/path-table format.
|
||||
// It checks all reads and rejects unknown fields rather than losing sync or
|
||||
// silently dropping data. Up to three zero alignment bytes are accepted.
|
||||
func DecodeBinary(data []byte) (string, error) {
|
||||
r := &binaryReader{data: data}
|
||||
frames, paths := int(r.u16()), int(r.u16())
|
||||
if r.err != nil {
|
||||
return "", r.err
|
||||
}
|
||||
if paths > (len(data)-r.pos)/4 {
|
||||
return "", fmt.Errorf("%w: path count exceeds input", ErrBinary)
|
||||
}
|
||||
images := make([]string, paths)
|
||||
for i := range images {
|
||||
images[i] = r.ascii()
|
||||
if r.err != nil {
|
||||
return "", r.err
|
||||
}
|
||||
}
|
||||
var out strings.Builder
|
||||
out.WriteString("#PVF_File\n\n")
|
||||
globals := int(r.u16())
|
||||
for i := 0; i < globals && r.err == nil; i++ {
|
||||
id := r.u16()
|
||||
switch id {
|
||||
case 0:
|
||||
field(&out, "LOOP", r.u8())
|
||||
case 1:
|
||||
field(&out, "SHADOW", r.u8())
|
||||
case 3:
|
||||
field(&out, "COORD", r.u16())
|
||||
case 28:
|
||||
field(&out, "OPERATION", r.u16())
|
||||
case 18:
|
||||
field(&out, "SPECTRUM", r.u8())
|
||||
field(&out, "SPECTRUM TERM", r.i32())
|
||||
field(&out, "SPECTRUM LIFE TIME", r.i32())
|
||||
field(&out, "SPECTRUM COLOR", r.u8(), r.u8(), r.u8(), r.u8())
|
||||
field(&out, "SPECTRUM EFFECT", quote(enumName(r.u16(), effects)))
|
||||
default:
|
||||
r.fail("global", id)
|
||||
}
|
||||
}
|
||||
if r.err != nil {
|
||||
return "", r.err
|
||||
}
|
||||
if frames > (len(data)-r.pos)/14 {
|
||||
return "", fmt.Errorf("%w: frame count exceeds input", ErrBinary)
|
||||
}
|
||||
field(&out, "FRAME MAX", frames)
|
||||
for i := 0; i < frames && r.err == nil; i++ {
|
||||
out.WriteByte('\n')
|
||||
field(&out, fmt.Sprintf("FRAME%03d", i))
|
||||
var frameOut, boxes strings.Builder
|
||||
count := int(r.u16())
|
||||
for j := 0; j < count && r.err == nil; j++ {
|
||||
id := r.u16()
|
||||
name := "DAMAGE BOX"
|
||||
if id == 15 {
|
||||
name = "ATTACK BOX"
|
||||
} else if id != 14 {
|
||||
r.fail("box", id)
|
||||
break
|
||||
}
|
||||
field(&boxes, name, r.i32(), r.i32(), r.i32(), r.i32(), r.i32(), r.i32())
|
||||
}
|
||||
if r.err != nil {
|
||||
return "", r.err
|
||||
}
|
||||
image := int(r.i16())
|
||||
if image >= 0 {
|
||||
if image >= len(images) {
|
||||
return "", fmt.Errorf("%w: frame %d image path index %d exceeds %d", ErrBinary, i, image, len(images))
|
||||
}
|
||||
field(&frameOut, "IMAGE", quote(images[image]), r.u16())
|
||||
} else {
|
||||
field(&frameOut, "IMAGE", quote(""), 0)
|
||||
}
|
||||
field(&frameOut, "IMAGE POS", r.i32(), r.i32())
|
||||
count = int(r.u16())
|
||||
for j := 0; j < count && r.err == nil; j++ {
|
||||
id := r.u16()
|
||||
switch id {
|
||||
case 0:
|
||||
field(&frameOut, "LOOP", r.u8())
|
||||
case 1:
|
||||
field(&frameOut, "SHADOW", r.u8())
|
||||
case 3:
|
||||
field(&frameOut, "COORD", r.u16())
|
||||
case 7:
|
||||
field(&frameOut, "IMAGE RATE", r.float(), r.float())
|
||||
case 8:
|
||||
field(&frameOut, "IMAGE ROTATE", r.float())
|
||||
case 9:
|
||||
field(&frameOut, "RGBA", r.u8(), r.u8(), r.u8(), r.u8())
|
||||
case 10:
|
||||
field(&frameOut, "INTERPOLATION", r.u8())
|
||||
case 11:
|
||||
effect := r.u16()
|
||||
field(&frameOut, "GRAPHIC EFFECT", quote(enumName(effect, effects)))
|
||||
if effect == 5 {
|
||||
fmt.Fprintf(&frameOut, "\t%d\t%d\t%d\n", r.u8(), r.u8(), r.u8())
|
||||
}
|
||||
if effect == 6 {
|
||||
fmt.Fprintf(&frameOut, "\t%d\t%d\n", r.i16(), r.i16())
|
||||
}
|
||||
case 12:
|
||||
field(&frameOut, "DELAY", r.i32())
|
||||
case 13:
|
||||
field(&frameOut, "DAMAGE TYPE", quote(enumName(r.u16(), damageTypes)))
|
||||
case 16:
|
||||
field(&frameOut, "PLAY SOUND", quote(r.ascii()))
|
||||
case 17:
|
||||
field(&frameOut, "PRELOAD", 1)
|
||||
case 23:
|
||||
field(&frameOut, "SET FLAG", r.i32())
|
||||
case 24:
|
||||
field(&frameOut, "FLIP TYPE", quote(enumName(r.u16(), flips)))
|
||||
case 25:
|
||||
field(&frameOut, "LOOP START")
|
||||
case 26:
|
||||
field(&frameOut, "LOOP END", r.i32())
|
||||
case 27:
|
||||
field(&frameOut, "CLIP", r.i16(), r.i16(), r.i16(), r.i16())
|
||||
default:
|
||||
r.fail("frame", id)
|
||||
}
|
||||
}
|
||||
if out.Len()+frameOut.Len()+boxes.Len() > maxDecodedBytes {
|
||||
return "", fmt.Errorf("%w: decoded source exceeds limit", ErrBinary)
|
||||
}
|
||||
frameOut.WriteString(boxes.String())
|
||||
for _, line := range strings.SplitAfter(frameOut.String(), "\n") {
|
||||
if line != "" {
|
||||
out.WriteByte('\t')
|
||||
out.WriteString(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
if r.err != nil {
|
||||
return "", r.err
|
||||
}
|
||||
tail := data[r.pos:]
|
||||
if len(tail) > 3 {
|
||||
return "", fmt.Errorf("%w: %d trailing bytes", ErrBinary, len(tail))
|
||||
}
|
||||
for _, b := range tail {
|
||||
if b != 0 {
|
||||
return "", fmt.Errorf("%w: nonzero trailing byte", ErrBinary)
|
||||
}
|
||||
}
|
||||
return out.String(), nil
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package ani
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"pvfine/internal/preview"
|
||||
)
|
||||
|
||||
func binaryFixture() []byte {
|
||||
var b bytes.Buffer
|
||||
put := func(v any) {
|
||||
if err := binary.Write(&b, binary.LittleEndian, v); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
put(uint16(2))
|
||||
put(uint16(1))
|
||||
image := "Monster/test.img"
|
||||
put(int32(len(image)))
|
||||
b.WriteString(image)
|
||||
put(uint16(5))
|
||||
put(uint16(0))
|
||||
put(byte(1))
|
||||
put(uint16(1))
|
||||
put(byte(1))
|
||||
put(uint16(3))
|
||||
put(uint16(2))
|
||||
put(uint16(28))
|
||||
put(uint16(8))
|
||||
put(uint16(18))
|
||||
put(byte(1))
|
||||
put(int32(30))
|
||||
put(int32(100))
|
||||
b.Write([]byte{1, 2, 3, 255})
|
||||
put(uint16(2))
|
||||
put(uint16(2)) // two collision boxes
|
||||
for _, id := range []uint16{14, 15} {
|
||||
put(id)
|
||||
for _, n := range []int32{-1, -2, -3, 4, 5, 6} {
|
||||
put(n)
|
||||
}
|
||||
}
|
||||
put(int16(0))
|
||||
put(uint16(8))
|
||||
put(int32(-12))
|
||||
put(int32(34))
|
||||
put(uint16(19))
|
||||
put(uint16(0))
|
||||
put(byte(0))
|
||||
put(uint16(1))
|
||||
put(byte(0))
|
||||
put(uint16(3))
|
||||
put(uint16(1))
|
||||
put(uint16(7))
|
||||
put(float32(1.5))
|
||||
put(float32(0.75))
|
||||
put(uint16(8))
|
||||
put(float32(-12.5))
|
||||
put(uint16(9))
|
||||
b.Write([]byte{255, 128, 64, 32})
|
||||
put(uint16(10))
|
||||
put(byte(1))
|
||||
put(uint16(11))
|
||||
put(uint16(5))
|
||||
b.Write([]byte{7, 8, 9})
|
||||
put(uint16(11))
|
||||
put(uint16(6))
|
||||
put(int16(-10))
|
||||
put(int16(20))
|
||||
put(uint16(12))
|
||||
put(int32(80))
|
||||
put(uint16(13))
|
||||
put(uint16(1))
|
||||
put(uint16(16))
|
||||
put(int32(9))
|
||||
b.WriteString("test`wave")
|
||||
put(uint16(17))
|
||||
put(uint16(23))
|
||||
put(int32(123))
|
||||
put(uint16(24))
|
||||
put(uint16(3))
|
||||
put(uint16(25))
|
||||
put(uint16(26))
|
||||
put(int32(4))
|
||||
put(uint16(27))
|
||||
for _, n := range []int16{-1, -2, 10, 20} {
|
||||
put(n)
|
||||
}
|
||||
put(uint16(13))
|
||||
put(uint16(99)) // unknown enum values still have a known width
|
||||
put(uint16(0))
|
||||
put(int16(-1))
|
||||
put(int32(0))
|
||||
put(int32(0))
|
||||
put(uint16(1))
|
||||
put(uint16(12))
|
||||
put(int32(40))
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
func TestDecodeBinaryAllFields(t *testing.T) {
|
||||
input := binaryFixture()
|
||||
before := bytes.Clone(input)
|
||||
text, err := DecodeBinary(input)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, fragment := range []string{
|
||||
"[FRAME MAX]\n\t2", "[IMAGE]\n\t`Monster/test.img`\t8", "[IMAGE POS]\n\t-12\t34",
|
||||
"[IMAGE RATE]\n\t1.5\t0.75", "[IMAGE ROTATE]\n\t-12.5", "[RGBA]\n\t255\t128\t64\t32",
|
||||
"[SPECTRUM TERM]\n\t30", "[SPECTRUM COLOR]\n\t1\t2\t3\t255", "[SPECTRUM EFFECT]\n\t`LINEARDODGE`",
|
||||
"[GRAPHIC EFFECT]\n\t`MONOCHROME`\n\t7\t8\t9", "[GRAPHIC EFFECT]\n\t`SPACEDISTORT`\n\t-10\t20",
|
||||
"[DAMAGE TYPE]\n\t`SUPERARMOR`", "[DAMAGE TYPE]\n\t`99`", "[PLAY SOUND]\n\t`test``wave`",
|
||||
"[FLIP TYPE]\n\t`ALL`", "[LOOP START]\n", "[LOOP END]\n\t4", "[CLIP]\n\t-1\t-2\t10\t20",
|
||||
"[DAMAGE BOX]\n\t-1\t-2\t-3\t4\t5\t6", "[ATTACK BOX]\n\t-1\t-2\t-3\t4\t5\t6",
|
||||
"[FRAME001]\n[IMAGE]\n\t``\t0", "[DELAY]\n\t40", "[PRELOAD]\n\t1", "[SET FLAG]\n\t123",
|
||||
} {
|
||||
unindent := func(s string) string {
|
||||
lines := strings.Split(s, "\n")
|
||||
for i := range lines {
|
||||
lines[i] = strings.TrimLeft(lines[i], "\t")
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
if !strings.Contains(unindent(text), unindent(fragment)) {
|
||||
t.Fatalf("missing decoded field %q", fragment)
|
||||
}
|
||||
}
|
||||
if !bytes.Equal(input, before) {
|
||||
t.Fatal("decoder changed input")
|
||||
}
|
||||
doc := preview.ParseANI(text)
|
||||
if !doc.Valid || !doc.Loop || !doc.Shadow || len(doc.Frames) != 2 || doc.Frames[0].Delay != 80 || len(doc.Frames[0].Layers) != 1 || doc.Frames[0].Layers[0].X != -12 || len(doc.Frames[1].Layers) != 1 || doc.Frames[1].Layers[0].Path != "" {
|
||||
t.Fatalf("preview mismatch: %+v", doc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeBinaryRejectsInvalidData(t *testing.T) {
|
||||
original := binaryFixture()
|
||||
for n := 0; n < len(original); n++ {
|
||||
if _, err := DecodeBinary(original[:n]); !errors.Is(err, ErrBinary) {
|
||||
t.Fatalf("truncated length %d: %v", n, err)
|
||||
}
|
||||
}
|
||||
for _, input := range [][]byte{
|
||||
{1, 0, 0, 0, 1, 0, 99, 0}, // unknown global
|
||||
{1, 0, 1, 0, 255, 255, 255, 255}, // negative path length
|
||||
{1, 0, 255, 255, 0, 0}, // path count out of bounds
|
||||
{255, 255, 0, 0, 0, 0}, // frame count out of bounds
|
||||
append(bytes.Clone(original), 0, 0, 0, 0), // unexplained tail
|
||||
append(bytes.Clone(original), 1),
|
||||
} {
|
||||
if _, err := DecodeBinary(input); !errors.Is(err, ErrBinary) {
|
||||
t.Fatal("invalid data accepted", err)
|
||||
}
|
||||
}
|
||||
for n := 0; n <= 3; n++ {
|
||||
if _, err := DecodeBinary(append(bytes.Clone(original), make([]byte, n)...)); err != nil {
|
||||
t.Fatal("alignment padding rejected", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeBinaryBadFrameFields(t *testing.T) {
|
||||
// One empty-image frame; exercise the variable-length sections separately.
|
||||
prefix := []byte{1, 0, 0, 0, 0, 0}
|
||||
for _, frame := range [][]byte{
|
||||
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // missing image path
|
||||
{1, 0, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // unknown collision field
|
||||
{0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 99, 0}, // unknown frame field
|
||||
} {
|
||||
if _, err := DecodeBinary(append(bytes.Clone(prefix), frame...)); !errors.Is(err, ErrBinary) {
|
||||
t.Fatal("bad frame accepted", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzDecodeBinary(f *testing.F) {
|
||||
f.Add(binaryFixture())
|
||||
f.Add([]byte{0, 0, 0, 0, 0, 0})
|
||||
f.Add([]byte{255, 255, 255, 255})
|
||||
f.Fuzz(func(t *testing.T, data []byte) { _, _ = DecodeBinary(data) })
|
||||
}
|
||||
Reference in new issue
Block a user