DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

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

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

No files matched your search

+298
View File
@@ -0,0 +1,298 @@
// 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), "字节")
}
}