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

+336
View File
@@ -0,0 +1,336 @@
// 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))
}
}