Files
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

280 lines
7.3 KiB
Go

package pvf
import (
"encoding/binary"
"fmt"
)
type fileItem struct {
nameOffset int
pathOffset int
chunkIndex int
dataOffset int
dataSize int
dataType int
}
type groupItem struct {
compressedSize int
originalSize int
}
type pvfHeader struct {
plain [headerSize]byte
fileCount int
bodySize int
groupCount int
hashSize int
nameSize int
}
func (a *Archive) parse() error {
if len(a.data) < headerSize {
return fmt.Errorf("%w: header too short", ErrInvalidArchive)
}
if err := a.parseNKPI(); err == nil {
return nil
}
if err := a.parseProtectedNKPI(); err == nil {
return nil
}
return fmt.Errorf("%w: unsupported archive format", ErrInvalidArchive)
}
func (a *Archive) parseNKPI() error {
a.resetParseState()
header := append([]byte(nil), a.data[:headerSize]...)
decryptGuard(header)
decrypt("HeaD", header)
if binary.LittleEndian.Uint32(header[0:4]) != magicSignature {
return fmt.Errorf("%w: bad signature", ErrInvalidArchive)
}
pvfHeader := parseHeaderSizes(header)
if err := validateHeader(pvfHeader); err != nil {
return err
}
return a.parseNKPITables(pvfHeader, false)
}
func (a *Archive) parseProtectedNKPI() error {
a.resetParseState()
header := append([]byte(nil), a.data[:headerSize]...)
decryptProtected("hEAd", header)
if binary.LittleEndian.Uint32(header[0:4]) != magicSignature {
return fmt.Errorf("%w: bad protected signature", ErrInvalidArchive)
}
pvfHeader := parseHeaderSizes(header)
if err := validateHeader(pvfHeader); err != nil {
return err
}
return a.parseNKPITables(pvfHeader, true)
}
func (a *Archive) parseNKPITables(header pvfHeader, protected bool) error {
pos := headerSize
tableOffset := pos
tableSize, err := checkedMul(header.fileCount, fileItemSize)
if err != nil {
return err
}
pos += tableSize
pos += header.hashSize
nameOffset := pos
pos += header.nameSize
groupOffset := pos
groupSize, err := checkedMul(header.groupCount, groupItemSize)
if err != nil {
return err
}
pos += groupSize
if pos < 0 || pos > len(a.data) || header.bodySize > len(a.data)-pos {
return fmt.Errorf("%w: section sizes exceed archive", ErrInvalidArchive)
}
a.header = header
a.tableOff = tableOffset
a.bodyOff = pos
nameBytes := append([]byte(nil), a.data[nameOffset:nameOffset+header.nameSize]...)
if protected {
if err := a.buildPStringBuffers(nameBytes); err != nil {
return err
}
a.format = FormatProtectedNKPI
} else {
a.buildStringBuffers(nameBytes)
a.format = FormatNKPI
}
groupBytes := append([]byte(nil), a.data[groupOffset:groupOffset+groupSize]...)
if protected {
decryptProtected("grpi", groupBytes)
} else {
decrypt("GRPI", groupBytes)
}
if err := a.parseGroups(groupBytes, header.groupCount); err != nil {
return err
}
return a.parseFiles(tableOffset, header.fileCount)
}
func (a *Archive) resetParseState() {
a.format = ""
a.items = nil
a.groups = nil
a.header = pvfHeader{}
a.pathIdx = make(map[string]int)
a.bodyOff = 0
a.strA = nil
a.strW = nil
}
func parseHeaderSizes(header []byte) pvfHeader {
var plain [headerSize]byte
copy(plain[:], header)
return pvfHeader{
plain: plain,
fileCount: readInt32(header[24:28]),
bodySize: readInt32(header[32:36]),
groupCount: readInt32(header[36:40]),
hashSize: readInt32(header[40:44]),
nameSize: readInt32(header[44:48]),
}
}
func validateHeader(header pvfHeader) error {
if header.fileCount < 0 || header.bodySize < 0 || header.groupCount < 0 || header.hashSize < 0 || header.nameSize < 0 {
return fmt.Errorf("%w: negative header size", ErrInvalidArchive)
}
return nil
}
func (a *Archive) parseGroups(data []byte, count int) error {
a.groups = make([]groupItem, 0, count)
for idx := 0; idx < count; idx++ {
off := idx * groupItemSize
compressedSize := readInt32(data[off : off+4])
originalSize := readInt32(data[off+4 : off+8])
if compressedSize < 0 || originalSize < 0 {
return fmt.Errorf("%w: negative group size", ErrInvalidArchive)
}
a.groups = append(a.groups, groupItem{
compressedSize: compressedSize,
originalSize: originalSize,
})
}
return nil
}
func (a *Archive) parseFiles(offset, count int) error {
a.items = make([]fileItem, 0, count)
for idx := 0; idx < count; idx++ {
off := offset + idx*fileItemSize
if off+fileItemSize > len(a.data) {
return fmt.Errorf("%w: file table exceeds archive", ErrInvalidArchive)
}
item := fileItem{
nameOffset: readInt32(a.data[off : off+4]),
pathOffset: readInt32(a.data[off+4 : off+8]),
chunkIndex: readInt32(a.data[off+8 : off+12]),
dataOffset: readInt32(a.data[off+12 : off+16]),
dataSize: readInt32(a.data[off+16 : off+20]),
dataType: readInt32(a.data[off+20 : off+24]),
}
name := a.resolveString(item.nameOffset)
dir := a.resolveString(item.pathOffset)
archivePath := joinArchivePath(dir, name)
a.items = append(a.items, item)
if archivePath != "" {
key := pathKey(archivePath)
if _, exists := a.pathIdx[key]; !exists {
a.pathIdx[key] = idx
}
}
}
return nil
}
func (a *Archive) buildStringBuffers(nameBytes []byte) {
if len(nameBytes) < 16 {
return
}
idx := 8
a.strA = decryptStringBuffer(nameBytes, &idx, "sTrA", 0xAA74472E)
a.strW = decryptStringBuffer(nameBytes, &idx, "sTrW", 0x9A82F037)
}
func (a *Archive) buildPStringBuffers(nameBytes []byte) error {
if len(nameBytes) < 16 {
return nil
}
idx := 8
strA, err := decryptPStringBuffer(nameBytes, &idx, "StRa", 0xAA74472E)
if err != nil {
return fmt.Errorf("decode protected pvf strA: %w", err)
}
strW, err := decryptPStringBuffer(nameBytes, &idx, "StRw", 0x9A82F037)
if err != nil {
return fmt.Errorf("decode protected pvf strW: %w", err)
}
a.strA = strA
a.strW = strW
return nil
}
func decryptStringBuffer(data []byte, idx *int, key string, xorConst uint32) []byte {
if *idx+8 > len(data) {
return nil
}
cnt1 := binary.LittleEndian.Uint32(data[*idx : *idx+4])
*idx += 4
*idx += 4
encSize := int(cnt1 ^ xorConst)
if encSize <= 0 || *idx+encSize > len(data) {
return nil
}
encrypted := append([]byte(nil), data[*idx:*idx+encSize]...)
*idx += encSize
decrypt2(key, encrypted)
out, err := zlibBytes(encrypted)
if err != nil {
return nil
}
return out
}
func decryptPStringBuffer(data []byte, idx *int, key string, xorConst uint32) ([]byte, error) {
if *idx+8 > len(data) {
return nil, nil
}
encodedSize := binary.LittleEndian.Uint32(data[*idx : *idx+4])
encodedOriginalSize := binary.LittleEndian.Uint32(data[*idx+4 : *idx+8])
*idx += 8
encSize := int(encodedSize ^ xorConst)
if encSize <= 0 || *idx+encSize > len(data) {
return nil, fmt.Errorf("%w: invalid protected string buffer size %d", ErrInvalidArchive, encSize)
}
encrypted := append([]byte(nil), data[*idx:*idx+encSize]...)
*idx += encSize
decryptPString(key, encrypted)
out, err := zlibBytes(encrypted)
if err != nil {
return nil, err
}
if want := int(encodedOriginalSize ^ uint32(encSize)); want >= 0 && want != len(out) {
return nil, fmt.Errorf("%w: protected string buffer size mismatch: want %d got %d", ErrInvalidArchive, want, len(out))
}
return out, nil
}
func readInt32(data []byte) int {
return int(int32(binary.LittleEndian.Uint32(data)))
}
func checkedMul(left, right int) (int, error) {
maxInt := int(^uint(0) >> 1)
if left < 0 || right < 0 || (right != 0 && left > maxInt/right) {
return 0, fmt.Errorf("%w: section size overflow", ErrInvalidArchive)
}
return left * right, nil
}