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

+147
View File
@@ -0,0 +1,147 @@
// pvfnpc 在 .twn 指向的地图文件里查找 [NPC] 段中的指定 NPC id。
//
// 背景:建会 NPC「博肯」= npc/npc.lst 里的 15 `Board.npc`。
// 地图 [NPC] 段记录形如:
//
// 1 `[left]` 1227 164 0 592 `[left]` 351 192 0
// 16 800 400 0
//
// 即 "id [方向] x y z" 的可重复序列(方向可省)。这里按字段扫描。
//
// go run ./cmd/scratch/pvfnpc -pvf <path> -npc 15
package main
import (
"flag"
"fmt"
"os"
"regexp"
"sort"
"strconv"
"strings"
platformpvf "longheng.io/server/internal/platform/pvf"
)
var (
areaRe = regexp.MustCompile("`([^`]+\\.map)`")
intRe = regexp.MustCompile(`\d+`)
quoteRe = regexp.MustCompile("`\\[[^`]*\\]`")
)
func main() {
pvfPath := flag.String("pvf", `../runtime/data/dnf/Script.pvf`, "Script.pvf path")
npcID := flag.Int64("npc", 15, "要查找的 NPC id")
verbose := flag.Bool("v", false, "打印每个城镇扫描了多少地图")
flag.Parse()
archive, err := platformpvf.Open(*pvfPath)
if err != nil {
fmt.Fprintln(os.Stderr, "open:", err)
os.Exit(1)
}
townList, err := archive.ReadText("town/town.lst")
if err != nil {
fmt.Fprintln(os.Stderr, "read town.lst:", err)
os.Exit(1)
}
towns := []string{}
for _, m := range regexp.MustCompile("`([^`]+\\.twn)`").FindAllStringSubmatch(townList, -1) {
towns = append(towns, "town/"+m[1])
}
index := map[string]int{}
all := archive.Files()
for _, f := range all {
index[strings.ToLower(f.ArchivePath)] = f.Index
}
fmt.Printf("城镇数=%d 归档文件=%d 查找 NPC=%d\n\n", len(towns), len(all), *npcID)
found := 0
scannedMaps := map[string]bool{}
for _, twn := range towns {
twnText, err := archive.ReadText(twn)
if err != nil {
continue
}
var maps []string
for _, m := range areaRe.FindAllStringSubmatch(twnText, -1) {
maps = append(maps, m[1])
}
hits := []string{}
for _, mp := range maps {
// .twn 里写的是 "Cataclysm/Town/...",归档里在 "map/" 前缀下。
candidates := []string{
strings.ToLower(mp),
strings.ToLower("map/" + mp),
}
resolved := false
for _, cand := range candidates {
idx, ok := index[cand]
if !ok {
continue
}
resolved = true
scannedMaps[cand] = true
text, err := archive.FileText(idx)
if err != nil {
continue
}
if npcIDInMap(text, *npcID) {
hits = append(hits, cand)
}
break
}
if !resolved && *verbose {
fmt.Printf(" [缺失] %s\n", mp)
}
}
if *verbose {
fmt.Printf("%-42s 地图 %d 命中 %d\n", twn, len(maps), len(hits))
}
if len(hits) > 0 {
found++
fmt.Printf("%s\n", twn)
for _, h := range hits {
fmt.Printf(" %s\n", h)
}
}
}
if found == 0 {
fmt.Printf("没有城镇地图的 [NPC] 段包含 NPC %d(已扫描 %d 张地图)\n", *npcID, len(scannedMaps))
} else {
fmt.Printf("\n共 %d 个城镇包含 NPC %d\n", found, *npcID)
}
sort.Strings(towns)
}
func npcIDInMap(mapText string, id int64) bool {
inNPC := false
for _, line := range strings.Split(strings.ReplaceAll(mapText, "\r\n", "\n"), "\n") {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "[") {
inNPC = strings.EqualFold(trimmed, "[npc]")
continue
}
if !inNPC {
continue
}
// 记录之间用 `[方向]` 或空白分隔。先把 `[方向]` 当分隔符拆开,
// 再按块取每块第一个整数。
for _, chunk := range quoteRe.Split(line, -1) {
m := intRe.FindString(chunk)
if m == "" {
continue
}
got, err := strconv.ParseInt(m, 10, 64)
if err != nil {
continue
}
if got == id {
return true
}
}
}
return false
}