// pvfrecapclear 只清空 etc/storydigest/scene.etc 里的**剧情回顾摘要**段, // 保留**章节过场影片**段不动。 // // 判定:段号 < 900 的段是回顾摘要(文字/图片/动画叙述); // 段号 >= 900 的段承载 Video/movie/*.avi 章节过场。 // // 做法(安全,不改结构): // - 回顾摘要段:内容整体替换为池中已有的占位动画 blackBoardScene.ani, // **段号保留**(digest 的 [scene] 列表引用这些段号); // - 影片段:原样保留; // - 不新增任何字符串(占位动画的池引用已存在,段 900 一直在用)。 package main import ( "encoding/binary" "flag" "fmt" "os" "path/filepath" platformpvf "longheng.io/server/internal/platform/pvf" ) const ( refScene = uint32(0x025138C6) // "[scene]" refVideo = uint32(0x02513B04) // "[video]" refEndSce = uint32(0x02513A44) // "[/scene]" refEmpty = uint32(0x00000B04) // 空串 ) func u32(b []byte) uint32 { return binary.LittleEndian.Uint32(b) } func put32(b []byte, v uint32) { binary.LittleEndian.PutUint32(b, v) } func tok5(typ byte, ref uint32) []byte { b := make([]byte, 5) b[0] = typ binary.LittleEndian.PutUint32(b[1:], ref) return b } func main() { input := flag.String("pvf", "", "input PVF") dir := flag.String("dir", "", "raw patch dir") dry := flag.Bool("dry-run", false, "只检查") minRecap := flag.Int("below", 900, "段号小于此值视为回顾摘要") flag.Parse() archive, err := platformpvf.Open(*input) if err != nil { panic(err) } offsets := archive.FindStringMagicOffsets([]string{"etc/StoryDigest/Animation/blackBoardScene.ani"}) off, ok := offsets["etc/StoryDigest/Animation/blackBoardScene.ani"] if !ok { panic("池中缺少 blackBoardScene.ani") } placeholder := uint32(off) fmt.Printf("占位动画引用: 0x%08X\n", placeholder) raw, err := archive.ReadRaw("etc/storydigest/scene.etc") if err != nil { panic(err) } toks := len(raw) / 5 fmt.Printf("scene.etc 原始 %d 字节 %d 令牌\n", len(raw), toks) var out []byte i := 0 cleared, kept := 0, 0 for i < toks { if raw[i*5] == 3 && u32(raw[i*5+1:i*5+5]) == refScene { // 段号 numTok := i + 1 num := uint32(0) if numTok < toks && raw[numTok*5] == 0 { num = u32(raw[numTok*5+1 : numTok*5+5]) } // 找到 [/scene] j := numTok + 1 for j < toks { if raw[j*5] == 3 && u32(raw[j*5+1:j*5+5]) == refEndSce { break } j++ } // 重建该段 out = append(out, tok5(3, refScene)...) out = append(out, tok5(0, num)...) out = append(out, tok5(6, refEmpty)...) if int(num) < *minRecap { // 回顾摘要 → 清空为占位动画 out = append(out, tok5(3, refVideo)...) out = append(out, tok5(6, placeholder)...) cleared++ } else { // 影片段 → 原样保留主体 out = append(out, raw[(numTok+1)*5:j*5]...) kept++ } if j < toks { out = append(out, tok5(3, refEndSce)...) i = j + 1 } else { i = toks } continue } out = append(out, raw[i*5:i*5+5]...) i++ } fmt.Printf("回顾摘要段清空 %d 个,影片段保留 %d 个,新大小 %d 字节\n", cleared, kept, len(out)) // 校验:影片段数量与引用未变;回顾段已无 image/ani 内容 newToks := len(out) / 5 videos := 0 for k := 0; k+1 < newToks; k++ { if out[k*5] == 3 && u32(out[k*5+1:k*5+5]) == refVideo { videos++ } } fmt.Printf("输出中 [video] 段数 = %d(回顾 %d + 影片 %d)\n", videos, cleared, kept) if kept == 0 { panic("影片段全部丢失") } // 段号唯一性 seen := map[uint32]bool{} for k := 0; k+1 < newToks; k++ { if out[k*5] == 3 && u32(out[k*5+1:k*5+5]) == refScene && out[(k+1)*5] == 0 { n := u32(out[(k+1)*5+1 : (k+1)*5+5]) if seen[n] { panic(fmt.Sprintf("段号 %d 重复", n)) } seen[n] = true } } fmt.Printf("段号唯一,共 %d 段\n", len(seen)) if *dry { fmt.Println("dry-run,未写文件") return } full := filepath.Join(*dir, "etc", "storydigest", "scene.etc") if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil { panic(err) } if err := os.WriteFile(full, out, 0o644); err != nil { panic(err) } fmt.Println("已写出", full, len(out), "字节") _ = put32 }