Files

278 lines
7.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Command companionoverlay — 支援兵覆盖工具(单文件版)。
//
// 把「支援兵 AIC 覆盖」做成一个可独立发布的可执行程序:内置 companionpvf 与
// profiles.json,用户只需拖入 Script.pvf(或直接双击用默认路径),即可:
//
// 1) 生成覆盖版 PVF(默认输出到同目录 Script.companion.pvf);
// 2) 可选地安装到客户端/服务端目录(自动备份、打印哈希)。
//
// 用法:
//
// 支援兵覆盖工具.exe 交互模式(使用默认客户端路径)
// 支援兵覆盖工具.exe <Script.pvf> 拖拽/命令行:只生成覆盖版
// 支援兵覆盖工具.exe <Script.pvf> --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)
}