// pvfdigestdisable 把 etc/storydigest/scene.etc 里**所有**剧情回顾段的影片替换为 // 池中已有的占位动画 blackBoardScene.ani,并清空文字摘要。 // // 目的:彻底停用章节过场/剧情回顾动画,且不新增任何字符串。 // // 做法:对每个段落([scene] N `` ... [/scene]),把其主体内容整体替换为 // // [video] // `Video/movie/...blackBoardScene...` ← 池中已有引用 // // 这样客户端的 digest 表仍然完整(段号/ending 结构不变),但不再播放任何影片。 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, "只检查") flag.Parse() archive, err := platformpvf.Open(*input) if err != nil { panic(err) } // 占位动画的池引用(池中已有:scene 900 一直在用) blackRef := archive.FindStringMagicOffsets([]string{"etc/StoryDigest/Animation/blackBoardScene.ani"}) off, ok := blackRef["etc/StoryDigest/Animation/blackBoardScene.ani"] if !ok { panic("池中缺少 blackBoardScene.ani") } ref := uint32(off) fmt.Printf("占位动画引用: 0x%08X\n", ref) 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) // 重建:扫描 token 流,遇到 [scene] 段头就输出段头 + 段号,然后 // 跳到对应的 [/scene],期间内容替换为 [video] + 占位引用。 var out []byte i := 0 segs := 0 for i < toks { if raw[i*5] == 3 && u32(raw[i*5+1:i*5+5]) == refScene { // [scene] out = append(out, tok5(3, refScene)...) // 段号(整数) if i+1 < toks && raw[(i+1)*5] == 0 { out = append(out, raw[(i+1)*5:(i+1)*5+5]...) } else { out = append(out, tok5(0, 0)...) } // 空串(段标题位):保留原 token 类型,值置空 out = append(out, tok5(6, refEmpty)...) // 占位影片 out = append(out, tok5(3, refVideo)...) out = append(out, tok5(6, ref)...) // 跳到 [/scene] j := i + 1 for j < toks { if raw[j*5] == 3 && u32(raw[j*5+1:j*5+5]) == refEndSce { break } j++ } if j < toks { out = append(out, tok5(3, refEndSce)...) i = j + 1 } else { i = toks } segs++ continue } // 段外内容(空白/换行)原样保留 out = append(out, raw[i*5:i*5+5]...) i++ } fmt.Printf("处理了 %d 个段,新大小 %d 字节 (%d 令牌)\n", segs, len(out), len(out)/5) // 校验:段数一致,且不再有任何 origin_cinematic / cinematic_ 引用 newToks := len(out) / 5 foundVideos := 0 origVideos := 0 for k := 0; k+1 < newToks; k++ { if out[k*5] == 3 && u32(out[k*5+1:k*5+5]) == refVideo { foundVideos++ if out[(k+1)*5] == 6 { r := u32(out[(k+1)*5+1 : (k+1)*5+5]) if r != ref { origVideos++ } } } } fmt.Printf("[video] 段数=%d,非占位=%d\n", foundVideos, origVideos) if origVideos != 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), "字节") }