DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,276 @@
|
||||
// pvfcutscenefix 重构 etc/storydigest/{scene,digest}.etc,使 13 个城镇区域的
|
||||
// 过场在各自开区等级所在的 story digest 段播出。
|
||||
//
|
||||
// 机制(PVF 实测):客户端按**角色等级**选出 digest 段,播放该段 [ending]
|
||||
// 指向的影片段。因此要让某区域的影片在其开区等级播出,必须让「包含该等级的
|
||||
// 段」的 [ending] 指向承载该影片的影片段。
|
||||
//
|
||||
// 约束:PVF 字符串是全局共享池引用,脚本文件里没有明文;新增字符串需扩建池并
|
||||
// 重算偏移,超出安全修改范围。因此只改整数 token:
|
||||
// - scene.etc:改影片段的段号(复用其已有的视频引用)
|
||||
// - digest.etc:改各段的 [level] 区间与 [ending]
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
platformpvf "longheng.io/server/internal/platform/pvf"
|
||||
)
|
||||
|
||||
const (
|
||||
refLevel = 0x003566E2 // "[level]"
|
||||
refEnding = 0x025138D6 // "[ending]"
|
||||
)
|
||||
|
||||
// 现有影片段号 -> (视频池引用, 影片名)。引用值由 pvftokendump 实测得到。
|
||||
var existingSegments = []struct {
|
||||
num uint32
|
||||
ref uint32
|
||||
film string
|
||||
}{
|
||||
{901, 0x02513B14, "origin_cinematic_01.avi"},
|
||||
{902, 0x02513BAC, "origin_cinematic_02.avi"},
|
||||
{903, 0x02513C4E, "origin_cinematic_03.avi"},
|
||||
{904, 0x02513CE8, "origin_cinematic_04.avi"},
|
||||
{905, 0x02513D84, "origin_cinematic_05.avi"},
|
||||
{906, 0x02513EC8, "origin_cinematic_06.avi"},
|
||||
{907, 0x02513F60, "origin_cinematic_09.avi"},
|
||||
{910, 0x025140F8, "cinematic_06.avi"},
|
||||
{911, 0x0251417C, "cinematic_07.avi"},
|
||||
{912, 0x0251420C, "origin_cinematic_07.avi"},
|
||||
{913, 0x0251429E, "cinematic_09.avi"},
|
||||
{915, 0x0251436A, "origin_cinematic_08.avi"},
|
||||
{916, 0x025143FA, "cinematic_11.avi"},
|
||||
{917, 0x025143FA, "cinematic_11.avi"},
|
||||
}
|
||||
|
||||
// 目标:每个区域 -> (影片名, 目标影片段号)
|
||||
//
|
||||
// 影片段号是该影片段在 scene.etc 中的段号,必须等于 digest 中对应段的 [ending]。
|
||||
// 段号分配避免冲突:同一影片只能占一个段号。
|
||||
var regionFilms = []struct {
|
||||
town, area uint32
|
||||
need uint32
|
||||
film string
|
||||
seg uint32
|
||||
}{
|
||||
{39, 0, 17, "origin_cinematic_01.avi", 901},
|
||||
{40, 4, 24, "origin_cinematic_02.avi", 902},
|
||||
{41, 0, 30, "origin_cinematic_03.avi", 903},
|
||||
{42, 0, 35, "origin_cinematic_04.avi", 904},
|
||||
{43, 0, 40, "origin_cinematic_05.avi", 905},
|
||||
{39, 5, 46, "origin_cinematic_06.avi", 906},
|
||||
{46, 0, 50, "origin_cinematic_07.avi", 907},
|
||||
{6, 0, 63, "origin_cinematic_08.avi", 908},
|
||||
{9, 0, 71, "origin_cinematic_09.avi", 909},
|
||||
{11, 0, 75, "cinematic_06.avi", 910},
|
||||
{14, 0, 81, "cinematic_07.avi", 911},
|
||||
{22, 0, 85, "cinematic_11.avi", 912},
|
||||
{30, 0, 87, "cinematic_09.avi", 913},
|
||||
}
|
||||
|
||||
// 根据区域开区等级切出 digest 段区间。
|
||||
// 每个区域独占一段,区间上界 = 下一个区域的 need-1(最后一段到 999)。
|
||||
func buildLayout() [][3]uint32 {
|
||||
filler := []uint32{914, 915, 916, 917}
|
||||
var out [][3]uint32
|
||||
for i, r := range regionFilms {
|
||||
lo := r.need
|
||||
hi := uint32(9998)
|
||||
if i+1 < len(regionFilms) {
|
||||
hi = regionFilms[i+1].need - 1
|
||||
}
|
||||
out = append(out, [3]uint32{lo, hi, r.seg})
|
||||
}
|
||||
// 区域只占 13 段;digest 有 17 段,末尾补 4 段填充。
|
||||
// 填充段用 9999 以上的等级区间(游戏内等级远达不到,不会被选中)。
|
||||
fillerLo := uint32(9999)
|
||||
for i, f := range filler {
|
||||
lo := fillerLo
|
||||
hi := lo + 999
|
||||
if i == len(filler)-1 {
|
||||
hi = 999999
|
||||
}
|
||||
out = append(out, [3]uint32{lo, hi, f})
|
||||
fillerLo = hi + 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// 目标段号 -> 影片名
|
||||
segToFilm := map[uint32]string{}
|
||||
for _, r := range regionFilms {
|
||||
if prev, dup := segToFilm[r.seg]; dup {
|
||||
panic(fmt.Sprintf("目标段号 %d 重复:%s 与 %s", r.seg, prev, r.film))
|
||||
}
|
||||
segToFilm[r.seg] = r.film
|
||||
}
|
||||
// 填充段也需存在
|
||||
for _, f := range []uint32{914, 915, 916, 917} {
|
||||
segToFilm[f] = "" // 空 = 用现有引用占位
|
||||
}
|
||||
|
||||
// 现有影片段:按引用找到它当前的实际段号 token
|
||||
type vblock struct {
|
||||
numTok, refTok int
|
||||
ref uint32
|
||||
oldNum uint32
|
||||
}
|
||||
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,
|
||||
u32(scene[(i+4)*5+1 : (i+4)*5+5]),
|
||||
u32(scene[(i+1)*5+1 : (i+1)*5+5])})
|
||||
}
|
||||
}
|
||||
fmt.Println("scene.etc 影片段:", len(blocks))
|
||||
|
||||
// 用引用反查影片名
|
||||
refToFilm := map[uint32]string{}
|
||||
for _, e := range existingSegments {
|
||||
refToFilm[e.ref] = e.film
|
||||
}
|
||||
|
||||
// 分配:每个目标段号需要一个影片块。
|
||||
// 做法:先按影片名把「目标段号」配给「已持有该影片的块」;
|
||||
// 若同一影片有多个块,多余的块挪到填充段号。
|
||||
filmToTarget := map[string]uint32{}
|
||||
for _, r := range regionFilms {
|
||||
filmToTarget[r.film] = r.seg
|
||||
}
|
||||
used := map[uint32]bool{}
|
||||
spare := []uint32{914, 915, 916, 917}
|
||||
spareIdx := 0
|
||||
|
||||
changed := 0
|
||||
for bi := range blocks {
|
||||
b := &blocks[bi]
|
||||
film := refToFilm[b.ref]
|
||||
target, ok := filmToTarget[film]
|
||||
if !ok || used[target] {
|
||||
// 该影片无区域需要,或目标号已被同影片的首个块占用
|
||||
if spareIdx < len(spare) {
|
||||
target = spare[spareIdx]
|
||||
spareIdx++
|
||||
} else {
|
||||
target = b.oldNum
|
||||
}
|
||||
}
|
||||
used[target] = true
|
||||
if b.oldNum != target {
|
||||
put32(scene[b.numTok*5+1:], target)
|
||||
fmt.Printf(" %-26s 段号 %d → %d\n", film, b.oldNum, target)
|
||||
changed++
|
||||
}
|
||||
}
|
||||
fmt.Println("scene.etc 改动:", changed)
|
||||
|
||||
// 唯一性校验
|
||||
seen := map[uint32]bool{}
|
||||
for _, b := range blocks {
|
||||
n := u32(scene[b.numTok*5+1 : b.numTok*5+5])
|
||||
if seen[n] {
|
||||
panic(fmt.Sprintf("scene.etc 段号 %d 重复", n))
|
||||
}
|
||||
seen[n] = true
|
||||
}
|
||||
fmt.Println("scene.etc 段号唯一性 OK:", len(seen))
|
||||
|
||||
// 校验:每个区域的目标段号都存在
|
||||
for _, r := range regionFilms {
|
||||
if !seen[r.seg] {
|
||||
panic(fmt.Sprintf("区域 %d·%d 的目标段号 %d 不存在", r.town, r.area, r.seg))
|
||||
}
|
||||
}
|
||||
fmt.Println("全部区域目标段号均存在 OK")
|
||||
|
||||
// ---------- 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})
|
||||
}
|
||||
layout := buildLayout()
|
||||
fmt.Println("digest.etc 段数:", len(slots), " 布局段数:", len(layout))
|
||||
if len(slots) != len(layout) {
|
||||
panic("段数不一致")
|
||||
}
|
||||
for i, s := range slots {
|
||||
put32(digest[s.loTok*5+1:], layout[i][0])
|
||||
put32(digest[s.hiTok*5+1:], layout[i][1])
|
||||
put32(digest[s.endTok*5+1:], layout[i][2])
|
||||
}
|
||||
fmt.Println("digest.etc 新布局:")
|
||||
for i, s := range slots {
|
||||
fmt.Printf(" 段%-3d level %5d-%-5d ending %d\n", i+1,
|
||||
u32(digest[s.loTok*5+1:]), u32(digest[s.hiTok*5+1:]), u32(digest[s.endTok*5+1:]))
|
||||
}
|
||||
|
||||
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), "字节")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user