package dnfbridge import ( "os" "path/filepath" "strings" "sync" "time" ) // 本文件实现支援兵自建 APC 的**开关**。 // // ⚠️ 默认必须保持关闭(环境变量与标记文件都不设)。这条通道已在零售客户端上 // 实测**导致客户端闪退**(op29 含自建 APC 时 0xC0000005 空指针),只有官方 // 配套的补丁客户端才能安全使用。完整结论见 // companion_dungeon_p0_start_map.go 顶部的说明。 // // 为什么还要有文件开关(而不是只用环境变量) // 环境变量必须在服务端**进程启动前**就设好,而本部署的启动链路是 // D:\90server\登录游戏.bat → DNF90Launcher.exe → DNF90Server.exe // 只要服务端已经在跑,登录器就会复用它,改环境变量不会生效 —— // 表现为"改了半天开关,行为一点没变",极难排查。 // // 文件开关在**每次进副本时**读一次,所以: // - 服务端已经跑着也能开/关 // - 登录器重建服务端也不受影响 // - 开关状态可被 GM 工具/一行命令直接写 // // 两种开关是**或**的关系,任一成立即启用;都关=行为与移植前完全一致。 const ( // 环境开关(兼容 Server-90 的既有名字)。 currentDungeonCompanionP0StartMapEnv = "DNF_COMPANION_P0_STARTMAP_APC" // 文件开关:内容为 "1"(或文件存在且内容非 0)即启用。 currentDungeonCompanionP0StartMapFlagFile = "companion_start_map.enabled" // 文件开关的读取缓存时长。进副本是低频操作,这个粒度足够, // 又不会在每次房间切换时都打系统调用。 currentDungeonCompanionP0StartMapFlagTTL = 5 * time.Second ) var ( companionStartMapFlagMu sync.Mutex companionStartMapFlagCache bool companionStartMapFlagChecked time.Time // companionStartMapFlagPathOverride 让测试把开关文件指向临时目录。 // // 没有这个钩子时,测试会去读**部署目录**里的真实开关文件 // (测试进程的工作目录同样是 runtime\),于是"用户打开开关"会 // 连带把一批与支援兵无关的测试弄失败 —— 那是环境耦合,不是被 // 测代码的问题。 companionStartMapFlagPathOverride string ) func currentDungeonCompanionP0StartMapEnabledValue(value string) bool { return value == "1" } // currentDungeonCompanionP0StartMapFlagPath 返回文件开关的绝对路径。 // 服务端的工作目录是 runtime\,所以默认落在 runtime\ 下。 func currentDungeonCompanionP0StartMapFlagPath() string { if override := strings.TrimSpace(companionStartMapFlagPathOverride); override != "" { return override } if dir, err := os.Getwd(); err == nil && strings.TrimSpace(dir) != "" { return filepath.Join(dir, currentDungeonCompanionP0StartMapFlagFile) } return currentDungeonCompanionP0StartMapFlagFile } // currentDungeonCompanionP0StartMapFlagFileEnabled 读文件开关(带 TTL 缓存)。 // // 语义:文件存在且内容不是 "0"/"false" 即为开。这样 `echo 1 > 文件` 和 // 直接 `type nul > 文件` 都能开,符合"放个标记文件"的直觉。 func currentDungeonCompanionP0StartMapFlagFileEnabled() bool { companionStartMapFlagMu.Lock() defer companionStartMapFlagMu.Unlock() now := time.Now() if !companionStartMapFlagChecked.IsZero() && now.Sub(companionStartMapFlagChecked) < currentDungeonCompanionP0StartMapFlagTTL { return companionStartMapFlagCache } companionStartMapFlagChecked = now companionStartMapFlagCache = false raw, err := os.ReadFile(currentDungeonCompanionP0StartMapFlagPath()) if err != nil { return false } switch strings.ToLower(strings.TrimSpace(string(raw))) { case "0", "false", "off", "no": return false } companionStartMapFlagCache = true return true } // currentDungeonCompanionP0StartMapEnabled 是这条通道的总开关。 func currentDungeonCompanionP0StartMapEnabled() bool { if currentDungeonCompanionP0StartMapEnabledValue(os.Getenv(currentDungeonCompanionP0StartMapEnv)) { return true } return currentDungeonCompanionP0StartMapFlagFileEnabled() }