服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
337 lines
11 KiB
Go
337 lines
11 KiB
Go
// 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))
|
||
}
|
||
}
|