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

223 lines
5.7 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.
// pvfdigestscenealign 修正 etc/storydigest/digest.etc 的 [scene] 引用,
// 使每个开区区域的「剧情回顾序号」与「章节过场影片序号」一致(与原版设计对齐)。
//
// 背景:
// - 回顾段 1..17 与影片段 901..917 是**一一配对**的(回顾 N ↔ 影片 900+N);
// - 但区域的开放等级顺序与回顾序号顺序在高段位发生错位:
// 区域 8 (6·0 根特, lv63) 落在段 9 → 原本拿到回顾 9(应为 8)
// 区域 9 (9·0 鲁夫特, lv71) 落在段 10 → 原本拿到回顾 10(应为 9)
// ...(区域 10-13 同理)
// - 另外有 4 个「填充段」(8/15/16/17) 并不对应任何区域,却占用着回顾 8/15/16/17。
//
// 本次修正(只改 [scene],不动段号 / [level] / [ending]):
// 1) 区域所在的段:按该段 [ending] 对应的影片序号 N,把 [scene] 设为 N;
// 2) 填充段:把 [scene] 设为 0(scene.etc 里的开场段,不被任何区域使用),
// 避免占用某个区域的回顾,同时它仍是"真实内容段",不会让播放序列中断。
package main
import (
"encoding/binary"
"flag"
"fmt"
"os"
"path/filepath"
platformpvf "longheng.io/server/internal/platform/pvf"
)
const (
refScene = uint32(0x025138C6) // "[scene]"
refEnding = uint32(0x025138D6) // "[ending]"
refLevel = uint32(0x003566E2) // "[level]"
)
func u32(b []byte) uint32 { return binary.LittleEndian.Uint32(b) }
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 dir")
dry := flag.Bool("dry-run", false, "只检查")
// 区域所在的段序号(1 起)。这些段的 [scene] 改为"该段影片序号"。
regionSegs := flag.String("region-segs", "1,2,3,4,5,6,7,9,10,11,12,13,14", "对应开区区域的段序号")
fillerScene := flag.Uint("filler-scene", 0, "填充段使用的回顾段号")
shiftSegs := flag.String("shift-segs", "", "只对这些段应用 -scene-offset(留空=所有区域段)")
sceneOffset := flag.Int("scene-offset", 0, "对目标段的 [scene] 增减值")
flag.Parse()
shift := map[int]bool{}
for _, s := range splitInts(*shiftSegs) {
shift[s] = true
}
applyShift := func(seg int) bool {
if *sceneOffset == 0 {
return false
}
if len(shift) == 0 {
return true
}
return shift[seg]
}
want := map[int]bool{}
for _, s := range splitInts(*regionSegs) {
want[s] = true
}
archive, err := platformpvf.Open(*input)
if err != nil {
panic(err)
}
raw, err := archive.ReadRaw("etc/storydigest/digest.etc")
if err != nil {
panic(err)
}
toks := len(raw) / 5
isSec := func(i int, ref uint32) bool {
return i < toks && raw[i*5] == 3 && u32(raw[i*5+1:i*5+5]) == ref
}
// 抽出每段当前的 [scene] 与 [ending](段序号 1 起)
sceneBySeg := map[int]uint32{}
endingBySeg := map[int]uint32{}
{
n := 0
for k := 0; k+1 < toks; k++ {
if isSec(k, refLevel) {
n++
}
if isSec(k, refEnding) {
endingBySeg[n] = u32(raw[(k+1)*5+1 : (k+1)*5+5])
}
if isSec(k, refScene) && raw[(k+1)*5] == 0 {
sceneBySeg[n] = u32(raw[(k+1)*5+1 : (k+1)*5+5])
}
}
}
segIndex := 0
var out []byte
i := 0
for i < toks {
if isSec(i, refLevel) {
segIndex++
out = append(out, raw[i*5:(i+2)*5+5]...)
i += 3
continue
}
if isSec(i, refScene) {
out = append(out, raw[i*5:i*5+5]...)
i++
for i < toks && raw[i*5] == 0 {
i++
}
var newScene uint32
if want[segIndex] {
cur := sceneBySeg[segIndex]
if applyShift(segIndex) {
if *sceneOffset >= 0 {
newScene = cur + uint32(*sceneOffset)
} else {
d := uint32(-*sceneOffset)
if cur > d {
newScene = cur - d
} else {
newScene = 0
}
}
} else {
newScene = cur
}
} else {
newScene = uint32(*fillerScene)
}
out = append(out, tok5(0, newScene)...)
continue
}
out = append(out, raw[i*5:i*5+5]...)
i++
}
fmt.Printf("处理 %d 段,新大小 %d 字节\n", segIndex, len(out))
// 校验 ending 不变
nt := len(out) / 5
oe, ne := []uint32{}, []uint32{}
for k := 0; k+1 < toks; k++ {
if isSec(k, refEnding) {
oe = append(oe, u32(raw[(k+1)*5+1:(k+1)*5+5]))
}
}
for k := 0; k+1 < nt; k++ {
if out[k*5] == 3 && u32(out[k*5+1:k*5+5]) == refEnding {
ne = append(ne, u32(out[(k+1)*5+1:(k+1)*5+5]))
}
}
if len(oe) != len(ne) {
panic("ending 数量不一致")
}
for k := range oe {
if oe[k] != ne[k] {
panic(fmt.Sprintf("第 %d 个 ending 变了", k+1))
}
}
fmt.Println("[ending] 全部保持原值 ✅")
// 打印结果
n := 0
for k := 0; k+1 < nt; k++ {
if out[k*5] == 3 && u32(out[k*5+1:k*5+5]) == refLevel {
n++
var sc uint32
for j := k; j+1 < nt; j++ {
if out[j*5] == 3 && u32(out[j*5+1:j*5+5]) == refScene && out[(j+1)*5] == 0 {
sc = u32(out[(j+1)*5+1 : (j+1)*5+5])
break
}
}
mark := "填充"
if want[n] {
mark = "区域"
}
fmt.Printf(" 段%-3d [scene] %-4d [ending] %-4d %s\n", n, sc, endingBySeg[n], mark)
}
}
if *dry {
fmt.Println("dry-run,未写文件")
return
}
full := filepath.Join(*dir, "etc", "storydigest", "digest.etc")
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
panic(err)
}
if err := os.WriteFile(full, out, 0o644); err != nil {
panic(err)
}
fmt.Println("已写出", full, len(out), "字节")
}
func splitInts(s string) []int {
var out []int
cur := 0
has := false
for _, r := range s {
if r >= '0' && r <= '9' {
cur = cur*10 + int(r-'0')
has = true
continue
}
if has {
out = append(out, cur)
cur, has = 0, false
}
}
if has {
out = append(out, cur)
}
return out
}