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

189 lines
5.6 KiB
Go

// pvfdisableattendance 把客户端 Script.pvf 里「累计在线得好礼」(event 2347) 的
// 活动定义清空,使客户端不再显示该活动。
//
// 目标文件:event/chn_event/chn_attendanceevent.evt
//
// [process time seconds] 1800 1800 3600 3600 ← 四阶段时长
// [reward item] 490006574..490006577 ← 阶段奖励
// [process seconds for max count] 5 10 15 25 ← 25 次满阶段
// [reward item for sum] 490006578..490006581 ← 满阶段奖励
//
// 做法:把 [reward item]、[reward item for sum] 两个段的**内容行**清空(保留段标签),
// 并把 [process time seconds] 的数值改为一个极大值,使阶段永远不可达。
//
// 刻意保持**总字节数不变**(用空格填充),因为归档重打包对文件长度变化更敏感;
// 等长替换把风险降到最低。
//
// 用法:
//
// go run ./cmd/scratch/pvfdisableattendance -pvf <in.pvf> -out <out.pvf>
package main
import (
"flag"
"fmt"
"os"
"regexp"
"strings"
platformpvf "longheng.io/server/internal/platform/pvf"
)
const targetPath = "event/chn_event/chn_attendanceevent.evt"
func main() {
pvfPath := flag.String("pvf", "", "源 Script.pvf")
outPath := flag.String("out", "", "输出 Script.pvf(不覆盖源文件)")
flag.Parse()
if *pvfPath == "" || *outPath == "" {
fmt.Fprintln(os.Stderr, "usage: pvfdisableattendance -pvf <in> -out <out>")
os.Exit(2)
}
archive, err := platformpvf.Open(*pvfPath)
if err != nil {
fmt.Fprintln(os.Stderr, "open:", err)
os.Exit(1)
}
text, err := archive.ReadText(targetPath)
if err != nil {
fmt.Fprintln(os.Stderr, "read target:", err)
os.Exit(1)
}
fmt.Printf("原文件 %d 字节\n", len(text))
fmt.Printf("---- 原文 ----\n%s\n--------------\n", text)
patched := disableActivity(text)
if len(patched) != len(text) {
fmt.Fprintf(os.Stderr, "!! 长度变化 %d -> %d,中止\n", len(text), len(patched))
os.Exit(1)
}
fmt.Printf("---- 改后(等长 %d 字节)----\n%s\n--------------\n", len(patched), patched)
raw, err := archive.RepackRaw([]platformpvf.RawReplacement{{Path: targetPath, Data: []byte(patched)}})
if err != nil {
fmt.Fprintln(os.Stderr, "repack:", err)
os.Exit(1)
}
if err := os.WriteFile(*outPath, raw, 0o644); err != nil {
fmt.Fprintln(os.Stderr, "write:", err)
os.Exit(1)
}
fmt.Printf("已写出: %s (%d 字节)\n", *outPath, len(raw))
}
// disableActivity 在等长前提下清空奖励内容。
func disableActivity(text string) string {
out := text
// 逐段处理:段标签之间的内容行替换为等长空白。
out = blankSectionBody(out, "reward item")
out = blankSectionBody(out, "reward item for sum")
// [process time seconds] 的数值改为极大值(等长:用同样位数的数字)。
out = rewriteSectionNumbers(out, "process time seconds", "999999999")
out = rewriteSectionNumbers(out, "process seconds for max count", "999999999")
return out
}
// blankSectionBody 把 [name] ... [/name] 之间的非空内容行清成空格(长度不变)。
// 段落名匹配用宽松规则:"[reward item]" 与 "[reward item for sum]" 都要能命中,
// 所以按精确标签匹配。
func blankSectionBody(text, name string) string {
open := "[" + name + "]"
closeTag := "[/" + name + "]"
start := indexOfLine(text, open)
if start < 0 {
fmt.Printf(" 提示:未找到段 %s\n", open)
return text
}
bodyStart := start + len(open)
end := strings.Index(text[bodyStart:], closeTag)
if end < 0 {
fmt.Printf(" 提示:段 %s 没有闭合标签\n", open)
return text
}
body := text[bodyStart : bodyStart+end]
// 逐行:保留换行,把可见字符换成空格。
var b strings.Builder
b.Grow(len(body))
for _, line := range strings.SplitAfter(body, "\n") {
nl := ""
content := line
if strings.HasSuffix(line, "\n") {
nl = "\n"
content = strings.TrimSuffix(line, "\n")
}
b.WriteString(strings.Repeat(" ", len(content)))
b.WriteString(nl)
}
return text[:bodyStart] + b.String() + text[bodyStart+end:]
}
// rewriteSectionNumbers 把段内所有数字替换为 given(逐行等长填充)。
func rewriteSectionNumbers(text, name, value string) string {
open := "[" + name + "]"
closeTag := "[/" + name + "]"
start := indexOfLine(text, open)
if start < 0 {
fmt.Printf(" 提示:未找到段 %s\n", open)
return text
}
bodyStart := start + len(open)
end := strings.Index(text[bodyStart:], closeTag)
if end < 0 {
return text
}
body := text[bodyStart : bodyStart+end]
numRe := regexp.MustCompile(`\d+`)
var b strings.Builder
b.Grow(len(body))
for _, line := range strings.SplitAfter(body, "\n") {
nl := ""
content := line
if strings.HasSuffix(line, "\n") {
nl = "\n"
content = strings.TrimSuffix(line, "\n")
}
replaced := numRe.ReplaceAllStringFunc(content, func(m string) string {
// 等长:用 value 的前 len(m) 个字符;若 value 更短则补 '9'
if len(m) <= len(value) {
return value[:len(m)]
}
return strings.Repeat("9", len(m))
})
// 保持行长度
if len(replaced) < len(content) {
replaced += strings.Repeat(" ", len(content)-len(replaced))
}
b.WriteString(replaced)
b.WriteString(nl)
}
return text[:bodyStart] + b.String() + text[bodyStart+end:]
}
// indexOfLine 找到以给定标签开头的那一行的偏移。
func indexOfLine(text, tag string) int {
idx := 0
for {
at := strings.Index(text[idx:], tag)
if at < 0 {
return -1
}
at += idx
// 必须位于行首(允许前导空白)
lineStart := strings.LastIndexByte(text[:at], '\n') + 1
if strings.TrimSpace(text[lineStart:at]) == "" {
return at
}
idx = at + len(tag)
}
}