Files
90CNServer/server/cmd/scratch/pvfchapterbind/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

299 lines
9.2 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.
// pvfchapterbind 把 etc/storydigest/{scene,digest}.etc 重建为「区域 ↔ 章节影片段」
// 的 1:1 绑定,且**无重复**。
//
// 目标:
//
// 区域 39·0 开区等级 17 → 段 901 → origin_cinematic_01.avi
// 区域 40·4 开区等级 24 → 段 902 → origin_cinematic_02.avi
// ...(13 个区域,13 个互不相同的影片段号)
//
// 机制:客户端按**角色等级**选出 digest 段,播放该段 [ending] 指向的影片段。
// 因此要让「完成某区域的开区任务时播它自己的章节影片」,必须:
// 1. 该影片所在的影片段号 = 900 + 区域序号;
// 2. 该区域的开区等级落在「[ending] = 该段号」的那个 digest 段区间内。
//
// 约束:PVF 字符串是全局共享池引用,脚本文件里没有明文;新增字符串需扩建池并
// 重算偏移,超出安全修改范围。因此只改整数 token —— 段号与等级区间。
//
// 安全性:等级区间全部限定在正常等级范围(1..99),不使用非常规数值,
// 避免客户端在处理越界区间时出问题。
package main
import (
"encoding/binary"
"flag"
"fmt"
"os"
"path/filepath"
platformpvf "longheng.io/server/internal/platform/pvf"
)
const (
refLevel = 0x003566E2 // "[level]"
refEnding = 0x025138D6 // "[ending]"
)
// 影片名(去扩展名前的基名)→ 目标影片段号 = 900 + 区域序号
var filmToSegment = map[string]uint32{
"origin_cinematic_01.avi": 901,
"origin_cinematic_02.avi": 902,
"origin_cinematic_03.avi": 903,
"origin_cinematic_04.avi": 904,
"origin_cinematic_05.avi": 905,
"origin_cinematic_06.avi": 906,
"origin_cinematic_07.avi": 907,
"origin_cinematic_08.avi": 908,
"origin_cinematic_09.avi": 909,
"origin_cinematic_10.avi": 910,
"origin_cinematic_11.avi": 911,
"origin_cinematic_12.avi": 912,
"origin_cinematic_13.avi": 913,
}
// 区域开区等级(升序)。区域 i(从 1 起)对应段号 900+i。
// 约束:区域 i 的等级必须落在「[ending] = 900+i」的那个 digest 段区间内。
var regionLevels = []uint32{17, 24, 30, 35, 40, 46, 50, 63, 71, 75, 81, 85, 87}
// digest 段区间与 ending(共 17 段)。
//
// 段 i(1..13)的 ending = 900+i,其等级区间必须包含 regionLevels[i-1];
// 另外 4 段(14..17)为填充,ending 用剩余未被占用的影片段号(914..917),
// 等级区间也限定在正常范围(≤99)。
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, 52, 907}, // 区域 7 46·0 镜像阿拉德
{53, 62, 914}, // 填充
{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, 915}, // 填充
{93, 96, 916}, // 填充
{97, 99, 917}, // 填充
}
func u32(b []byte) uint32 { return binary.LittleEndian.Uint32(b) }
func put32(b []byte, v uint32) { binary.LittleEndian.PutUint32(b, v) }
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)
}
// ---------- scene.etc ----------
scene, err := archive.ReadRaw("etc/storydigest/scene.etc")
if err != nil {
panic(err)
}
stoks := len(scene) / 5
type vblock struct{ numTok, refTok int }
var blocks []vblock
for i := 0; i+5 < stoks; i++ {
if scene[i*5] == 3 && scene[(i+1)*5] == 0 && scene[(i+2)*5] == 6 &&
scene[(i+3)*5] == 3 && scene[(i+4)*5] == 6 && scene[(i+5)*5] == 3 {
blocks = append(blocks, vblock{i + 1, i + 4})
}
}
fmt.Println("scene.etc 影片段:", len(blocks))
// 现有引用 → 影片名(由 pvftokendump 实测的引用表)
refToFilm := map[uint32]string{
0x02513B14: "origin_cinematic_01.avi",
0x02513BAC: "origin_cinematic_02.avi",
0x02513C4E: "origin_cinematic_03.avi",
0x02513CE8: "origin_cinematic_04.avi",
0x02513D84: "origin_cinematic_05.avi",
0x02513EC8: "origin_cinematic_06.avi",
0x02513F60: "origin_cinematic_09.avi",
0x025140F8: "cinematic_06.avi",
0x0251417C: "cinematic_07.avi",
0x0251420C: "origin_cinematic_07.avi",
0x0251429E: "cinematic_09.avi",
0x0251436A: "origin_cinematic_08.avi",
0x025143FA: "cinematic_11.avi",
}
// 目标段号集合(13 个区域 + 4 个填充 = 17)
targets := []uint32{901, 902, 903, 904, 905, 906, 907, 908, 909, 910, 911, 912, 913, 914, 915, 916, 917}
// 分配:每个目标段号需要一个影片块。
// 先满足 13 个区域影片;剩余影片块分给填充段号(同影片只保留一个块)。
assigned := map[int]uint32{} // blockIndex -> target segment
usedTarget := map[uint32]bool{}
// 第一轮:按影片匹配区域目标
for bi, b := range blocks {
film := refToFilm[u32(scene[b.refTok*5+1:b.refTok*5+5])]
want, ok := filmToSegment[film]
if !ok {
continue
}
if usedTarget[want] {
continue
}
assigned[bi] = want
usedTarget[want] = true
}
// 第二轮:剩余块分给未占用的填充段号
spareIdx := 0
spare := []uint32{}
for _, t := range targets {
if !usedTarget[t] {
spare = append(spare, t)
}
}
for bi := range blocks {
if _, ok := assigned[bi]; ok {
continue
}
for spareIdx < len(spare) && usedTarget[spare[spareIdx]] {
spareIdx++
}
if spareIdx >= len(spare) {
break
}
assigned[bi] = spare[spareIdx]
usedTarget[spare[spareIdx]] = true
spareIdx++
}
fmt.Println("--- scene.etc 段号重排 ---")
changed := 0
for bi, b := range blocks {
film := refToFilm[u32(scene[b.refTok*5+1:b.refTok*5+5])]
old := u32(scene[b.numTok*5+1 : b.numTok*5+5])
want, ok := assigned[bi]
if !ok {
fmt.Printf(" %-26s 段号 %d (无目标,保持)\n", film, old)
continue
}
if old != want {
put32(scene[b.numTok*5+1:], want)
fmt.Printf(" %-26s 段号 %d → %d\n", film, old, want)
changed++
}
}
fmt.Println("改动:", changed)
// 唯一性 + 覆盖校验
seen := map[uint32]string{}
for bi, b := range blocks {
n := u32(scene[b.numTok*5+1 : b.numTok*5+5])
film := refToFilm[u32(scene[b.refTok*5+1:b.refTok*5+5])]
if prev, dup := seen[n]; dup {
panic(fmt.Sprintf("段号 %d 重复:%s 与 %s", n, prev, film))
}
seen[n] = film
_ = bi
}
for film, seg := range filmToSegment {
got, ok := seen[seg]
if !ok {
panic(fmt.Sprintf("段号 %d (%s) 未被分配", seg, film))
}
if got != film {
panic(fmt.Sprintf("段号 %d 应为 %s,实际 %s", seg, film, got))
}
}
fmt.Println("scene.etc 校验通过:13 个区域影片段号唯一且正确")
// ---------- 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, refEnding) {
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})
}
fmt.Println("digest.etc 段数:", len(slots), " 布局段数:", len(digestLayout))
if len(slots) != len(digestLayout) {
panic("段数不一致")
}
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("--- digest.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:]))
}
// 逐区域校验:开区等级落在 ending = 900+i 的段区间内
fmt.Println("--- 逐个区域校验 ---")
ok := true
for i, lv := range regionLevels {
seg := uint32(901 + i)
found := false
for _, row := range digestLayout {
if row[2] == seg && lv >= row[0] && lv <= row[1] {
found = true
break
}
}
status := "OK"
if !found {
status = "❌ 等级未落在该段区间"
ok = false
}
fmt.Printf(" 区域序号 %2d 开区等级 %-3d 段号 %d %s\n", i+1, lv, seg, status)
}
if !ok {
panic("区域等级与段区间不匹配")
}
fmt.Println("全部区域校验通过")
if *dry {
fmt.Println("dry-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.Println("已写出:", full, len(data), "字节")
}
}