// pvfattendancepatch 对客户端 Script.pvf 里的「累计在线得好礼」活动定义 // (event/chn_event/chn_attendanceevent.evt) 做**等长 raw 字节改写**,目的是让该活动 // 在客户端彻底无法展示。 // // 已验证:raw 改写(ReadRaw + RepackRaw,长度不变)不会破坏 type-1 令牌流; // 用 ReadText 解码文本再写回则会破坏(实测只剩 1 行)。 // // 两种改写: // // zeroitems —— 把奖励物品名的字符串引用清零,使奖励位为空(已验证有效: // 解码后 [reward item] 变成 `0 1 0 1 0 1 0 1`) // unreachable —— 把"阶段时长"与"满阶段计数"的数值改到极大,使任何阶段都不可达 // // go run ./cmd/scratch/pvfattendancepatch -pvf -out -mode unreachable package main import ( "flag" "fmt" "os" platformpvf "longheng.io/server/internal/platform/pvf" ) const targetPath = "event/chn_event/chn_attendanceevent.evt" func main() { pvfPath := flag.String("pvf", "", "源 Script.pvf") outPath := flag.String("out", "", "输出 Script.pvf") mode := flag.String("mode", "unreachable", "zeroitems | unreachable | both") flag.Parse() if *pvfPath == "" || *outPath == "" { fmt.Fprintln(os.Stderr, "usage: pvfattendancepatch -pvf -out [-mode ...]") os.Exit(2) } archive, err := platformpvf.Open(*pvfPath) if err != nil { fmt.Fprintln(os.Stderr, "open:", err) os.Exit(1) } raw, err := archive.ReadRaw(targetPath) if err != nil { fmt.Fprintln(os.Stderr, "read raw:", err) os.Exit(1) } fmt.Printf("原始 raw %d 字节\n", len(raw)) patched := make([]byte, len(raw)) copy(patched, raw) if *mode == "zeroitems" || *mode == "both" { n := zeroStringRefs(patched) fmt.Printf("清零字符串引用 %d 处\n", n) } if *mode == "unreachable" || *mode == "both" { n := makeUnreachable(patched) fmt.Printf("改写数值为不可达 %d 处\n", n) } if len(patched) != len(raw) { fmt.Fprintln(os.Stderr, "!! 长度变化,中止") os.Exit(1) } repacked, err := archive.RepackRaw([]platformpvf.RawReplacement{{Path: targetPath, Data: patched}}) if err != nil { fmt.Fprintln(os.Stderr, "repack:", err) os.Exit(1) } if err := os.WriteFile(*outPath, repacked, 0o644); err != nil { fmt.Fprintln(os.Stderr, "write:", err) os.Exit(1) } fmt.Printf("已写出 %s (%d 字节)\n", *outPath, len(repacked)) verify, err := platformpvf.Open(*outPath) if err != nil { fmt.Fprintln(os.Stderr, "!! 产物无法打开:", err) os.Exit(1) } txt, err := verify.ReadText(targetPath) if err != nil { fmt.Fprintf(os.Stderr, "!! 产物内解码失败: %v\n", err) os.Exit(1) } fmt.Printf("---- 产物内该文件 ----\n%s\n----------------------\n", txt) } // zeroStringRefs 把形如 ?? E8 34 1D 的字符串引用(低字节连续递增的一组)清零。 func zeroStringRefs(data []byte) int { n := 0 for i := 0; i+3 < len(data); i++ { if data[i+1] == 0xE8 && data[i+2] == 0x34 && data[i+3] == 0x1D { data[i], data[i+1], data[i+2], data[i+3] = 0, 0, 0, 0 n++ i += 3 } } return n } // makeUnreachable 把活动里的"时长/计数"类数值改到极大。 // // 这些数值在 raw 里以 u32 LE 出现,且原始值都很小(1800/3600/5/10/15/25)。 // 这里按"已知原始值"精确匹配,避免误伤其它数据。 func makeUnreachable(data []byte) int { // 原始值 -> 替换值(u32 LE)。保持长度不变。 repl := map[uint32]uint32{ 1800: 0x7FFFFFFF, 3600: 0x7FFFFFFF, 5: 0x7FFFFFFF, 10: 0x7FFFFFFF, 15: 0x7FFFFFFF, 25: 0x7FFFFFFF, } n := 0 for i := 0; i+3 < len(data); i++ { v := uint32(data[i]) | uint32(data[i+1])<<8 | uint32(data[i+2])<<16 | uint32(data[i+3])<<24 nv, ok := repl[v] if !ok { continue } data[i] = byte(nv) data[i+1] = byte(nv >> 8) data[i+2] = byte(nv >> 16) data[i+3] = byte(nv >> 24) n++ i += 3 } return n }