// Command pvfexport extracts an item_id -> display-name map from a DNF // Script.pvf archive. It reads the stackable.lst and equipment.lst indexes, // resolves each item file, and pulls the [name] section via ScriptMetadata. // // Usage: // // pvfexport -pvf Script.pvf -out items.json package main import ( "encoding/json" "flag" "fmt" "log" "os" "strconv" "strings" "pvfine/internal/pvf" ) type itemEntry struct { ID int64 `json:"id"` Name string `json:"name"` Rarity int32 `json:"rarity"` Path string `json:"path,omitempty"` Kind string `json:"kind,omitempty"` } type exportResult struct { PVFPath string `json:"pvf_path"` Total int `json:"total"` Items []itemEntry `json:"items"` } func main() { pvfPath := flag.String("pvf", "Script.pvf", "path to Script.pvf") outPath := flag.String("out", "items.json", "output JSON path") compact := flag.Bool("compact", false, "输出紧凑的 {item_id: name} 映射(仅保留有名称的项)") flag.Parse() archive, err := pvf.Open(*pvfPath) if err != nil { log.Fatalf("打开 PVF 失败: %v", err) } defer archive.Release() items := make([]itemEntry, 0, 200000) seen := make(map[int64]bool) // 顺序处理两类物品列表,堆叠道具优先。 for _, list := range []struct { root string path string kind string }{ {"stackable", "stackable/stackable.lst", "stackable"}, {"equipment", "equipment/equipment.lst", "equipment"}, } { count, err := exportList(archive, list.root, list.path, list.kind, seen, &items) if err != nil { log.Printf("警告: 处理 %s 失败: %v", list.path, err) continue } log.Printf("%s: 解析 %d 条", list.path, count) } result := exportResult{ PVFPath: *pvfPath, Total: len(items), Items: items, } var data []byte if *compact { // 紧凑映射:{item_id: name},仅保留有名称的项 compactMap := make(map[string]string, len(items)) for _, it := range items { name := strings.TrimSpace(it.Name) if name != "" { compactMap[strconv.FormatInt(it.ID, 10)] = name } } data, err = json.Marshal(compactMap) } else { data, err = json.MarshalIndent(result, "", " ") } if err != nil { log.Fatalf("序列化 JSON 失败: %v", err) } if err := os.WriteFile(*outPath, data, 0o644); err != nil { log.Fatalf("写入 %s 失败: %v", *outPath, err) } fmt.Printf("导出完成: %d 个物品 -> %s\n", len(items), *outPath) } func exportList(archive *pvf.Archive, root, listPath, kind string, seen map[int64]bool, items *[]itemEntry) (int, error) { idx, found := archive.Find(listPath) if !found { return 0, fmt.Errorf("未找到 %s", listPath) } text, err := archive.Text(idx) if err != nil { return 0, fmt.Errorf("读取 %s: %w", listPath, err) } pairs, err := pvf.ParseListText(text) if err != nil { return 0, fmt.Errorf("解析 %s: %w", listPath, err) } count := 0 for _, pair := range pairs { id, err := strconv.ParseInt(strings.TrimSpace(pair.ID), 10, 64) if err != nil || id <= 0 { continue } if seen[id] { continue } path := cleanPath(pair.Path) itemIdx, ok := resolveItemFile(archive, root, path) if !ok { continue } meta, err := archive.ScriptMetadata(itemIdx) if err != nil { continue } seen[id] = true count++ *items = append(*items, itemEntry{ ID: id, Name: meta.Name, Rarity: meta.Rarity, Path: path, Kind: kind, }) } return count, nil } // resolveItemFile 复刻服务端 readDungeonDropText 的候选路径策略: // 先直接用 lst 里的路径,再尝试 root/ 前缀,最后尝试 (r) 修订文件名。 func resolveItemFile(archive *pvf.Archive, root, listedPath string) (int32, bool) { clean := cleanPath(listedPath) candidates := []string{clean} root = cleanPath(root) if root != "" && !strings.HasPrefix(strings.ToLower(clean), strings.ToLower(root)+"/") { candidates = append(candidates, root+"/"+clean) } for _, candidate := range append([]string(nil), candidates...) { dir, base := splitPath(candidate) if base == "" || strings.HasPrefix(strings.ToLower(base), "(r)") { continue } candidates = append(candidates, dir+"(r)"+base) } for _, candidate := range candidates { if idx, found := archive.Find(candidate); found { return idx, true } } return 0, false } func cleanPath(value string) string { value = strings.TrimSpace(strings.ReplaceAll(value, "\\", "/")) value = strings.TrimPrefix(value, "./") return strings.TrimPrefix(value, "/") } func splitPath(value string) (string, string) { idx := strings.LastIndex(value, "/") if idx < 0 { return "", value } return value[:idx+1], value[idx+1:] }