服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
234 lines
6.5 KiB
Go
234 lines
6.5 KiB
Go
// Package storydigest 提供 etc/storydigest/digest.etc 的只读目录。
|
||
//
|
||
// 这张表定义「章节过场动画」的播放边界。每段结构:
|
||
//
|
||
// [digest]
|
||
// `5` ← 段落号
|
||
// [level]
|
||
// 40 45 ← 该段对应的角色等级区间
|
||
// [opening]
|
||
// 0
|
||
// [scene]
|
||
// 1 2 3 4 5 ← 该段播放文字摘要时要展示的场景
|
||
// [ending]
|
||
// 905 ← 段落结束时要播放的影片段号
|
||
// `6` ← 下一段由此开始(同一 [digest] 块内续写)
|
||
//
|
||
// etc/storydigest/scene.etc 把段号映射到内容:段 N(N=1..17)是第 N 章的
|
||
// 文字摘要,段 900+N 是 `[video]` 影片段。因此 `[ending] 905` 对应的影片段
|
||
// 就是 scene.etc 里的 905 → Video/movie/origin_cinematic_05.avi。
|
||
//
|
||
// 客户端 sub_11DBE40 的判定:比较「由 last_level 选出的小段」与「由角色当前
|
||
// 等级选出的小段」,不同才播。所以要让客户端播某段结尾的影片,服务端需要
|
||
// 把 last_level 下发到该段的 [level] 下界(该段起点)。
|
||
package storydigest
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"sort"
|
||
"strings"
|
||
|
||
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
|
||
)
|
||
|
||
// DefaultDigestPath 是章节摘要段表。
|
||
const DefaultDigestPath = "etc/storydigest/digest.etc"
|
||
|
||
// ErrSourceRequired 表示没有传入可读的 PVF 源。
|
||
var ErrSourceRequired = errors.New("dnf story digest pvf source is required")
|
||
|
||
// Source 是读取 PVF 文本的最小接口(与 town 等包保持一致)。
|
||
type Source interface {
|
||
ReadText(string) (string, error)
|
||
}
|
||
|
||
// Segment 是 digest.etc 的一个 [digest] 段。
|
||
type Segment struct {
|
||
// Index 是段号(1..17)。
|
||
Index int
|
||
// LevelMin/LevelMax 是该段对应的角色等级区间。
|
||
LevelMin int
|
||
LevelMax int
|
||
// Scenes 是该段要展示的文字摘要场景号。
|
||
Scenes []int
|
||
// Ending 是段落结束时要播放的影片段号(scene.etc 里的 900+N)。
|
||
// 0 表示该段没有结尾影片。
|
||
Ending int
|
||
}
|
||
|
||
// Digest 是段表。
|
||
type Digest struct {
|
||
segments []Segment
|
||
}
|
||
|
||
// Segments 按段号升序返回全部段。
|
||
func (d *Digest) Segments() []Segment {
|
||
if d == nil {
|
||
return nil
|
||
}
|
||
out := make([]Segment, len(d.segments))
|
||
copy(out, d.segments)
|
||
return out
|
||
}
|
||
|
||
// Len 返回段数。
|
||
func (d *Digest) Len() int {
|
||
if d == nil {
|
||
return 0
|
||
}
|
||
return len(d.segments)
|
||
}
|
||
|
||
// SegmentForEnding 返回以该影片段号结尾的段。
|
||
//
|
||
// 用途:城镇区域开启后要知道「该区域的过场影片属于哪一段」,从而把
|
||
// last_level 下发到那段,让客户端在段结束处播放影片。
|
||
func (d *Digest) SegmentForEnding(ending int) (Segment, bool) {
|
||
if d == nil || ending <= 0 {
|
||
return Segment{}, false
|
||
}
|
||
for _, segment := range d.segments {
|
||
if segment.Ending == ending {
|
||
return segment, true
|
||
}
|
||
}
|
||
return Segment{}, false
|
||
}
|
||
|
||
// SegmentForLevel 返回包含该角色等级的段(等级区间命中)。
|
||
func (d *Digest) SegmentForLevel(level int) (Segment, bool) {
|
||
if d == nil {
|
||
return Segment{}, false
|
||
}
|
||
for _, segment := range d.segments {
|
||
if level >= segment.LevelMin && level <= segment.LevelMax {
|
||
return segment, true
|
||
}
|
||
}
|
||
return Segment{}, false
|
||
}
|
||
|
||
// Load 读取 etc/storydigest/digest.etc。
|
||
//
|
||
// 文件缺失不算错误:此时没有任何段,过场不会被触发。
|
||
func Load(source Source, path string) (*Digest, error) {
|
||
if source == nil {
|
||
return nil, ErrSourceRequired
|
||
}
|
||
if strings.TrimSpace(path) == "" {
|
||
path = DefaultDigestPath
|
||
}
|
||
text, err := source.ReadText(path)
|
||
if err != nil {
|
||
return &Digest{}, nil
|
||
}
|
||
doc, err := dnfpvf.Parse(path, text)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("parse story digest %s: %w", path, err)
|
||
}
|
||
return parseDigest(doc)
|
||
}
|
||
|
||
// parseDigest 解析 digest.etc。
|
||
//
|
||
// 该文件的写法比较特殊:[digest] 只出现一次,段与段之间**没有闭合标记** ——
|
||
// 段号以 `` `N` `` 形式直接写在上一段的 [ending] 值之后。因此这里按 token
|
||
// 顺序扫描:遇到 [digest] 段头开始,之后每遇到一个「[level] 段头」就收一段。
|
||
func parseDigest(doc *dnfpvf.Document) (*Digest, error) {
|
||
result := &Digest{}
|
||
if doc == nil || len(doc.Tokens) == 0 {
|
||
return result, nil
|
||
}
|
||
tokens := doc.Tokens
|
||
index := -1
|
||
for i := 0; i < len(tokens); i++ {
|
||
if tokens[i].Kind == dnfpvf.TokenSection && strings.EqualFold(strings.TrimSpace(tokens[i].Value), "digest") {
|
||
index = i + 1
|
||
break
|
||
}
|
||
}
|
||
if index < 0 {
|
||
return result, nil
|
||
}
|
||
|
||
var current *Segment
|
||
for i := index; i < len(tokens); i++ {
|
||
token := tokens[i]
|
||
if token.Kind == dnfpvf.TokenSection {
|
||
switch strings.ToLower(strings.TrimSpace(token.Value)) {
|
||
case "level":
|
||
// 每个 [level] 开启新的一段。段号是它前面最近的一个「裸字符串」
|
||
// (形如 `5`),也就是上一段 [ending] 之后紧跟的那个 `` `N` ``。
|
||
segment := Segment{Index: bareIntBefore(tokens, i)}
|
||
values := intsUntilSection(tokens, i+1)
|
||
if len(values) >= 1 {
|
||
segment.LevelMin = values[0]
|
||
}
|
||
if len(values) >= 2 {
|
||
segment.LevelMax = values[1]
|
||
}
|
||
result.segments = append(result.segments, segment)
|
||
current = &result.segments[len(result.segments)-1]
|
||
case "scene":
|
||
if current != nil {
|
||
current.Scenes = append(current.Scenes, intsUntilSection(tokens, i+1)...)
|
||
}
|
||
case "ending":
|
||
if current != nil {
|
||
if values := intsUntilSection(tokens, i+1); len(values) >= 1 {
|
||
current.Ending = int(values[0])
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
sort.SliceStable(result.segments, func(a, b int) bool {
|
||
return result.segments[a].Index < result.segments[b].Index
|
||
})
|
||
return result, nil
|
||
}
|
||
|
||
// bareIntBefore 返回 tokens[before] 之前最近的一个裸字符串里蕴含的整数
|
||
// (digest.etc 用 `` `5` `` 写段号)。找不到返回 -1。
|
||
func bareIntBefore(tokens []dnfpvf.Token, before int) int {
|
||
for i := before - 1; i >= 0; i-- {
|
||
token := tokens[i]
|
||
if token.Kind != dnfpvf.TokenString && token.Kind != dnfpvf.TokenIdent {
|
||
continue
|
||
}
|
||
value := strings.Trim(strings.TrimSpace(token.Value), "`")
|
||
if value == "" {
|
||
continue
|
||
}
|
||
n := 0
|
||
ok := true
|
||
for _, r := range value {
|
||
if r < '0' || r > '9' {
|
||
ok = false
|
||
break
|
||
}
|
||
n = n*10 + int(r-'0')
|
||
}
|
||
if ok {
|
||
return n
|
||
}
|
||
// 只回看一步:段号紧邻 [level]。
|
||
return -1
|
||
}
|
||
return -1
|
||
}
|
||
|
||
func intsUntilSection(tokens []dnfpvf.Token, start int) []int {
|
||
values := make([]int, 0, 2)
|
||
for i := start; i < len(tokens); i++ {
|
||
if tokens[i].Kind == dnfpvf.TokenSection {
|
||
break
|
||
}
|
||
if tokens[i].Kind == dnfpvf.TokenInt {
|
||
values = append(values, int(tokens[i].Int))
|
||
}
|
||
}
|
||
return values
|
||
}
|