服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
148 lines
3.6 KiB
Go
148 lines
3.6 KiB
Go
// 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
|
|
}
|