服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
280 lines
7.3 KiB
Go
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
|
|
}
|