Files
90CNServer/server/cmd/scratch/pvfchapter1to1/main.go
T
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

337 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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))
}
}