// 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 -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 }