Files
90CNServer/server/cmd/scratch/pvfstruct/main.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

271 lines
8.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// pvfstruct 打印 protected PVF 的容器结构:header 各段尺寸、HASH 段样本、
// 文件表原始字段与排序状况(只读)。
//
// 动机:客户端可能不是线性扫文件表,而是按 HASH 段或按「排序后的表」做查找。
// 若新增条目被追加到表尾而表本来是排序的,查找就会失效(新增道具查不到 → 邮件附件空白)。
//
// go run ./cmd/scratch/pvfstruct -pvf <Script.pvf> [-tail 16] [-hashdump 256]
package main
import (
"encoding/binary"
"flag"
"fmt"
"os"
"sort"
"strings"
"unicode/utf16"
platformpvf "longheng.io/server/internal/platform/pvf"
)
const (
headerSize = 0x30
fileItemSize = 0x18
groupItemSize = 8
magicSignature = 0x69706B6E
)
func decryptProtectedCore(key string, buf []byte, magic uint32) {
if key == "" || len(buf) == 0 {
return
}
words := utf16.Encode([]rune(key))
if len(words) < 4 {
return
}
size := len(buf)
tail := size
seed := uint32(0x339E9711)*uint32(words[0]) +
0x393*(uint32(words[3])+0x393*(uint32(words[2])+0x393*uint32(words[1])))
if size >= 4 {
quadCount := size >> 2
tail = size - (quadCount << 2)
for idx := 0; idx < quadCount; idx++ {
t1 := 0x343FD*seed + magic
seed = 0x343FD*t1 + magic
xorKey := (t1 & 0xFFFF0000) + (seed >> 16)
off := idx << 2
binary.LittleEndian.PutUint32(buf[off:off+4], binary.LittleEndian.Uint32(buf[off:off+4])^xorKey)
}
}
if tail > 0 {
t1 := 0x343FD*seed + magic
t2 := 0x343FD*t1 + magic
finalKey := (t1 & 0xFFFF0000) + (t2 >> 16)
var kb [4]byte
binary.LittleEndian.PutUint32(kb[:], finalKey)
start := size - tail
for idx := 0; idx < tail; idx++ {
buf[start+idx] ^= kb[idx]
}
}
}
func i32(b []byte) int { return int(int32(binary.LittleEndian.Uint32(b))) }
func main() {
pvfPath := flag.String("pvf", "", "Script.pvf 路径")
tail := flag.Int("tail", 16, "打印文件表尾部多少条")
hashDump := flag.Int("hashdump", 256, "打印 HASH 段前多少字节")
order := flag.Int("order", 12, "打印前多少个排序破坏点")
rng := flag.String("range", "", "dump 文件表某区间,格式 from:to")
flag.Parse()
if *pvfPath == "" {
fmt.Fprintln(os.Stderr, "-pvf is required")
os.Exit(2)
}
data, err := os.ReadFile(*pvfPath)
if err != nil {
fmt.Fprintln(os.Stderr, "read:", err)
os.Exit(1)
}
fmt.Printf("文件: %s\n大小: %d 字节\n", *pvfPath, len(data))
hdr := append([]byte(nil), data[:headerSize]...)
decryptProtectedCore("hEAd", hdr, 0x269EC3)
sig := binary.LittleEndian.Uint32(hdr[0:4])
fmt.Printf("\n=== header ===\nmagic: 0x%08X (%s)\n", sig, map[bool]string{true: "nkpi ✅", false: "不匹配 ❌"}[sig == magicSignature])
fileCount := i32(hdr[24:28])
bodySize := i32(hdr[32:36])
groupCount := i32(hdr[36:40])
hashSize := i32(hdr[40:44])
nameSize := i32(hdr[44:48])
fmt.Printf("fileCount = %d\n", fileCount)
fmt.Printf("bodySize = %d\n", bodySize)
fmt.Printf("groupCount = %d\n", groupCount)
fmt.Printf("hashSize = %d\n", hashSize)
fmt.Printf("nameSize = %d\n", nameSize)
tableOff := headerSize
hashOff := tableOff + fileCount*fileItemSize
nameOff := hashOff + hashSize
groupOff := nameOff + nameSize
bodyOff := groupOff + groupCount*groupItemSize
fmt.Printf("\n=== 段偏移 ===\n文件表: %d .. %d (%d 项 × 0x18)\n", tableOff, hashOff, fileCount)
fmt.Printf("HASH : %d .. %d (%d 字节)\n", hashOff, nameOff, hashSize)
fmt.Printf("字符串: %d .. %d (%d 字节)\n", nameOff, groupOff, nameSize)
fmt.Printf("组表 : %d .. %d (%d 项 × 8)\n", groupOff, bodyOff, groupCount)
fmt.Printf("body : %d .. %d (声明 %d)\n", bodyOff, bodyOff+bodySize, bodySize)
fmt.Printf("段之和 = %d,实际文件 = %d -> %s\n", bodyOff+bodySize, len(data),
map[bool]string{true: "一致 ✅", false: "不一致 ❌"}[bodyOff+bodySize == len(data)])
// ---- HASH 段样本 ----
if hashSize > 0 {
n := *hashDump
if n > hashSize {
n = hashSize
}
fmt.Printf("\n=== HASH 段前 %d 字节(原样,未解密)===\n", n)
for off := 0; off < n; off += 16 {
end := off + 16
if end > n {
end = n
}
var hexPart, asciiPart strings.Builder
for i := off; i < end; i++ {
fmt.Fprintf(&hexPart, "%02X ", data[hashOff+i])
if c := data[hashOff+i]; c >= 32 && c < 127 {
asciiPart.WriteByte(c)
} else {
asciiPart.WriteByte('.')
}
}
fmt.Printf(" %08X %-48s %s\n", off, hexPart.String(), asciiPart.String())
}
// 全零检查
allZero := true
for i := 0; i < hashSize; i++ {
if data[hashOff+i] != 0 {
allZero = false
break
}
}
fmt.Printf("HASH 段是否全零: %v\n", allZero)
fmt.Printf("HASH 段 / 24 = %.3f (若接近整数,可能是每文件一项)\n", float64(hashSize)/float64(fileItemSize))
fmt.Printf("HASH 段 / 文件数 = %.3f\n", float64(hashSize)/float64(fileCount))
}
// ---- 文件表尾部原始字段 ----
fmt.Printf("\n=== 文件表尾部 %d 条(原始字段)===\n", *tail)
fmt.Printf("%-9s %-10s %-10s %-6s %-10s %-8s %-4s\n", "idx", "nameOff", "pathOff", "chunk", "dataOff", "dataSize", "type")
start := fileCount - *tail
if start < 0 {
start = 0
}
for idx := start; idx < fileCount; idx++ {
off := tableOff + idx*fileItemSize
fmt.Printf("%-9d %-10d %-10d %-6d %-10d %-8d %-4d\n", idx,
i32(data[off:off+4]), i32(data[off+4:off+8]), i32(data[off+8:off+12]),
i32(data[off+12:off+16]), i32(data[off+16:off+20]), i32(data[off+20:off+24]))
}
// ---- 用归档 API 看排序 ----
archive, err := platformpvf.Open(*pvfPath)
if err != nil {
fmt.Fprintln(os.Stderr, "open:", err)
os.Exit(1)
}
files := archive.Files()
fmt.Printf("\n=== 文件表排序检查(用解析出的路径)===\n")
if len(files) != fileCount {
fmt.Printf("注意: archive.Files() 数量 %d != header.fileCount %d\n", len(files), fileCount)
}
type row struct {
idx int
dir string
name string
full string
}
rows := make([]row, 0, len(files))
for _, f := range files {
rows = append(rows, row{idx: f.Index, dir: f.Path, name: f.Name, full: f.ArchivePath})
}
check := func(label string, key func(row) string) {
breaks := 0
var samples []string
for i := 1; i < len(rows); i++ {
if key(rows[i-1]) > key(rows[i]) {
breaks++
if len(samples) < *order {
samples = append(samples, fmt.Sprintf("idx%d %q > idx%d %q",
rows[i-1].idx, trunc(key(rows[i-1])), rows[i].idx, trunc(key(rows[i]))))
}
}
}
fmt.Printf("%s: 逆序点 %d / %d\n", label, breaks, len(rows)-1)
for _, s := range samples {
fmt.Printf(" %s\n", s)
}
}
check("按完整路径(小写)递增", func(r row) string { return strings.ToLower(r.full) })
check("按目录(小写)递增", func(r row) string { return strings.ToLower(r.dir) })
check("按文件名(小写)递增", func(r row) string { return strings.ToLower(r.name) })
// 目录是否成块(同一目录是否连续)
seen := map[string]bool{}
blocks, reopens := 0, 0
prev := ""
for _, r := range rows {
if r.dir != prev {
blocks++
if seen[r.dir] {
reopens++
}
seen[r.dir] = true
prev = r.dir
}
}
fmt.Printf("目录块数: %d,其中「目录再次出现」次数: %d\n", blocks, reopens)
// 末尾新增的目录
dirCount := map[string]int{}
for _, r := range rows {
dirCount[r.dir]++
}
var dirs []string
for d := range dirCount {
dirs = append(dirs, d)
}
sort.Strings(dirs)
fmt.Printf("目录总数: %d\n", len(dirs))
fmt.Printf("\n=== 尾部 20 条解析出的路径 ===\n")
for i := len(rows) - 20; i < len(rows); i++ {
if i < 0 {
continue
}
fmt.Printf(" idx=%-9d dir=%-28q name=%q\n", rows[i].idx, rows[i].dir, rows[i].name)
}
// ---- 指定区间 dump ----
if *rng != "" {
var from, to int
if _, scanErr := fmt.Sscanf(*rng, "%d:%d", &from, &to); scanErr != nil {
fmt.Printf("\n-range 格式应为 from:to(%v)\n", scanErr)
} else {
if from < 0 {
from = 0
}
if to > len(rows) {
to = len(rows)
}
fmt.Printf("\n=== 文件表区间 [%d, %d) ===\n", from, to)
for i := from; i < to && i < len(rows); i++ {
fmt.Printf(" idx=%-9d %s\n", rows[i].idx, rows[i].full)
}
}
}
}
func trunc(s string) string {
if len(s) > 46 {
return s[:46] + "…"
}
return s
}