// pvfchapter1to1 重建 etc/storydigest/{scene,digest}.etc,使 13 个城镇区域的 // 章节过场与其序号严格 1:1 对应、且互不重复: // // 区域序号 i(按开区等级升序)→ 影片段号 900+i → Video/movie/origin_cinematic_{i:02d}.avi // // 机制:客户端按**角色等级**选出 digest 段,播放该段 [ending] 指向的影片段。 // 因此必须让「包含区域 i 开区等级的 digest 段」的 [ending] = 900+i, // 且影片段 900+i 承载 origin_cinematic_{i:02d}.avi。 // // 实现: // - scene.etc:把每个已存在的影片块 [video] 引用改成其段号对应的影片; // 缺 908/909 两个块 → 追加(字符串已在全局池中,无需扩建池)。 // - digest.etc:段 i 的 [level] 区间包含区域 i 的开区等级,[ending] = 900+i。 // // 全部字符串引用通过 FindStringMagicOffsets 从池中反查,绝不新建字符串。 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]" refEmpty = uint32(0x00000B04) // 空串 refLevel = uint32(0x003566E2) // "[level]" refEnd = uint32(0x025138D6) // "[ending]" ) // 段号 → 影片序号(1..13)。前 13 段与区域一一对应。 var segmentFilmIndex = map[uint32]int{ 901: 1, 902: 2, 903: 3, 904: 4, 905: 5, 906: 6, 907: 7, 908: 8, 909: 9, 910: 10, 911: 11, 912: 12, 913: 13, } // 填充段号 → 填充影片下标(0..3,对应 fillerWants:cinematic_06/07/09/11)。 // 这样 17 个段的影片互不重复。 var segmentFillerIndex = map[uint32]int{ 914: 0, 915: 1, 916: 2, 917: 3, } // wantRefForSegment 返回段号应承载的影片池引用。 func wantRefForSegment(seg uint32, refForIndex map[int]uint32, refForFiller []uint32) (uint32, string, bool) { if idx, ok := segmentFilmIndex[seg]; ok { return refForIndex[idx], fmt.Sprintf("origin_cinematic_%02d.avi", idx), true } if fi, ok := segmentFillerIndex[seg]; ok { return refForFiller[fi], fillerWants[fi], true } return 0, "", false } // fillerWants 是填充段使用的影片路径(池里已有,且不被任何区域占用)。 var fillerWants = []string{ "cinematic_06.avi", "cinematic_07.avi", "cinematic_09.avi", "cinematic_11.avi", } // 区域开区等级(升序)。区域 i(从 1 起)→ 段号 900+i。 var regionLevels = []uint32{17, 24, 30, 35, 40, 46, 50, 63, 71, 75, 81, 85, 87} // digest 段区间。规则:段 i 的 [ending] = 900+i,且区间必须包含 regionLevels[i-1]。 // // 区域 1..12 直接落在段 1..12;段 13 与段 17 是填充段(ending 用池里已有的 // cinematic_* 影片段号,避免新增字符串)。 var digestLayout = [][3]uint32{ {17, 23, 901}, // 区域 1 39·0 赫顿玛尔 {24, 29, 902}, // 区域 2 40·4 天帷巨兽 {30, 34, 903}, // 区域 3 41·0 阿法利亚 {35, 39, 904}, // 区域 4 42·0 暗黑城 {40, 45, 905}, // 区域 5 43·0 斯顿雪域 {46, 49, 906}, // 区域 6 39·5 诺斯玛尔 {50, 54, 907}, // 区域 7 46·0 镜像阿拉德 {55, 62, 915}, // 填充(ending 915 = origin_cinematic_08 的块号) {63, 70, 908}, // 区域 8 6·0 根特 {71, 74, 909}, // 区域 9 9·0 鲁夫特悬空海港 {75, 80, 910}, // 区域 10 11·0 素喃 {81, 84, 911}, // 区域 11 14·0 斯乌工业基地 {85, 86, 912}, // 区域 12 22·0 克洛诺斯岛 {87, 89, 913}, // 区域 13 30·0 地轨中心 {90, 92, 916}, // 填充 {93, 95, 917}, // 填充 {96, 120, 914}, // 填充 } 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 directory output") dry := flag.Bool("dry-run", false, "只检查") flag.Parse() archive, err := platformpvf.Open(*input) if err != nil { panic(err) } // 从池中反查全部 13 个 origin_cinematic 影片的引用 wants := make([]string, 0, 13) for i := 1; i <= 13; i++ { wants = append(wants, fmt.Sprintf("Video/movie/origin_cinematic_%02d.avi", i)) } // 填充段影片路径与区域影片一起反查(fillerWants 见包级声明) allWants := make([]string, 0, len(wants)+len(fillerWants)) allWants = append(allWants, wants...) for _, f := range fillerWants { allWants = append(allWants, "Video/movie/"+f) } offsets := archive.FindStringMagicOffsets(allWants) refForIndex := map[int]uint32{} for i, w := range wants { off, ok := offsets[w] if !ok { panic("池中缺少字符串: " + w) } refForIndex[i+1] = uint32(off) } refForFiller := make([]uint32, 0, len(fillerWants)) for _, w := range fillerWants { off, ok := offsets["Video/movie/"+w] if !ok { panic("池中缺少填充字符串: " + w) } refForFiller = append(refForFiller, uint32(off)) } fmt.Println("=== 池中 13 个影片引用 ===") for i := 1; i <= 13; i++ { fmt.Printf(" origin_cinematic_%02d.avi ref=0x%08X\n", i, refForIndex[i]) } fmt.Println("=== 填充段影片(池中已有,保证 17 段无重复)===") for i, w := range fillerWants { fmt.Printf(" %-34s ref=0x%08X\n", w, refForFiller[i]) } // ---------- scene.etc ---------- scene, err := archive.ReadRaw("etc/storydigest/scene.etc") if err != nil { panic(err) } toks := len(scene) / 5 fmt.Printf("\nscene.etc 原始 %d 字节 %d 令牌\n", len(scene), toks) // 收集影片块:块起始 token、段号 token、[video] 之后那个 token(影片引用) type blk struct{ start, numTok, filmTok int } var blocks []blk for i := 0; i+4 < toks; i++ { if scene[i*5] != 3 || u32(scene[i*5+1:i*5+5]) != refScene { continue } if scene[(i+1)*5] != 0 { continue } if scene[(i+2)*5] != 6 || u32(scene[(i+2)*5+1:(i+2)*5+5]) != refEmpty { continue } if scene[(i+3)*5] != 3 || u32(scene[(i+3)*5+1:(i+3)*5+5]) != refVideo { continue } blocks = append(blocks, blk{i, i + 1, i + 4}) } fmt.Printf("找到 %d 个影片块\n", len(blocks)) // 1) 已有块:按段号把影片引用改成该段号对应的影片 have := map[uint32]bool{} for _, b := range blocks { seg := u32(scene[b.numTok*5+1 : b.numTok*5+5]) if seg < 901 || seg > 917 { fmt.Printf(" 跳过段号 %d(不在 901..917)\n", seg) continue } wantRef, wantName, ok := wantRefForSegment(seg, refForIndex, refForFiller) if !ok { fmt.Printf(" 跳过段号 %d(无影片映射)\n", seg) continue } oldRef := u32(scene[b.filmTok*5+1 : b.filmTok*5+5]) if oldRef != wantRef { put32(scene[b.filmTok*5+1:], wantRef) fmt.Printf(" 段 %d: 影片引用 0x%08X → 0x%08X (%s)\n", seg, oldRef, wantRef, wantName) } have[seg] = true } // 2) 缺失的段号:追加新块 var appended []byte for seg := uint32(901); seg <= 917; seg++ { if have[seg] { continue } wantRef, wantName, ok := wantRefForSegment(seg, refForIndex, refForFiller) if !ok { panic(fmt.Sprintf("段 %d 无影片映射", seg)) } appended = append(appended, tok5(3, refScene)...) appended = append(appended, tok5(0, seg)...) appended = append(appended, tok5(6, refEmpty)...) appended = append(appended, tok5(3, refVideo)...) appended = append(appended, tok5(6, wantRef)...) appended = append(appended, tok5(3, 0x02513A44)...) // [/scene] fmt.Printf(" 追加段 %d (%s)\n", seg, wantName) } if len(appended) > 0 { scene = append(scene, appended...) } fmt.Printf("scene.etc 新大小 %d 字节 (%d 令牌)\n", len(scene), len(scene)/5) // 校验:901..917 各一个块,且影片引用正确 ntoks := len(scene) / 5 found := map[uint32]uint32{} for i := 0; i+4 < ntoks; i++ { if scene[i*5] != 3 || u32(scene[i*5+1:i*5+5]) != refScene { continue } if scene[(i+1)*5] != 0 { continue } if scene[(i+2)*5] != 6 || u32(scene[(i+2)*5+1:(i+2)*5+5]) != refEmpty { continue } if scene[(i+3)*5] != 3 || u32(scene[(i+3)*5+1:(i+3)*5+5]) != refVideo { continue } seg := u32(scene[(i+1)*5+1 : (i+1)*5+5]) ref := u32(scene[(i+4)*5+1 : (i+4)*5+5]) if prev, dup := found[seg]; dup { panic(fmt.Sprintf("段 %d 有重复块(ref 0x%08X / 0x%08X)", seg, prev, ref)) } found[seg] = ref } for seg := uint32(901); seg <= 917; seg++ { ref, ok := found[seg] if !ok { panic(fmt.Sprintf("段 %d 缺失", seg)) } want, wantName, ok := wantRefForSegment(seg, refForIndex, refForFiller) if !ok { panic(fmt.Sprintf("段 %d 无影片映射", seg)) } if ref != want { panic(fmt.Sprintf("段 %d 影片错: 0x%08X 期望 0x%08X (%s)", seg, ref, want, wantName)) } } fmt.Println("scene.etc 校验通过:901..917 各一个块且影片正确(无重复)") // ---------- digest.etc ---------- digest, err := archive.ReadRaw("etc/storydigest/digest.etc") if err != nil { panic(err) } dtoks := len(digest) / 5 isSec := func(buf []byte, n, i int, ref uint32) bool { return i < n && buf[i*5] == 3 && u32(buf[i*5+1:i*5+5]) == ref } type slot struct{ loTok, hiTok, endTok int } var slots []slot for i := 0; i < dtoks; i++ { if !isSec(digest, dtoks, i, refLevel) { continue } endTok := -1 for j := i + 3; j < dtoks; j++ { if isSec(digest, dtoks, j, refEnd) { endTok = j + 1 break } if isSec(digest, dtoks, j, refLevel) { break } } if endTok < 0 { panic("找不到 [ending]") } slots = append(slots, slot{i + 1, i + 2, endTok}) } if len(slots) != len(digestLayout) { panic(fmt.Sprintf("digest 段数 %d != 布局 %d", len(slots), len(digestLayout))) } for i, s := range slots { put32(digest[s.loTok*5+1:], digestLayout[i][0]) put32(digest[s.hiTok*5+1:], digestLayout[i][1]) put32(digest[s.endTok*5+1:], digestLayout[i][2]) } fmt.Println("\ndigest.etc 新布局:") for i, s := range slots { fmt.Printf(" 段%-3d %3d-%-3d ending %d\n", i+1, u32(digest[s.loTok*5+1:]), u32(digest[s.hiTok*5+1:]), u32(digest[s.endTok*5+1:])) } for i, lv := range regionLevels { seg := uint32(901 + i) ok := false for _, row := range digestLayout { if row[2] == seg && lv >= row[0] && lv <= row[1] { ok = true break } } if !ok { panic(fmt.Sprintf("区域 %d 等级 %d 不在段 %d 区间", i+1, lv, seg)) } } fmt.Println("digest.etc 校验通过:13 个区域各自落在对应段区间内") if *dry { fmt.Println("\ndry-run,未写文件") return } for path, data := range map[string][]byte{ "etc/storydigest/scene.etc": scene, "etc/storydigest/digest.etc": digest, } { full := filepath.Join(*dir, filepath.FromSlash(path)) if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil { panic(err) } if err := os.WriteFile(full, data, 0o644); err != nil { panic(err) } fmt.Printf("已写出 %s %d 字节\n", full, len(data)) } }