DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
hanfuit committed 2026-11-04 14:31:55 +08:00
commit 645b64fe21
4388 files changed
+1405457

No files matched your search

+550
View File
@@ -0,0 +1,550 @@
// Command pvfrepack rebuilds an existing DNF PVF with raw file replacements.
// It never edits the base archive in place and does not add or delete paths.
package main
import (
"bufio"
"bytes"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"errors"
"flag"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
"time"
platformpvf "longheng.io/server/internal/platform/pvf"
)
const (
toolVersion = "2026.08.17"
defaultMaxFileBytes = int64(256 * 1024 * 1024)
defaultModifiedDir = "PVF修改文件"
defaultOutputPVFName = "Script_重新打包.pvf"
)
type packOptions struct {
basePath string
modifiedRoot string
outputPath string
maxBytes int64
maxFileBytes int64
overwrite bool
dryRun bool
continueOnError bool
}
type scanResult struct {
scanned int
unchanged int
failed int
replacements []platformpvf.RawReplacement
}
func main() {
prepareConsole()
if len(os.Args) == 1 {
reader := bufio.NewReader(os.Stdin)
err := runInteractive(reader, os.Stdout, os.Stderr)
if err != nil {
fmt.Fprintln(os.Stderr, "pvfrepack:", err)
}
fmt.Fprint(os.Stdout, "\n按回车键退出……")
_, _ = reader.ReadString('\n')
if err != nil {
os.Exit(1)
}
return
}
if err := run(os.Args[1:], os.Stdout, os.Stderr); err != nil {
if errors.Is(err, flag.ErrHelp) {
return
}
fmt.Fprintln(os.Stderr, "pvfrepack:", err)
os.Exit(1)
}
}
func run(args []string, stdout, stderr io.Writer) error {
flags := flag.NewFlagSet("pvfrepack", flag.ContinueOnError)
flags.SetOutput(stderr)
base := flags.String("pvf", "", "作为基底的原始 Script.pvf(只读)")
modified := flags.String("dir", "", "按 PVF 相对路径存放的 raw 修改文件目录")
output := flags.String("out", "", "新 PVF 输出路径")
maxBytes := flags.Int64("max-bytes", platformpvf.DefaultMaxBytes, "允许读取的最大 PVF 字节数")
maxFileBytes := flags.Int64("max-file-bytes", defaultMaxFileBytes, "单个替换文件的最大字节数")
overwrite := flags.Bool("overwrite", false, "输出已存在时备份旧文件并写入新文件")
dryRun := flags.Bool("dry-run", false, "只扫描和比较修改,不生成 PVF")
continueOnError := flags.Bool("continue-on-error", false, "扫描时记录单文件错误并继续;存在错误时不会打包")
version := flags.Bool("version", false, "显示工具版本")
flags.Usage = func() {
fmt.Fprintf(flags.Output(), "PVF 打包工具 %s(raw 修改文件)\n\n", toolVersion)
fmt.Fprintln(flags.Output(), "用法:")
fmt.Fprintln(flags.Output(), " PVF打包工具.exe -pvf <原始 Script.pvf> -dir <修改文件目录> -out <新 PVF>")
fmt.Fprintln(flags.Output(), "\n修改目录必须来自解包工具的 raw 模式,并保持 PVF 内部相对路径。")
fmt.Fprintln(flags.Output(), "decoded 可读文本不能直接重新封包。工具不会覆盖基底 PVF,也不新增或删除路径。")
fmt.Fprintln(flags.Output(), "\n选项:")
flags.PrintDefaults()
}
if err := flags.Parse(args); err != nil {
return err
}
if *version {
fmt.Fprintf(stdout, "pvfrepack %s\n", toolVersion)
return nil
}
if flags.NArg() != 0 {
return fmt.Errorf("不支持位置参数:%s", strings.Join(flags.Args(), " "))
}
options := packOptions{
basePath: strings.TrimSpace(*base),
modifiedRoot: strings.TrimSpace(*modified),
outputPath: strings.TrimSpace(*output),
maxBytes: *maxBytes,
maxFileBytes: *maxFileBytes,
overwrite: *overwrite,
dryRun: *dryRun,
continueOnError: *continueOnError,
}
return pack(options, stdout, stderr)
}
func pack(options packOptions, stdout, stderr io.Writer) error {
resolved, err := resolvePackOptions(options)
if err != nil {
return err
}
archive, err := platformpvf.LoadArchive(platformpvf.Options{
Path: resolved.basePath,
MaxBytes: resolved.maxBytes,
})
if err != nil {
return err
}
snapshot := archive.Snapshot()
fmt.Fprintf(stdout, "base format=%s files=%d groups=%d size=%d sha256=%s\n",
snapshot.Format, snapshot.FileCount, snapshot.GroupCount, snapshot.Size, snapshot.Checksum)
scan, err := scanModifications(archive, resolved, stdout, stderr)
if err != nil {
return err
}
fmt.Fprintf(stdout, "scan scanned=%d changed=%d unchanged=%d failed=%d\n",
scan.scanned, len(scan.replacements), scan.unchanged, scan.failed)
if scan.failed != 0 {
return fmt.Errorf("有 %d 个修改文件无效,未生成 PVF", scan.failed)
}
if len(scan.replacements) == 0 {
fmt.Fprintln(stdout, "没有检测到内容变化,未生成 PVF。")
return nil
}
if resolved.dryRun {
fmt.Fprintln(stdout, "dry-run 完成,未生成 PVF。")
return nil
}
fmt.Fprintf(stdout, "repack changed_files=%d ...\n", len(scan.replacements))
repacked, err := archive.RepackRaw(scan.replacements)
if err != nil {
return fmt.Errorf("重建 PVF: %w", err)
}
temporary, outputHash, err := writeTemporaryCandidate(resolved.outputPath, repacked)
if err != nil {
return err
}
repacked = nil
committed := false
defer func() {
if !committed {
_ = os.Remove(temporary)
}
}()
if err := verifyCandidate(temporary, scan.replacements, resolved.maxBytes); err != nil {
return err
}
backup, err := commitCandidate(temporary, resolved.outputPath, resolved.overwrite)
if err != nil {
return err
}
committed = true
info, err := os.Stat(resolved.outputPath)
if err != nil {
return fmt.Errorf("读取输出 PVF: %w", err)
}
fmt.Fprintf(stdout, "done changed=%d output=%s size=%d sha256=%s\n",
len(scan.replacements), resolved.outputPath, info.Size(), outputHash)
if backup != "" {
fmt.Fprintf(stdout, "previous_output_backup=%s\n", backup)
}
return nil
}
func resolvePackOptions(options packOptions) (packOptions, error) {
if options.basePath == "" {
return options, errors.New("必须提供 -pvf")
}
if options.modifiedRoot == "" {
return options, errors.New("必须提供 -dir")
}
if options.outputPath == "" && !options.dryRun {
return options, errors.New("必须提供 -out;只检查可使用 -dry-run")
}
if options.maxBytes <= 0 || options.maxFileBytes <= 0 {
return options, errors.New("大小限制必须大于 0")
}
var err error
options.basePath, err = filepath.Abs(trimWrappingQuotes(options.basePath))
if err != nil {
return options, fmt.Errorf("解析基底 PVF: %w", err)
}
options.modifiedRoot, err = filepath.Abs(trimWrappingQuotes(options.modifiedRoot))
if err != nil {
return options, fmt.Errorf("解析修改目录: %w", err)
}
if options.outputPath != "" {
options.outputPath, err = filepath.Abs(trimWrappingQuotes(options.outputPath))
if err != nil {
return options, fmt.Errorf("解析输出 PVF: %w", err)
}
if sameWindowsPath(options.basePath, options.outputPath) {
return options, errors.New("输出路径不能与基底 PVF 相同;工具禁止原地覆盖")
}
outputInfo, outputErr := os.Stat(options.outputPath)
switch {
case outputErr == nil && outputInfo.IsDir():
return options, errors.New("输出路径是目录,必须指向 .pvf 文件")
case outputErr == nil && !options.overwrite && !options.dryRun:
return options, fmt.Errorf("输出已存在:%s;使用 -overwrite 才会备份并替换", options.outputPath)
case outputErr != nil && !errors.Is(outputErr, os.ErrNotExist):
return options, fmt.Errorf("检查输出 PVF: %w", outputErr)
}
}
baseInfo, err := os.Stat(options.basePath)
if err != nil {
return options, fmt.Errorf("读取基底 PVF: %w", err)
}
if !baseInfo.Mode().IsRegular() {
return options, errors.New("基底 PVF 不是普通文件")
}
modifiedInfo, err := os.Stat(options.modifiedRoot)
if err != nil {
return options, fmt.Errorf("读取修改目录: %w", err)
}
if !modifiedInfo.IsDir() {
return options, errors.New("-dir 必须是目录")
}
return options, nil
}
func scanModifications(
archive *platformpvf.Archive,
options packOptions,
stdout io.Writer,
stderr io.Writer,
) (scanResult, error) {
var result scanResult
err := filepath.WalkDir(options.modifiedRoot, func(localPath string, entry fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if entry.Type()&os.ModeSymlink != 0 {
return fmt.Errorf("修改目录不允许符号链接:%s", localPath)
}
if entry.IsDir() {
return nil
}
absolute, err := filepath.Abs(localPath)
if err != nil {
return err
}
if sameWindowsPath(absolute, options.basePath) ||
(options.outputPath != "" && sameWindowsPath(absolute, options.outputPath)) {
return nil
}
relative, err := filepath.Rel(options.modifiedRoot, absolute)
if err != nil || relative == "." || relative == ".." || strings.HasPrefix(relative, ".."+string(os.PathSeparator)) {
return fmt.Errorf("修改文件越过根目录:%s", localPath)
}
archivePath := filepath.ToSlash(relative)
result.scanned++
file, found := archive.FindFile(archivePath)
if !found {
return handleScanError(options.continueOnError, stderr, &result,
fmt.Errorf("修改文件在基底 PVF 中不存在:%s", archivePath))
}
info, err := entry.Info()
if err != nil {
return handleScanError(options.continueOnError, stderr, &result, err)
}
if info.Size() > options.maxFileBytes {
return handleScanError(options.continueOnError, stderr, &result,
fmt.Errorf("修改文件过大:%s size=%d limit=%d", archivePath, info.Size(), options.maxFileBytes))
}
modified, err := os.ReadFile(absolute)
if err != nil {
return handleScanError(options.continueOnError, stderr, &result, err)
}
original, err := archive.FileRaw(file.Index)
if err != nil {
return handleScanError(options.continueOnError, stderr, &result,
fmt.Errorf("读取基底文件 %s: %w", archivePath, err))
}
if bytes.Equal(original, modified) {
result.unchanged++
return nil
}
if err := validateRawReplacement(file, original, modified); err != nil {
return handleScanError(options.continueOnError, stderr, &result,
fmt.Errorf("%s: %w", archivePath, err))
}
result.replacements = append(result.replacements, platformpvf.RawReplacement{
Path: archivePath,
Data: modified,
})
fmt.Fprintf(stdout, "changed\t%d\t%d->%d\t%s\n", file.DataType, len(original), len(modified), archivePath)
if result.scanned%1000 == 0 {
fmt.Fprintf(stdout, "scanned=%d changed=%d\n", result.scanned, len(result.replacements))
}
return nil
})
if err != nil {
return result, fmt.Errorf("扫描修改目录: %w", err)
}
return result, nil
}
func handleScanError(continueOnError bool, stderr io.Writer, result *scanResult, err error) error {
if !continueOnError {
return err
}
result.failed++
fmt.Fprintf(stderr, "[错误] %v\n", err)
return nil
}
func validateRawReplacement(file platformpvf.File, original, modified []byte) error {
switch file.DataType {
case 1:
if len(modified)%5 != 0 {
return errors.New("type-1 脚本不是五字节 token 流;疑似使用了 decoded 文本,请重新以 raw 模式解包")
}
allowed := map[byte]struct{}{0: {}, 2: {}, 3: {}, 5: {}, 6: {}, 7: {}}
for offset := 0; offset+5 <= len(original); offset += 5 {
allowed[original[offset]] = struct{}{}
}
for offset := 0; offset+5 <= len(modified); offset += 5 {
if _, ok := allowed[modified[offset]]; !ok {
return fmt.Errorf("type-1 token 偏移 %d 的类型 %d 无效;疑似 decoded 文本", offset, modified[offset])
}
if (modified[offset] == 3 || modified[offset] == 6) &&
int32(binary.LittleEndian.Uint32(modified[offset+1:offset+5])) < 0 {
return fmt.Errorf("type-1 字符串 token 偏移 %d 使用负引用", offset)
}
}
case 3:
if len(modified)%2 != 0 {
return errors.New("type-3 UTF-16LE 文件字节数不是偶数;疑似使用了 decoded UTF-8 文本")
}
}
return nil
}
func writeTemporaryCandidate(outputPath string, data []byte) (string, string, error) {
directory := filepath.Dir(outputPath)
if err := os.MkdirAll(directory, 0o755); err != nil {
return "", "", fmt.Errorf("创建输出目录: %w", err)
}
file, err := os.CreateTemp(directory, ".pvfrepack-*.pvf")
if err != nil {
return "", "", fmt.Errorf("创建临时 PVF: %w", err)
}
temporary := file.Name()
keep := false
defer func() {
_ = file.Close()
if !keep {
_ = os.Remove(temporary)
}
}()
hash := sha256.New()
if _, err := io.Copy(io.MultiWriter(file, hash), bytes.NewReader(data)); err != nil {
return "", "", fmt.Errorf("写入临时 PVF: %w", err)
}
if err := file.Sync(); err != nil {
return "", "", fmt.Errorf("同步临时 PVF: %w", err)
}
if err := file.Close(); err != nil {
return "", "", fmt.Errorf("关闭临时 PVF: %w", err)
}
keep = true
return temporary, strings.ToUpper(hex.EncodeToString(hash.Sum(nil))), nil
}
func verifyCandidate(path string, replacements []platformpvf.RawReplacement, maxBytes int64) error {
verified, err := platformpvf.LoadArchive(platformpvf.Options{Path: path, MaxBytes: maxBytes})
if err != nil {
return fmt.Errorf("重新打开候选 PVF: %w", err)
}
for _, replacement := range replacements {
raw, err := verified.ReadRaw(replacement.Path)
if err != nil {
return fmt.Errorf("校验 %s: %w", replacement.Path, err)
}
if !bytes.Equal(raw, replacement.Data) {
return fmt.Errorf("校验 %s: 成品字节与修改文件不一致", replacement.Path)
}
}
return nil
}
func commitCandidate(temporary, outputPath string, overwrite bool) (string, error) {
_, err := os.Stat(outputPath)
if errors.Is(err, fs.ErrNotExist) {
if err := os.Rename(temporary, outputPath); err != nil {
return "", fmt.Errorf("发布输出 PVF: %w", err)
}
return "", nil
}
if err != nil {
return "", fmt.Errorf("检查输出 PVF: %w", err)
}
if !overwrite {
return "", fmt.Errorf("输出已存在:%s;使用 -overwrite 才会备份并替换", outputPath)
}
backup := fmt.Sprintf("%s.previous-%s", outputPath, time.Now().Format("20060102-150405"))
if _, err := os.Stat(backup); !errors.Is(err, fs.ErrNotExist) {
return "", fmt.Errorf("输出备份路径已存在:%s", backup)
}
if err := os.Rename(outputPath, backup); err != nil {
return "", fmt.Errorf("备份旧输出 PVF: %w", err)
}
if err := os.Rename(temporary, outputPath); err != nil {
_ = os.Rename(backup, outputPath)
return "", fmt.Errorf("发布新 PVF: %w", err)
}
return backup, nil
}
func runInteractive(reader *bufio.Reader, stdout, stderr io.Writer) error {
fmt.Fprintf(stdout, "PVF 打包工具 %s(raw 修改文件)\n", toolVersion)
fmt.Fprintln(stdout, "不会原地修改基底 PVF;decoded 文本不能直接重新封包。")
defaultBase, defaultRoot := adjacentDefaults()
basePrompt := "\n请输入或拖入原始 Script.pvf:"
if defaultBase != "" {
basePrompt = fmt.Sprintf("\n基底 PVF [默认 %s]:", defaultBase)
}
base, err := promptLine(reader, stdout, basePrompt)
if err != nil {
return err
}
base = trimWrappingQuotes(base)
if base == "" {
base = defaultBase
}
if base == "" {
return errors.New("必须提供基底 PVF")
}
modifiedDefault := filepath.Join(defaultRoot, defaultModifiedDir)
modified, err := promptLine(reader, stdout, fmt.Sprintf("raw 修改文件目录 [默认 %s]:", modifiedDefault))
if err != nil {
return err
}
modified = trimWrappingQuotes(modified)
if modified == "" {
modified = modifiedDefault
}
outputDefault := filepath.Join(defaultRoot, defaultOutputPVFName)
output, err := promptLine(reader, stdout, fmt.Sprintf("新 PVF 输出路径 [默认 %s]:", outputDefault))
if err != nil {
return err
}
output = trimWrappingQuotes(output)
if output == "" {
output = outputDefault
}
overwrite, err := promptLine(reader, stdout, "输出存在时备份并替换?y/N:")
if err != nil {
return err
}
dryRun, err := promptLine(reader, stdout, "只检查修改、不生成 PVF?y/N:")
if err != nil {
return err
}
fmt.Fprintf(stdout, "\n基底:%s\n修改目录:%s\n输出:%s\n", base, modified, output)
fmt.Fprintln(stdout, "修改目录只能放 raw 模式导出的文件,并保持原 PVF 相对路径。")
confirm, err := promptLine(reader, stdout, "输入 PACK 开始:")
if err != nil {
return err
}
if !strings.EqualFold(strings.TrimSpace(confirm), "PACK") {
fmt.Fprintln(stdout, "已取消,没有生成 PVF。")
return nil
}
args := []string{"-pvf", base, "-dir", modified, "-out", output, "-continue-on-error"}
if answerIsYes(overwrite, false) {
args = append(args, "-overwrite")
}
if answerIsYes(dryRun, false) {
args = append(args, "-dry-run")
}
return run(args, stdout, stderr)
}
func adjacentDefaults() (string, string) {
executable, err := os.Executable()
if err != nil {
current, _ := os.Getwd()
return "", current
}
root := filepath.Dir(executable)
candidate := filepath.Join(root, "Script.pvf")
info, err := os.Stat(candidate)
if err != nil || !info.Mode().IsRegular() {
return "", root
}
return candidate, root
}
func promptLine(reader *bufio.Reader, output io.Writer, prompt string) (string, error) {
fmt.Fprint(output, prompt)
value, err := reader.ReadString('\n')
if err != nil && !errors.Is(err, io.EOF) {
return "", err
}
if errors.Is(err, io.EOF) && value == "" {
return "", errors.New("输入已关闭")
}
return strings.TrimSpace(value), nil
}
func trimWrappingQuotes(value string) string {
value = strings.TrimSpace(strings.TrimPrefix(value, "\uFEFF"))
if len(value) >= 2 && ((value[0] == '"' && value[len(value)-1] == '"') ||
(value[0] == '\'' && value[len(value)-1] == '\'')) {
return strings.TrimSpace(value[1 : len(value)-1])
}
return value
}
func answerIsYes(value string, defaultValue bool) bool {
value = strings.TrimSpace(value)
if value == "" {
return defaultValue
}
return strings.EqualFold(value, "y") || strings.EqualFold(value, "yes")
}
func sameWindowsPath(left, right string) bool {
return strings.EqualFold(filepath.Clean(left), filepath.Clean(right))
}