// Command companionoverlay — 支援兵覆盖工具(单文件版)。 // // 把「支援兵 AIC 覆盖」做成一个可独立发布的可执行程序:内置 companionpvf 与 // profiles.json,用户只需拖入 Script.pvf(或直接双击用默认路径),即可: // // 1) 生成覆盖版 PVF(默认输出到同目录 Script.companion.pvf); // 2) 可选地安装到客户端/服务端目录(自动备份、打印哈希)。 // // 用法: // // 支援兵覆盖工具.exe 交互模式(使用默认客户端路径) // 支援兵覆盖工具.exe 拖拽/命令行:只生成覆盖版 // 支援兵覆盖工具.exe --client <客户端目录> --server <服务端数据目录> package main import ( "bufio" "crypto/sha256" "embed" "encoding/hex" "fmt" "io" "os" "os/exec" "path/filepath" "strings" "time" ) //go:embed companionpvf.exe profiles.json var assets embed.FS const ( appTitle = "DNF90 支援兵覆盖工具" appVersion = "1.0.0" defaultClient = `D:\90server\Client\Script.pvf` minOutputBytes = 50 << 20 ) func main() { fmt.Printf("%s v%s\n", appTitle, appVersion) fmt.Println(strings.Repeat("=", 60)) args := os.Args[1:] pvfPath, clientDir, serverDir, install, interactive := parseArgs(args) if interactive { pvfPath = prompt("要处理的 Script.pvf 路径", pvfPath) if !ask("是否同时安装到客户端/服务端目录?(y/N): ") { install = false } else { install = true clientDir = prompt("客户端目录(含 Script.pvf)", clientDir) serverDir = prompt("服务端数据目录(含 Script.pvf)", serverDir) } } if !install { clientDir, serverDir = "", "" } if strings.TrimSpace(pvfPath) == "" { fail("没有指定 Script.pvf") } source, err := filepath.Abs(pvfPath) if err != nil || !exists(source) { fail("找不到 PVF:" + pvfPath) } outPath := strings.TrimSuffix(source, filepath.Ext(source)) + ".companion.pvf" if dir := filepath.Dir(source); dir != "" { outPath = filepath.Join(dir, "Script.companion.pvf") } fmt.Printf("输入 PVF:%s\n", source) fmt.Printf("输出 PVF:%s\n", outPath) if err := runOverlay(source, outPath); err != nil { fail(err.Error()) } size := fileSize(outPath) fmt.Printf("覆盖版已生成:%d 字节(%.1f MB)\n", size, float64(size)/(1<<20)) if size < minOutputBytes { fail("输出文件过小,疑似失败(原 PVF 未被修改)") } hash := fileHash(outPath) fmt.Printf("覆盖版 SHA256:%s\n", hash) if install { if err := installTo(source, outPath, hash, clientDir, serverDir); err != nil { fail(err.Error()) } } else { fmt.Println() fmt.Println("未安装。手动替换方法:") fmt.Println(" 客户端:把输出文件改名为 Script.pvf 覆盖到 客户端目录。") fmt.Println(" 服务端:同样覆盖到 服务端 runtime\\data\\dnf\\Script.pvf。") fmt.Println(" 完成后重启服务端,再启动游戏。") } fmt.Println() fmt.Println("完成。") if interactive { fmt.Print("按回车退出...") bufio.NewReader(os.Stdin).ReadString('\n') } } func parseArgs(args []string) (pvf, clientDir, serverDir string, install, interactive bool) { clientDir = `D:\90server\Client` serverDir = `D:\90server\Server\runtime\data\dnf` pvf = defaultClient interactive = len(args) == 0 for i := 0; i < len(args); i++ { switch args[i] { case "--client": if i+1 < len(args) { clientDir = args[i+1] install = true i++ } case "--server": if i+1 < len(args) { serverDir = args[i+1] install = true i++ } case "--install": install = true default: if !strings.HasPrefix(args[i], "--") { pvf = args[i] } } } return pvf, clientDir, serverDir, install, interactive } func runOverlay(source, outPath string) error { toolPath := filepath.Join(os.TempDir(), fmt.Sprintf("companionpvf-%d.exe", time.Now().UnixNano())) toolBytes, err := assets.ReadFile("companionpvf.exe") if err != nil { return fmt.Errorf("读取内置工具失败:%w", err) } if err := os.WriteFile(toolPath, toolBytes, 0o700); err != nil { return fmt.Errorf("释放内置工具失败:%w", err) } defer os.Remove(toolPath) profilesPath := filepath.Join(os.TempDir(), fmt.Sprintf("companion-profiles-%d.json", time.Now().UnixNano())) profilesBytes, err := assets.ReadFile("profiles.json") if err != nil { return fmt.Errorf("读取内置配置失败:%w", err) } if err := os.WriteFile(profilesPath, profilesBytes, 0o600); err != nil { return fmt.Errorf("释放内置配置失败:%w", err) } defer os.Remove(profilesPath) fmt.Println("生成覆盖版(约需 10–60 秒)...") cmd := exec.Command(toolPath, "--profile-overlay", source, outPath, profilesPath) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("覆盖失败:%w(原 PVF 未被修改)", err) } return nil } func installTo(source, outPath, hash, clientDir, serverDir string) error { if hasProcess("DNF") { return fmt.Errorf("请先关闭 DNF 客户端:PVF 正被占用") } targets := []string{ filepath.Join(clientDir, "Script.pvf"), filepath.Join(serverDir, "Script.pvf"), } stamp := time.Now().Format("20060102-150405") for _, target := range targets { if !exists(target) { fmt.Printf("跳过(不存在):%s\n", target) continue } if fileHash(target) == hash { fmt.Printf("已是最新,跳过:%s\n", target) continue } backup := target + ".bak_before_companion_" + stamp if err := copyFile(target, backup); err != nil { return fmt.Errorf("备份失败:%w", err) } fmt.Printf("已备份:%s\n", backup) if err := copyFile(outPath, target); err != nil { return fmt.Errorf("安装失败:%w", err) } fmt.Printf("已安装:%s\n", target) } fmt.Println("请重启 DNF90 服务端(服务端启动时加载 PVF),再启动游戏。") return nil } func prompt(label, fallback string) string { fmt.Printf("%s [%s]: ", label, fallback) reader := bufio.NewReader(os.Stdin) text, _ := reader.ReadString('\n') text = strings.TrimSpace(text) if text == "" { return fallback } return strings.Trim(text, `"`) } func ask(label string) bool { fmt.Print(label) reader := bufio.NewReader(os.Stdin) text, _ := reader.ReadString('\n') text = strings.ToLower(strings.TrimSpace(text)) return text == "y" || text == "yes" } func exists(path string) bool { info, err := os.Stat(path) return err == nil && !info.IsDir() } func fileSize(path string) int64 { info, err := os.Stat(path) if err != nil { return 0 } return info.Size() } func fileHash(path string) string { f, err := os.Open(path) if err != nil { return "" } defer f.Close() h := sha256.New() if _, err := io.Copy(h, f); err != nil { return "" } return strings.ToUpper(hex.EncodeToString(h.Sum(nil))) } func copyFile(src, dst string) error { in, err := os.Open(src) if err != nil { return err } defer in.Close() out, err := os.Create(dst) if err != nil { return err } if _, err := io.Copy(out, in); err != nil { out.Close() return err } return out.Close() } func hasProcess(name string) bool { cmd := exec.Command("tasklist", "/FI", "IMAGENAME eq "+name+".exe", "/NH") output, err := cmd.Output() if err != nil { return false } return strings.Contains(strings.ToLower(string(output)), strings.ToLower(name+".exe")) } func fail(message string) { fmt.Fprintf(os.Stderr, "\n[错误] %s\n", message) fmt.Print("按回车退出...") bufio.NewReader(os.Stdin).ReadString('\n') os.Exit(1) }