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

436 lines
13 KiB
Go

package pvf
import (
"bytes"
"context"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"io"
"os"
"strings"
"testing"
"golang.org/x/text/encoding/traditionalchinese"
)
func twFixture(t *testing.T) []byte {
t.Helper()
return twFixtureWithScript(t, nil)
}
func twFixtureWithScript(t *testing.T, nativeScript []byte) []byte {
t.Helper()
encode := func(s string) []byte {
b, e := traditionalchinese.Big5.NewEncoder().Bytes([]byte(s))
if e != nil {
t.Fatal(e)
}
return b
}
values := []string{"[name]", "測試裝備", "[rarity]", "text/test.str", "equip_name"}
table := make([]byte, 4+(len(values)+1)*4)
binary.LittleEndian.PutUint32(table, uint32(len(values)))
for i, s := range values {
binary.LittleEndian.PutUint32(table[4+i*4:], uint32(len(table)-4))
table = append(table, encode(s)...)
}
binary.LittleEndian.PutUint32(table[4+len(values)*4:], uint32(len(table)-4))
// Native TW numbering and two-byte script prefix, including reference pairs.
script := []byte{0xb0, 0xd0, 5, 0, 0, 0, 0, 7, 1, 0, 0, 0, 5, 2, 0, 0, 0, 2, 4, 0, 0, 0}
// Legacy writers sometimes declare padding as part of the script.
script = append(script, 0, 0)
if nativeScript != nil {
script = nativeScript
}
reference := []byte{0xb0, 0xd0, 5, 0, 0, 0, 0, 9, 0, 0, 0, 0, 10, 4, 0, 0, 0}
list := []byte{0xb0, 0xd0, 2, 0, 0, 0, 0, 7, 3, 0, 0, 0}
var tree, body bytes.Buffer
paths := []string{"stringtable.bin", "equipment/test.equ", "equipment/ref.equ", "n_string.lst", "text/test.str", "etc/raw.bin", "etc/empty.bin", "sqr/common.nut"}
rawFiles := [][]byte{table, script, reference, list, encode("equip_name>繁體名稱\r\n"), {0, 1, 2, 3, 255}, {}, encode("// 繁體註解\r\nfunction test() { return 1; }\r\n")}
for i, path := range paths {
number := uint32(i + 7)
enc, crc := twEncrypt(rawFiles[i], number)
if len(rawFiles[i]) == 0 {
crc = 0
}
binary.Write(&tree, binary.LittleEndian, number)
binary.Write(&tree, binary.LittleEndian, uint32(len(path)))
tree.WriteString(path)
for _, v := range []uint32{uint32(len(rawFiles[i])), crc, uint32(body.Len())} {
binary.Write(&tree, binary.LittleEndian, v)
}
body.Write(enc)
}
enc, crc := twEncrypt(tree.Bytes(), uint32(len(paths)))
var out bytes.Buffer
binary.Write(&out, binary.LittleEndian, uint32(36))
out.WriteString("00000000-0000-0000-0000-000000000000")
for _, v := range []uint32{48984, uint32(len(enc)), crc, uint32(len(paths))} {
binary.Write(&out, binary.LittleEndian, v)
}
out.Write(enc)
out.Write(body.Bytes())
return out.Bytes()
}
func TestTWOpaqueNumericTokenRoundTrip(t *testing.T) {
native := []byte{0xb0, 0xd0, 5, 2, 0, 0, 0}
for _, v := range []uint32{0, 1, 0x7fffffff, 0x80000000, 0xffffffff} {
native = append(native, 3, 0, 0, 0, 0)
binary.LittleEndian.PutUint32(native[len(native)-4:], v)
}
original := twFixtureWithScript(t, native)
a, err := Parse(original)
if err != nil {
t.Fatal(err)
}
defer a.Release()
i, _ := a.Find("equipment/test.equ")
raw, err := a.RawBytes(i)
if err != nil {
t.Fatal(err)
}
for p := 5; p < len(raw); p += 5 {
if raw[p] != 1 || !bytes.Equal(raw[p+1:p+5], native[p+3:p+7]) {
t.Fatal("native token type/value not preserved")
}
}
var untouched bytes.Buffer
if err := a.SaveTo(&untouched); err != nil || !bytes.Equal(original, untouched.Bytes()) {
t.Fatal("untouched archive changed")
}
text, err := a.Text(i)
if err != nil || !strings.Contains(text, "{1=-2147483648}") || !strings.Contains(text, "{1=-1}") {
t.Fatal("opaque tokens not rendered losslessly")
}
reader := a.NewFileComparisonReader()
fp, err := reader.CanonicalFingerprint(context.Background(), i, raw)
if err != nil {
t.Fatal(err)
}
changed := bytes.Clone(raw)
changed[6]++
next, err := reader.CanonicalFingerprint(context.Background(), i, changed)
if err != nil || fp.Digest == next.Digest {
t.Fatal("comparison ignored opaque token value")
}
if err := a.SetText(i, text); err != nil {
t.Fatal(err)
}
var saved bytes.Buffer
if err := a.SaveTo(&saved); err != nil {
t.Fatal(err)
}
b, err := Parse(saved.Bytes())
if err != nil {
t.Fatal(err)
}
defer b.Release()
i, _ = b.Find("equipment/test.equ")
got, err := b.tw.payload(b.data, i)
if err != nil || !bytes.Equal(native, got) {
t.Fatal("native token bytes changed after text edit/save/reopen")
}
}
func TestTWRoundTrip(t *testing.T) {
original := twFixture(t)
a, err := Parse(original)
if err != nil {
t.Fatal(err)
}
if a.Format() != FormatTW || a.ClientVersion() != "TW" || a.Modified() {
t.Fatal("incorrect TW detection/state")
}
if cap := a.WriteCapabilities(); !cap.CanSave || !cap.CanChangeStructure {
t.Fatal(cap)
}
var unchanged bytes.Buffer
if err := a.SaveTo(&unchanged); err != nil {
t.Fatal(err)
}
if !bytes.Equal(original, unchanged.Bytes()) {
t.Fatal("untouched TW changed")
}
i, _ := a.Find("equipment/test.equ")
text, err := a.Text(i)
if err != nil || !strings.Contains(text, "測試裝備") || !strings.Contains(text, "[rarity]") {
t.Fatalf("TW script: %q %v", text, err)
}
ref, _ := a.Find("equipment/ref.equ")
if name, ok := a.ItemName(ref); !ok || name != "繁體名稱" {
t.Fatalf("TW name reference: %q %v", name, ok)
}
rawIndex, _ := a.Find("etc/raw.bin")
raw, _ := a.RawBytes(rawIndex)
if !bytes.Equal(raw, []byte{0, 1, 2, 3, 255}) {
t.Fatal("binary/padding mismatch")
}
if err := a.SetText(i, "[name]\n`修改裝備`\n[rarity]\n5"); err != nil {
t.Fatal(err)
}
if _, err := a.AddFileText("equipment/new.equ", "[name]\n`新增裝備`", TypeScript); err != nil {
t.Fatal(err)
}
if err := a.SetText(ref, "[name]\n{8=`<0::equip_name>`}\n[rarity]\n3"); err != nil {
t.Fatal(err)
}
str, _ := a.Find("text/test.str")
if err := a.SetText(str, "equip_name>更新名稱\r\n"); err != nil {
t.Fatal(err)
}
if _, err := a.RemoveFiles([]int32{rawIndex}); err != nil {
t.Fatal(err)
}
clone := a.CloneForBatch()
var edited bytes.Buffer
if err := clone.SaveTo(&edited); err != nil {
t.Fatal(err)
}
if !a.Modified() || clone.Modified() {
t.Fatal("clone save changed live state")
}
b, err := Parse(edited.Bytes())
if err != nil {
t.Fatal(err)
}
for path, want := range map[string]string{"equipment/test.equ": "修改裝備", "equipment/new.equ": "新增裝備", "text/test.str": "更新名稱"} {
i, ok := b.Find(path)
if !ok {
t.Fatal("missing", path)
}
text, err := b.Text(i)
if err != nil || !strings.Contains(text, want) {
t.Fatalf("%s: %q %v", path, text, err)
}
}
if _, ok := b.Find("etc/raw.bin"); ok {
t.Fatal("removed file remains")
}
ref, _ = b.Find("equipment/ref.equ")
if name, ok := b.ItemName(ref); !ok || name != "更新名稱" {
t.Fatalf("saved reference: %q %v", name, ok)
}
if err := b.SetText(ref, "[name]\n`再次修改`"); err != nil {
t.Fatal(err)
}
identityBeforeSave := b.FileIdentity(ref)
if err := b.SaveTo(io.Discard); err != nil {
t.Fatal(err)
}
if b.FileIdentity(ref) != identityBeforeSave {
t.Fatal("TW save changed surviving entry identity")
}
if b.Modified() {
t.Fatal("save did not adopt TW state")
}
visited := 0
if err := b.ForEachRawFile(context.Background(), func(index int32, path string, file File, raw []byte) bool {
visited++
if int32(len(raw)) != file.DataSize {
t.Errorf("logical size mismatch: %s", path)
}
return true
}); err != nil {
t.Fatal(err)
}
if visited != int(b.FileCount()) {
t.Fatal("scan missed files")
}
}
func TestTWNutTextRoundTrip(t *testing.T) {
a, err := Parse(twFixture(t))
if err != nil {
t.Fatal(err)
}
i, ok := a.Find("sqr/common.nut")
if !ok || a.File(i).DataType != TypeUnicode {
t.Fatal("NUT source is not editable text")
}
original := "// 繁體註解\r\nfunction test() { return 1; }\r\n"
if text, err := a.Text(i); err != nil || text != original {
t.Fatalf("NUT read mismatch: %v", err)
}
edited := "// 修改註解\r\nfunction test() { return 2; }\r\n"
if err := a.SetText(i, edited); err != nil {
t.Fatal(err)
}
if _, err := a.AddFileText("sqr/new.nut", "function added() { return 3; }", TypeUnicode); err != nil {
t.Fatal(err)
}
var out bytes.Buffer
if err := a.SaveTo(&out); err != nil {
t.Fatal(err)
}
b, err := Parse(out.Bytes())
if err != nil {
t.Fatal(err)
}
i, _ = b.Find("sqr/common.nut")
if text, err := b.Text(i); err != nil || text != edited {
t.Fatalf("NUT save/reopen mismatch: %v", err)
}
raw, err := b.tw.payload(b.data, b.items[i].chunk)
if err != nil {
t.Fatal(err)
}
expected, err := traditionalchinese.Big5.NewEncoder().Bytes([]byte(edited))
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(raw, expected) {
t.Fatal("NUT was saved as tokens or UTF-16 instead of Big5 source")
}
i, ok = b.Find("sqr/new.nut")
if !ok || b.File(i).DataType != TypeUnicode {
t.Fatal("new NUT is not editable")
}
if text, err := b.Text(i); err != nil || text != "function added() { return 3; }" {
t.Fatal("new NUT source changed")
}
}
func TestTWRejectMalformed(t *testing.T) {
fixture := twFixture(t)
for _, n := range []int{0, 4, 48, 55, len(fixture) - 1} {
if _, err := Parse(fixture[:n]); err == nil {
t.Fatalf("accepted truncated TW at %d", n)
}
}
corrupt := bytes.Clone(fixture)
corrupt[60] ^= 1
if _, err := Parse(corrupt); err == nil {
t.Fatal("accepted corrupt directory")
}
corrupt = bytes.Clone(fixture)
corrupt[len(corrupt)-1] ^= 1
if _, err := Parse(corrupt); err == nil {
t.Fatal("accepted corrupt payload")
}
a, err := Parse(fixture)
if err != nil {
t.Fatal(err)
}
i, _ := a.Find("equipment/test.equ")
if err := a.SetText(i, "[name]\n`😀`"); err != nil {
t.Fatal(err)
}
var out bytes.Buffer
if err := a.SaveTo(&out); err == nil {
t.Fatal("accepted text outside Big5")
}
if out.Len() != 0 || !a.Modified() {
t.Fatal("failed save mutated state/output")
}
}
func TestTWRealArchive(t *testing.T) {
path := os.Getenv("PVF_TW_TESTFILE")
if path == "" {
t.Skip("PVF_TW_TESTFILE not set")
}
a, err := Open(path)
if err != nil {
t.Fatal(err)
}
defer a.Release()
if a.Format() != FormatTW || a.FileCount() == 0 {
t.Fatal("TW not recognized")
}
t.Logf("TW: %d files, %d strings", a.FileCount(), len(a.tw.strings))
var target int32 = -1
for i, it := range a.items {
if it.typ == TypeScript && strings.HasSuffix(a.Path(int32(i)), ".equ") {
target = int32(i)
break
}
}
if target < 0 {
t.Fatal("no equipment script")
}
nutIndex, ok := a.Find("sqr/common.nut")
if !ok || a.File(nutIndex).DataType != TypeUnicode {
t.Fatal("real NUT is not editable")
}
nutRaw, err := a.tw.payload(a.data, a.items[nutIndex].chunk)
if err != nil {
t.Fatal(err)
}
nutText, err := a.Text(nutIndex)
if err != nil || nutText != twDecodeText(nutRaw) {
t.Fatal("real NUT source decode mismatch")
}
text, err := a.Text(target)
if err != nil || !strings.Contains(text, "[name]") {
t.Fatalf("equipment read: %v", err)
}
hash := sha256.New()
if err := a.SaveTo(hash); err != nil {
t.Fatal(err)
}
if hex.EncodeToString(hash.Sum(nil)) != a.SourceHash() {
t.Fatal("untouched real TW bytes changed")
}
if err := a.ForEachRawFile(context.Background(), func(index int32, path string, file File, raw []byte) bool {
if int32(len(raw)) != file.DataSize {
t.Errorf("TW logical size mismatch at entry %d", index)
}
return true
}); err != nil {
t.Fatal(err)
}
editedNut := nutText + "\r\n// TW NUT regression\r\n"
if err := a.SetText(nutIndex, editedNut); err != nil {
t.Fatal(err)
}
if err := a.SetText(target, text+"\n[zz tw regression]\n123"); err != nil {
t.Fatal(err)
}
if _, err := a.AddFileText("etc/zz_tw_test.equ", "[name]\n`繁體測試`", TypeScript); err != nil {
t.Fatal(err)
}
var out bytes.Buffer
if err := a.SaveTo(&out); err != nil {
t.Fatal(err)
}
b, err := Parse(out.Bytes())
if err != nil {
t.Fatal(err)
}
defer b.Release()
nutIndex, ok = b.Find("sqr/common.nut")
if !ok {
t.Fatal("real NUT missing after save")
}
if text, err := b.Text(nutIndex); err != nil || text != editedNut {
t.Fatal("real NUT save/reopen mismatch")
}
nutRaw, err = b.tw.payload(b.data, b.items[nutIndex].chunk)
if err != nil {
t.Fatal(err)
}
expectedNut, err := traditionalchinese.Big5.NewEncoder().Bytes([]byte(editedNut))
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(nutRaw, expectedNut) {
t.Fatal("real NUT native encoding changed")
}
i, ok := b.Find("etc/zz_tw_test.equ")
if !ok {
t.Fatal("added file missing")
}
if text, err := b.Text(i); err != nil || !strings.Contains(text, "繁體測試") {
t.Fatalf("edited TW reopen: %v", err)
}
i, ok = b.Find(a.Path(target))
if !ok {
t.Fatal("edited equipment missing")
}
if text, err := b.Text(i); err != nil || !strings.Contains(text, "[zz tw regression]") {
t.Fatalf("equipment edit reopen: %v", err)
}
}