package dnfbridge import ( "fmt" "os" "path/filepath" "strings" "time" ) // 新建角色「第一次进入地图」时播放一次性告示片。 // // 触发点:服务端把 start-map(op29)包发给客户端、客户端正式进入房间的那一刻 // (dungeon_navigation_runtime.go 里 StartMap 发送成功之后)。 // // 机制:客户端补丁的「内置播放器」读取 Client\90CN_play_cutscene.txt, // 内容为影片相对路径时自绘播放(不依赖游戏自带播放器)。 // 这里写入该文件,并按角色落标记,保证每个角色只播一次。 // // 可选配置(放在客户端目录,缺省则用默认值): // 90CN_first_map_movie.txt 一行影片相对路径,默认 Video/movie/demo_mjpeg.avi const ( firstMapMovieFileName = "90CN_first_map_movie.txt" defaultFirstMapMoviePath = "Video/movie/demo_mjpeg.avi" firstMapTriggerDelay = 1500 * time.Millisecond ) // cutsceneStateDir 返回保存「已播过」标记的目录(服务端工作目录下的 cutscene_state)。 func cutsceneStateDir() (string, error) { working, err := os.Getwd() if err != nil { return "", err } dir := filepath.Join(working, "cutscene_state") if err := os.MkdirAll(dir, 0o755); err != nil { return "", err } return dir, nil } // clientConfigFile 读取客户端目录下的单行配置;没有则返回空串。 func clientConfigFile(name string) string { candidates := []string{} if dir := strings.TrimSpace(os.Getenv("DNF90_CLIENT_DIR")); dir != "" { candidates = append(candidates, filepath.Join(dir, name)) } if triggerPath, err := resolveClientCutsceneTriggerPath(); err == nil { candidates = append(candidates, filepath.Join(filepath.Dir(triggerPath), name)) } for _, candidate := range candidates { raw, readErr := os.ReadFile(candidate) if readErr != nil { continue } if text := strings.TrimSpace(string(raw)); text != "" { return text } } return "" } // resolveFirstMapMovie 返回要播放的影片相对路径。 func resolveFirstMapMovie() string { if text := clientConfigFile(firstMapMovieFileName); text != "" { return text } return defaultFirstMapMoviePath } // triggerClientCutscenePath 写触发文件,内容为影片相对路径(补丁内置播放器接管)。 func triggerClientCutscenePath(relative string) error { if strings.TrimSpace(relative) == "" { return fmt.Errorf("empty movie path") } path, err := resolveClientCutsceneTriggerPath() if err != nil { return err } return os.WriteFile(path, []byte(strings.TrimSpace(relative)+"\n"), 0o644) } // maybePlayFirstMapCutscene 在角色进入地图成功后调用: // 该角色第一次进入地图时安排一次告示片播放。 func (s *Service) maybePlayFirstMapCutscene(session *gameSession) { if s == nil || session == nil || session.selectedCharacterID == 0 { return } characterID := session.selectedCharacterID stateDir, err := cutsceneStateDir() if err != nil { return } marker := filepath.Join(stateDir, fmt.Sprintf("first_map_%d.done", characterID)) if _, statErr := os.Stat(marker); statErr == nil { return // 这个角色已经播过 } if writeErr := os.WriteFile(marker, []byte(time.Now().Format(time.RFC3339)+"\n"), 0o644); writeErr != nil { return } movie := resolveFirstMapMovie() s.logGameEvent(session, "game-first-map-cutscene-armed", "char_id", characterID, "movie", movie) go func(character uint16, path string) { time.Sleep(firstMapTriggerDelay) if triggerErr := triggerClientCutscenePath(path); triggerErr != nil { s.logGameEvent(session, "game-first-map-cutscene-trigger-failed", "char_id", character, "error", triggerErr) return } s.logGameEvent(session, "game-first-map-cutscene-triggered", "char_id", character, "movie", path) }(characterID, movie) }