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

250 lines
7.6 KiB
Go

// npkinspect prints the IMG index and basic frame metadata from DNF NPK files.
package main
import (
"bufio"
"crypto/sha256"
"encoding/binary"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
const npkMagic = "NeoplePack_Bill\x00"
var npkNameKey = []byte("puchikon@neople dungeon and fighter " +
"DNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNF\x00")
type entry struct {
Name string
Offset uint32
Length uint32
}
type spriteEntry struct {
Index int
Type int32
Target int32
Compression int32
Width int32
Height int32
Length int32
X int32
Y int32
FrameWidth int32
FrameHeight int32
}
func main() {
npkPath := flag.String("npk", "", "one NPK file")
directory := flag.String("dir", "", "scan an ImagePacks2 directory")
match := flag.String("match", "", "case-insensitive IMG path filter")
includeFrames := flag.Bool("frames", false, "print every sprite entry for matched IMG members")
includeHashes := flag.Bool("hashes", false, "print SHA-256 of every matched IMG member")
flag.Parse()
if strings.TrimSpace(*npkPath) == "" && strings.TrimSpace(*directory) == "" {
fmt.Fprintln(os.Stderr, "-npk or -dir is required")
os.Exit(2)
}
paths := []string{*npkPath}
if strings.TrimSpace(*directory) != "" {
paths = nil
matches, err := filepath.Glob(filepath.Join(*directory, "*.NPK"))
if err != nil {
panic(err)
}
paths = matches
}
filter := strings.ToLower(strings.TrimSpace(*match))
for _, filePath := range paths {
if strings.TrimSpace(filePath) == "" {
continue
}
entries, err := readNPK(filePath)
if err != nil {
fmt.Fprintf(os.Stderr, "skip %s: %v\n", filePath, err)
continue
}
for _, item := range entries {
if filter != "" && !strings.Contains(strings.ToLower(item.Name), filter) {
continue
}
version, frameCount, imageMagic, metadataErr := readIMGMetadata(filePath, item)
if metadataErr != nil {
fmt.Printf("npk=%s img=%s offset=%d length=%d metadata_error=%q\n", filepath.Base(filePath), item.Name, item.Offset, item.Length, metadataErr)
continue
}
memberHash := ""
if *includeHashes {
hash, hashErr := readIMGHash(filePath, item)
if hashErr != nil {
fmt.Printf("npk=%s img=%s offset=%d length=%d hash_error=%q\n", filepath.Base(filePath), item.Name, item.Offset, item.Length, hashErr)
continue
}
memberHash = fmt.Sprintf(" sha256=%x", hash)
}
fmt.Printf("npk=%s img=%s offset=%d length=%d magic=%q version=%d frames=%d%s\n", filepath.Base(filePath), item.Name, item.Offset, item.Length, imageMagic, version, frameCount, memberHash)
if *includeFrames {
sprites, spriteErr := readIMGFrames(filePath, item)
if spriteErr != nil {
fmt.Printf(" frame_error=%q\n", spriteErr)
continue
}
for _, sprite := range sprites {
if sprite.Type == 0x11 {
fmt.Printf(" frame=%d type=LINK target=%d\n", sprite.Index, sprite.Target)
continue
}
fmt.Printf(" frame=%d type=0x%02X compression=0x%02X size=%dx%d bytes=%d offset=%d,%d canvas=%dx%d\n",
sprite.Index, sprite.Type, sprite.Compression, sprite.Width, sprite.Height,
sprite.Length, sprite.X, sprite.Y, sprite.FrameWidth, sprite.FrameHeight)
}
}
}
}
}
func readIMGHash(filePath string, item entry) ([32]byte, error) {
file, err := os.Open(filePath)
if err != nil {
return [32]byte{}, err
}
defer file.Close()
data := make([]byte, item.Length)
if _, err := file.ReadAt(data, int64(item.Offset)); err != nil {
return [32]byte{}, err
}
return sha256.Sum256(data), nil
}
func readIMGFrames(filePath string, item entry) ([]spriteEntry, error) {
file, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer file.Close()
data := make([]byte, item.Length)
if _, err := file.ReadAt(data, int64(item.Offset)); err != nil {
return nil, err
}
if len(data) < 32 || !strings.HasPrefix(string(data[:16]), "Neople Img File") {
return nil, fmt.Errorf("frame audit supports only the 32-byte IMG header")
}
count := int(binary.LittleEndian.Uint32(data[28:32]))
if count < 0 || count > 1_000_000 {
return nil, fmt.Errorf("unreasonable frame count %d", count)
}
offset := 32
sprites := make([]spriteEntry, 0, count)
for index := 0; index < count; index++ {
if offset+8 > len(data) {
return nil, fmt.Errorf("frame %d header exceeds IMG member", index)
}
spriteType := int32(binary.LittleEndian.Uint32(data[offset : offset+4]))
if spriteType == 0x11 {
sprites = append(sprites, spriteEntry{
Index: index,
Type: spriteType,
Target: int32(binary.LittleEndian.Uint32(data[offset+4 : offset+8])),
})
offset += 8
continue
}
if offset+36 > len(data) {
return nil, fmt.Errorf("frame %d metadata exceeds IMG member", index)
}
readI32 := func(start int) int32 {
return int32(binary.LittleEndian.Uint32(data[offset+start : offset+start+4]))
}
sprites = append(sprites, spriteEntry{
Index: index,
Type: spriteType,
Compression: readI32(4),
Width: readI32(8),
Height: readI32(12),
Length: readI32(16),
X: readI32(20),
Y: readI32(24),
FrameWidth: readI32(28),
FrameHeight: readI32(32),
})
offset += 36
}
return sprites, nil
}
func readNPK(filePath string) ([]entry, error) {
file, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer file.Close()
header := make([]byte, 20)
if _, err := io.ReadFull(file, header); err != nil {
return nil, err
}
if string(header[:16]) != npkMagic {
return nil, fmt.Errorf("invalid magic %q", header[:16])
}
count := binary.LittleEndian.Uint32(header[16:20])
if count > 1_000_000 {
return nil, fmt.Errorf("unreasonable IMG count %d", count)
}
entries := make([]entry, 0, count)
for index := uint32(0); index < count; index++ {
row := make([]byte, 264)
if _, err := io.ReadFull(file, row); err != nil {
return nil, fmt.Errorf("read index %d: %w", index, err)
}
nameBytes := append([]byte(nil), row[8:]...)
for position := range nameBytes {
nameBytes[position] ^= npkNameKey[position]
}
name := string(nameBytes)
if zero := strings.IndexByte(name, 0); zero >= 0 {
name = name[:zero]
}
entries = append(entries, entry{
Offset: binary.LittleEndian.Uint32(row[0:4]),
Length: binary.LittleEndian.Uint32(row[4:8]),
Name: strings.TrimSpace(name),
})
}
return entries, nil
}
func readIMGMetadata(filePath string, item entry) (uint32, uint32, string, error) {
file, err := os.Open(filePath)
if err != nil {
return 0, 0, "", err
}
defer file.Close()
if _, err := file.Seek(int64(item.Offset), io.SeekStart); err != nil {
return 0, 0, "", err
}
reader := bufio.NewReader(file)
magic, err := reader.ReadString(0)
if err != nil {
return 0, 0, "", err
}
magic = strings.TrimSuffix(magic, "\x00")
var metadata [16]byte
if _, err := io.ReadFull(reader, metadata[:]); err != nil {
return 0, 0, magic, err
}
if strings.HasPrefix(magic, "Neople Image File") {
// Legacy form uses a two-byte reserved field, so its version/count are
// not aligned with the newer four-int header.
version := binary.LittleEndian.Uint32(metadata[6:10])
frames := binary.LittleEndian.Uint32(metadata[10:14])
return version, frames, magic, nil
}
if strings.HasPrefix(magic, "Neople Img File") {
return binary.LittleEndian.Uint32(metadata[8:12]), binary.LittleEndian.Uint32(metadata[12:16]), magic, nil
}
return 0, 0, magic, fmt.Errorf("unsupported IMG magic")
}