// Command chchaptertable 打印「章节动画」的完整对应表 —— 即最终修复依赖的那张表: // // 城镇区域 → 需等级 → 前置任务 → 影片 → scene 段号(900+N) → digest 段 → 下发值(LevelMin-1) // // 数据全部来自 PVF:etc/townmovielist.etc、各城镇 .twn 的 area [permission]、 // etc/storydigest/digest.etc,以及 n_quest 任务表(用于显示任务名)。 // // 用法: // // go run ./cmd/scratch/chchaptertable -pvf package main import ( "context" "flag" "fmt" "os" "path" "sort" "strings" dnfpvf "longheng.io/server/internal/modules/dnf/pvf" dnfquest "longheng.io/server/internal/modules/dnf/quest" dnfstorydigest "longheng.io/server/internal/modules/dnf/storydigest" dnftown "longheng.io/server/internal/modules/dnf/town" platformpvf "longheng.io/server/internal/platform/pvf" ) // cinematicSequence 从 `Video/movie/origin_cinematic_05.avi` 取出 5(与 // dnfbridge.townMovieCinematicSequence 完全一致)。 func cinematicSequence(filePath string) (int, bool) { const prefix = "origin_cinematic_" normalized := strings.ReplaceAll(strings.TrimSpace(filePath), "\\", "/") name := path.Base(normalized) name = strings.TrimSuffix(name, ".avi") idx := strings.LastIndex(name, prefix) if idx < 0 { return 0, false } digits := name[idx+len(prefix):] if digits == "" { return 0, false } value := 0 for _, r := range digits { if r < '0' || r > '9' { return 0, false } value = value*10 + int(r-'0') } if value <= 0 { return 0, false } return value, true } func main() { pvfPath := flag.String("pvf", "", "Script.pvf 路径") flag.Parse() if *pvfPath == "" { fmt.Fprintln(os.Stderr, "需要 -pvf") os.Exit(2) } archive, err := platformpvf.LoadArchive(platformpvf.Options{Path: *pvfPath}) if err != nil { fmt.Fprintln(os.Stderr, "打开 PVF 失败:", err) os.Exit(1) } ctx := context.Background() towns, err := dnftown.Load(ctx, archive, dnftown.Options{}) if err != nil { fmt.Fprintf(os.Stderr, "加载城镇表失败: %v\n", err) } movies, err := dnftown.LoadTownMovieTable(archive, dnftown.DefaultMovieListPath) if err != nil { fmt.Fprintln(os.Stderr, "加载过场表失败:", err) os.Exit(1) } digest, err := dnfstorydigest.Load(archive, dnfstorydigest.DefaultDigestPath) if err != nil { fmt.Fprintln(os.Stderr, "加载 digest 段表失败:", err) os.Exit(1) } questNames := map[int64]string{} if index, buildErr := dnfpvf.Build(ctx, archive, dnfpvf.BuildOptions{Lists: []string{dnfquest.DefaultList}}); buildErr == nil { if catalog, loadErr := dnfquest.Load(ctx, index); loadErr == nil { for _, def := range catalog.Definitions() { if strings.TrimSpace(def.Name) != "" { questNames[def.ID] = def.Name } } } } type row struct { play dnftown.TownMoviePlay area dnftown.Area town dnftown.Town sequence int ending int segment dnfstorydigest.Segment found bool } plays := movies.Plays() rows := make([]row, 0, len(plays)) for _, play := range plays { area, areaOK := towns.FindArea(play.TownID, play.AreaID) town, _ := towns.Find(play.TownID) r := row{play: play, area: area, town: town, found: areaOK} if seq, ok := cinematicSequence(play.FilePath); ok { r.sequence = seq r.ending = 900 + seq if seg, ok := digest.SegmentForEnding(r.ending); ok { r.segment = seg } } rows = append(rows, r) } sort.SliceStable(rows, func(i, j int) bool { li, lj := rows[i].area.MinLevel, rows[j].area.MinLevel if li == 0 { li = 1 << 30 } if lj == 0 { lj = 1 << 30 } return li < lj }) fmt.Printf("=== 章节动画对应表(%d 条)===\n\n", len(rows)) fmt.Printf("%-4s %-4s %-4s %-13s %-5s %-6s %-8s %-9s %s\n", "Lv", "城镇", "区域", "影片标题", "片段号", "digest段", "段等级", "下发值", "前置任务") fmt.Println(strings.Repeat("-", 128)) for _, r := range rows { level := "-" if r.area.MinLevel > 0 { level = fmt.Sprintf("%d", r.area.MinLevel) } title := r.play.Title if title == "" { title = "(无标题)" } seqText := "-" segText := "-" levelRange := "-" sendText := "-" if r.sequence > 0 { seqText = fmt.Sprintf("900+%02d", r.sequence) } if r.segment.Index > 0 { segText = fmt.Sprintf("#%d", r.segment.Index) levelRange = fmt.Sprintf("%d-%d", r.segment.LevelMin, r.segment.LevelMax) sendLevel := r.segment.LevelMin - 1 if sendLevel < 0 { sendLevel = 0 } sendText = fmt.Sprintf("%d", sendLevel) } quests := "-" if len(r.area.NeedQuests) > 0 { parts := make([]string, 0, len(r.area.NeedQuests)) for _, id := range r.area.NeedQuests { if name, ok := questNames[id]; ok { parts = append(parts, fmt.Sprintf("%d(%s)", id, name)) } else { parts = append(parts, fmt.Sprintf("%d", id)) } } quests = strings.Join(parts, ", ") } fmt.Printf("%-4s %-4d %-4d %-13s %-5s %-6s %-8s %-9s %s\n", level, r.play.TownID, r.play.AreaID, title, seqText, segText, levelRange, sendText, quests) } fmt.Printf("\n=== digest 段表(etc/storydigest/digest.etc,共 %d 段)===\n\n", digest.Len()) fmt.Printf("%-5s %-9s %-8s %s\n", "段号", "等级区间", "结尾段号", "说明") fmt.Println(strings.Repeat("-", 60)) for _, seg := range digest.Segments() { note := "" if seg.Ending == 0 { note = "无结尾影片" } else { note = fmt.Sprintf("scene %d", seg.Ending) if seg.Ending >= 901 && seg.Ending <= 913 { note += fmt.Sprintf(" → origin_cinematic_%02d.avi", seg.Ending-900) } } fmt.Printf("%-5d %-9s %-8d %s\n", seg.Index, fmt.Sprintf("%d-%d", seg.LevelMin, seg.LevelMax), seg.Ending, note) } }