Files
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

765 lines
22 KiB
Go

package services
import (
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"sync"
"github.com/wailsapp/wails/v3/pkg/application"
appconfig "pvfine/config"
)
const (
bookmarkDocumentVersion = 1
builtinBookmarkBookID = "builtin"
bookmarkSourcePath = "config/bookmarks.json"
)
// BookmarkEntry is one PVF-internal file bookmark.
type BookmarkEntry struct {
Path string `json:"path"`
Name string `json:"name"`
}
// BookmarkGroup is a recursively nested bookmark group.
type BookmarkGroup struct {
ID string `json:"id"`
Name string `json:"name"`
Groups []BookmarkGroup `json:"groups,omitempty"`
Entries []BookmarkEntry `json:"entries,omitempty"`
}
// BookmarkBook is a runtime bookmark book. Builtin books are read-only in
// packaged applications and editable when running from the repository.
type BookmarkBook struct {
ID string `json:"id"`
Name string `json:"name"`
Builtin bool `json:"builtin"`
Editable bool `json:"editable"`
Groups []BookmarkGroup `json:"groups,omitempty"`
Entries []BookmarkEntry `json:"entries,omitempty"`
}
// BookmarkDocument is the complete runtime bookmark state.
type BookmarkDocument struct {
Version int `json:"version"`
ActiveBookID string `json:"activeBookId"`
Books []BookmarkBook `json:"books"`
}
// BookmarkBookFile is the portable JSON representation of one bookmark book.
// IDs and runtime flags are intentionally omitted so imports always receive
// fresh local identities and are treated as editable books.
type BookmarkBookFile struct {
Version int `json:"version"`
Name string `json:"name"`
Groups []BookmarkGroup `json:"groups,omitempty"`
Entries []BookmarkEntry `json:"entries,omitempty"`
}
type storedBookmarkBook struct {
ID string `json:"id"`
Name string `json:"name"`
Groups []BookmarkGroup `json:"groups,omitempty"`
Entries []BookmarkEntry `json:"entries,omitempty"`
}
type storedBookmarkDocument struct {
Version int `json:"version"`
ActiveBookID string `json:"activeBookId"`
Books []storedBookmarkBook `json:"books"`
}
// BookmarkService persists user books and supplies the embedded system book.
type BookmarkService struct {
mu sync.Mutex
path string
sourcePath string
builtin BookmarkBook
initErr error
}
// NewBookmarkService creates the application bookmark service. A repository
// source file takes precedence during local development, mirroring the
// existing annotation configuration behavior.
func NewBookmarkService() *BookmarkService {
configDir, err := os.UserConfigDir()
if err != nil {
return &BookmarkService{initErr: fmt.Errorf("获取用户配置目录失败: %w", err)}
}
sourcePath, _ := findBookmarkSourcePath()
return newBookmarkServiceWithSource(filepath.Join(configDir, "pvfine", "bookmarks.json"), sourcePath)
}
func newBookmarkService(path string) *BookmarkService {
return newBookmarkServiceWithSource(path, "")
}
func newBookmarkServiceWithSource(path, sourcePath string) *BookmarkService {
service := &BookmarkService{path: path, sourcePath: sourcePath}
data := appconfig.BookmarksJSON
if sourcePath != "" {
var err error
data, err = os.ReadFile(sourcePath)
if err != nil {
service.initErr = fmt.Errorf("读取内置书签配置失败: %w", err)
return service
}
}
builtin, err := parseBuiltinBookmarkBook(data)
if err != nil {
service.initErr = err
return service
}
builtin.Editable = sourcePath != ""
service.builtin = builtin
return service
}
// LoadBookmarks loads custom books and injects the current built-in book.
func (s *BookmarkService) LoadBookmarks() (BookmarkDocument, error) {
s.mu.Lock()
defer s.mu.Unlock()
if s.initErr != nil {
return BookmarkDocument{}, s.initErr
}
stored := storedBookmarkDocument{Version: bookmarkDocumentVersion, ActiveBookID: builtinBookmarkBookID}
data, err := os.ReadFile(s.path)
if err == nil {
if err := decodeBookmarkJSON(data, &stored, "书签簿"); err != nil {
return BookmarkDocument{}, err
}
} else if !os.IsNotExist(err) {
return BookmarkDocument{}, fmt.Errorf("读取书签簿失败: %w", err)
}
custom, activeID, err := normalizeStoredBookmarkDocument(stored, s.builtin)
if err != nil {
return BookmarkDocument{}, err
}
books := make([]BookmarkBook, 0, len(custom)+1)
books = append(books, cloneBookmarkBook(s.builtin))
for _, storedBook := range custom {
book := BookmarkBook{
ID: storedBook.ID,
Name: storedBook.Name,
Editable: true,
Groups: cloneBookmarkGroups(storedBook.Groups),
Entries: cloneBookmarkEntries(storedBook.Entries),
}
books = append(books, book)
}
if !bookmarkBookIDExists(books, activeID) {
activeID = builtinBookmarkBookID
}
return BookmarkDocument{
Version: bookmarkDocumentVersion,
ActiveBookID: activeID,
Books: books,
}, nil
}
// SaveBookmarks persists custom books and, in development mode, writes the
// built-in book back to config/bookmarks.json.
func (s *BookmarkService) SaveBookmarks(document BookmarkDocument) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.initErr != nil {
return s.initErr
}
if document.Version == 0 {
document.Version = bookmarkDocumentVersion
}
if document.Version != bookmarkDocumentVersion {
return fmt.Errorf("不支持的书签簿版本: %d", document.Version)
}
builtin := cloneBookmarkBook(s.builtin)
custom := make([]storedBookmarkBook, 0, len(document.Books))
seenBookIDs := make(map[string]struct{}, len(document.Books))
seenBookNames := map[string]struct{}{builtin.Name: {}}
var suppliedBuiltin *BookmarkBook
for index := range document.Books {
book := document.Books[index]
book.ID = strings.TrimSpace(book.ID)
if book.ID == builtinBookmarkBookID {
if !book.Builtin {
return fmt.Errorf("保留的系统书签簿 id 不能用于自定义书签簿")
}
if suppliedBuiltin != nil {
return fmt.Errorf("系统书签簿 id 重复")
}
suppliedBuiltin = &book
continue
}
if book.Builtin {
return fmt.Errorf("只有固定 id 的书签簿可以标记为内置")
}
if _, exists := seenBookIDs[book.ID]; exists {
return fmt.Errorf("书签簿 id 重复: %q", book.ID)
}
seenBookIDs[book.ID] = struct{}{}
book.Editable = true
if err := normalizeBookmarkBook(&book, false, book.ID); err != nil {
return err
}
if _, exists := seenBookNames[book.Name]; exists {
return fmt.Errorf("书签簿名称不能重复: %q", book.Name)
}
seenBookNames[book.Name] = struct{}{}
custom = append(custom, storedBookmarkBook{
ID: book.ID,
Name: book.Name,
Groups: cloneBookmarkGroups(book.Groups),
Entries: cloneBookmarkEntries(book.Entries),
})
}
if suppliedBuiltin != nil {
candidate := cloneBookmarkBook(*suppliedBuiltin)
candidate.ID = builtinBookmarkBookID
candidate.Builtin = true
if err := normalizeBookmarkBook(&candidate, true, "builtin"); err != nil {
return err
}
if s.sourcePath == "" {
if !sameBookmarkContent(candidate, builtin) {
return fmt.Errorf("生产模式下系统书签簿不可编辑")
}
} else if !sameBookmarkContent(candidate, builtin) {
if _, exists := seenBookNames[candidate.Name]; exists && candidate.Name != builtin.Name {
return fmt.Errorf("书签簿名称不能重复: %q", candidate.Name)
}
if err := writeBuiltinBookmarkFile(s.sourcePath, toBookmarkBookFile(candidate)); err != nil {
return err
}
builtin = candidate
builtin.Builtin = true
builtin.Editable = true
}
}
activeID := strings.TrimSpace(document.ActiveBookID)
if activeID != builtinBookmarkBookID && !storedBookmarkBookIDExists(custom, activeID) {
activeID = builtinBookmarkBookID
}
storedDocument := storedBookmarkDocument{
Version: bookmarkDocumentVersion,
ActiveBookID: activeID,
Books: custom,
}
if len(custom) == 0 {
if err := os.Remove(s.path); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("删除空书签簿文件失败: %w", err)
}
} else if err := writeBookmarkJSON(s.path, storedDocument, 0o600, ".bookmarks-*.tmp"); err != nil {
return fmt.Errorf("保存书签簿失败: %w", err)
}
s.builtin = builtin
return nil
}
// ImportBookmarkBookDialog opens and parses one portable bookmark book.
func (s *BookmarkService) ImportBookmarkBookDialog() (*BookmarkBookFile, error) {
paths, err := application.Get().Dialog.OpenFile().
CanChooseFiles(true).
CanChooseDirectories(false).
AddFilter("书签簿 JSON", "*.json").
SetTitle("导入书签簿").
PromptForSingleSelection()
if err != nil {
return nil, err
}
if paths == "" {
return nil, nil
}
data, err := os.ReadFile(paths)
if err != nil {
return nil, fmt.Errorf("读取书签簿导入文件失败: %w", err)
}
book, err := parseBookmarkBookFile(data)
if err != nil {
return nil, err
}
return &book, nil
}
// ExportBookmarkBookDialog saves one portable bookmark book selected by the
// frontend. Built-in books are intentionally exportable.
func (s *BookmarkService) ExportBookmarkBookDialog(book BookmarkBookFile) (string, error) {
if err := normalizeBookmarkBookFile(&book); err != nil {
return "", err
}
filename := safeBookmarkFilename(book.Name) + ".json"
path, err := application.Get().Dialog.SaveFile().
SetFilename(filename).
AddFilter("书签簿 JSON", "*.json").
SetMessage("导出书签簿").
PromptForSingleSelection()
if err != nil {
return "", err
}
if path == "" {
return "", nil
}
data, err := marshalBookmarkJSON(book)
if err != nil {
return "", err
}
if err := os.WriteFile(path, data, 0o644); err != nil {
return "", fmt.Errorf("写入书签簿导出文件失败: %w", err)
}
return path, nil
}
func parseBuiltinBookmarkBook(data []byte) (BookmarkBook, error) {
file, err := parseBookmarkBookFile(data)
if err != nil {
return BookmarkBook{}, fmt.Errorf("解析内置书签配置失败: %w", err)
}
book := BookmarkBook{
ID: builtinBookmarkBookID,
Name: file.Name,
Builtin: true,
Groups: file.Groups,
Entries: file.Entries,
}
if err := normalizeBookmarkBook(&book, true, "builtin"); err != nil {
return BookmarkBook{}, fmt.Errorf("内置书签配置无效: %w", err)
}
return book, nil
}
func parseBookmarkBookFile(data []byte) (BookmarkBookFile, error) {
var file BookmarkBookFile
if err := decodeBookmarkJSON(data, &file, "书签簿导入文件"); err != nil {
return BookmarkBookFile{}, err
}
if err := normalizeBookmarkBookFile(&file); err != nil {
return BookmarkBookFile{}, err
}
return file, nil
}
func normalizeBookmarkBookFile(file *BookmarkBookFile) error {
if file.Version == 0 {
file.Version = bookmarkDocumentVersion
}
if file.Version != bookmarkDocumentVersion {
return fmt.Errorf("不支持的书签簿版本: %d", file.Version)
}
book := BookmarkBook{
ID: "portable",
Name: file.Name,
Groups: file.Groups,
Entries: file.Entries,
}
if err := normalizeBookmarkBook(&book, false, "portable"); err != nil {
return err
}
file.Name = book.Name
file.Groups = book.Groups
file.Entries = book.Entries
return nil
}
func normalizeStoredBookmarkDocument(document storedBookmarkDocument, builtin BookmarkBook) ([]storedBookmarkBook, string, error) {
if document.Version == 0 {
document.Version = bookmarkDocumentVersion
}
if document.Version != bookmarkDocumentVersion {
return nil, "", fmt.Errorf("不支持的书签簿版本: %d", document.Version)
}
custom := make([]storedBookmarkBook, 0, len(document.Books))
seenIDs := make(map[string]struct{}, len(document.Books))
seenNames := map[string]struct{}{builtin.Name: {}}
for index := range document.Books {
stored := document.Books[index]
stored.ID = strings.TrimSpace(stored.ID)
if stored.ID == builtinBookmarkBookID {
candidate := BookmarkBook{
ID: builtinBookmarkBookID,
Name: stored.Name,
Builtin: true,
Groups: stored.Groups,
Entries: stored.Entries,
}
if err := normalizeBookmarkBook(&candidate, true, "builtin"); err != nil {
return nil, "", err
}
if !sameBookmarkContent(candidate, builtin) {
return nil, "", fmt.Errorf("用户配置中的系统书签簿与内置配置不一致")
}
continue
}
if stored.ID == "" {
return nil, "", fmt.Errorf("书签簿 id 为空")
}
if _, exists := seenIDs[stored.ID]; exists {
return nil, "", fmt.Errorf("书签簿 id 重复: %q", stored.ID)
}
seenIDs[stored.ID] = struct{}{}
book := BookmarkBook{ID: stored.ID, Name: stored.Name, Groups: stored.Groups, Entries: stored.Entries}
if err := normalizeBookmarkBook(&book, false, stored.ID); err != nil {
return nil, "", err
}
if _, exists := seenNames[book.Name]; exists {
return nil, "", fmt.Errorf("书签簿名称不能重复: %q", book.Name)
}
seenNames[book.Name] = struct{}{}
custom = append(custom, storedBookmarkBook{
ID: book.ID,
Name: book.Name,
Groups: book.Groups,
Entries: book.Entries,
})
}
activeID := strings.TrimSpace(document.ActiveBookID)
if activeID == "" {
activeID = builtinBookmarkBookID
}
return custom, activeID, nil
}
func normalizeBookmarkBook(book *BookmarkBook, builtin bool, idPrefix string) error {
book.ID = strings.TrimSpace(book.ID)
book.Name = strings.TrimSpace(book.Name)
if book.ID == "" || book.Name == "" {
return fmt.Errorf("书签簿名称或 id 为空")
}
if builtin && book.ID != builtinBookmarkBookID {
return fmt.Errorf("内置书签簿 id 无效: %q", book.ID)
}
if !builtin {
if book.ID == builtinBookmarkBookID {
return fmt.Errorf("保留的系统书签簿 id 不能用于自定义书签簿")
}
book.Builtin = false
book.Editable = true
}
usedGroupIDs := make(map[string]struct{})
groupCounter := 0
if err := normalizeBookmarkEntries(&book.Entries); err != nil {
return fmt.Errorf("书签簿 %q: %w", book.Name, err)
}
if err := normalizeBookmarkGroups(book.Groups, usedGroupIDs, &groupCounter, idPrefix); err != nil {
return fmt.Errorf("书签簿 %q: %w", book.Name, err)
}
return nil
}
func normalizeBookmarkGroups(groups []BookmarkGroup, usedIDs map[string]struct{}, counter *int, idPrefix string) error {
seenNames := make(map[string]struct{}, len(groups))
for index := range groups {
group := &groups[index]
group.Name = strings.TrimSpace(group.Name)
if group.Name == "" {
return fmt.Errorf("分组名称不能为空")
}
if _, exists := seenNames[group.Name]; exists {
return fmt.Errorf("同级分组名称不能重复: %q", group.Name)
}
seenNames[group.Name] = struct{}{}
group.ID = strings.TrimSpace(group.ID)
if group.ID == "" {
for {
(*counter)++
group.ID = fmt.Sprintf("%s-group-%d", idPrefix, *counter)
if _, exists := usedIDs[group.ID]; !exists {
break
}
}
}
if _, exists := usedIDs[group.ID]; exists {
return fmt.Errorf("分组 id 重复: %q", group.ID)
}
usedIDs[group.ID] = struct{}{}
if err := normalizeBookmarkEntries(&group.Entries); err != nil {
return fmt.Errorf("分组 %q: %w", group.Name, err)
}
if err := normalizeBookmarkGroups(group.Groups, usedIDs, counter, idPrefix); err != nil {
return fmt.Errorf("分组 %q: %w", group.Name, err)
}
}
return nil
}
func normalizeBookmarkEntries(entries *[]BookmarkEntry) error {
seenPaths := make(map[string]struct{}, len(*entries))
result := make([]BookmarkEntry, 0, len(*entries))
for _, entry := range *entries {
path, err := normalizeBookmarkPath(entry.Path)
if err != nil {
return err
}
if _, exists := seenPaths[path]; exists {
continue
}
seenPaths[path] = struct{}{}
entry.Path = path
entry.Name = strings.TrimSpace(entry.Name)
if entry.Name == "" {
entry.Name = bookmarkPathBase(path)
}
result = append(result, entry)
}
*entries = result
return nil
}
func normalizeBookmarkPath(raw string) (string, error) {
path := strings.Trim(strings.ReplaceAll(strings.TrimSpace(raw), "\\", "/"), "/")
if path == "" {
return "", fmt.Errorf("书签路径不能为空")
}
parts := strings.Split(path, "/")
clean := make([]string, 0, len(parts))
for _, part := range parts {
if part == "" {
continue
}
if part == "." || part == ".." {
return "", fmt.Errorf("书签路径包含非法分段: %q", raw)
}
clean = append(clean, part)
}
if len(clean) == 0 {
return "", fmt.Errorf("书签路径不能为空")
}
return strings.Join(clean, "/"), nil
}
func bookmarkPathBase(path string) string {
if slash := strings.LastIndexByte(path, '/'); slash >= 0 {
return path[slash+1:]
}
return path
}
func cloneBookmarkEntries(entries []BookmarkEntry) []BookmarkEntry {
if len(entries) == 0 {
return nil
}
return append([]BookmarkEntry(nil), entries...)
}
func cloneBookmarkGroups(groups []BookmarkGroup) []BookmarkGroup {
if len(groups) == 0 {
return nil
}
result := make([]BookmarkGroup, len(groups))
for index, group := range groups {
result[index] = BookmarkGroup{
ID: group.ID,
Name: group.Name,
Groups: cloneBookmarkGroups(group.Groups),
Entries: cloneBookmarkEntries(group.Entries),
}
}
return result
}
func cloneBookmarkBook(book BookmarkBook) BookmarkBook {
book.Groups = cloneBookmarkGroups(book.Groups)
book.Entries = cloneBookmarkEntries(book.Entries)
return book
}
func sameBookmarkContent(left, right BookmarkBook) bool {
return left.Name == right.Name &&
bookmarkGroupsEqual(left.Groups, right.Groups) &&
bookmarkEntriesEqual(left.Entries, right.Entries)
}
func bookmarkGroupsEqual(left, right []BookmarkGroup) bool {
if len(left) != len(right) {
return false
}
for index := range left {
if left[index].ID != right[index].ID || left[index].Name != right[index].Name ||
!bookmarkEntriesEqual(left[index].Entries, right[index].Entries) ||
!bookmarkGroupsEqual(left[index].Groups, right[index].Groups) {
return false
}
}
return true
}
func bookmarkEntriesEqual(left, right []BookmarkEntry) bool {
if len(left) != len(right) {
return false
}
for index := range left {
if left[index] != right[index] {
return false
}
}
return true
}
func bookmarkBookIDExists(books []BookmarkBook, id string) bool {
for _, book := range books {
if book.ID == id {
return true
}
}
return false
}
func storedBookmarkBookIDExists(books []storedBookmarkBook, id string) bool {
for _, book := range books {
if book.ID == id {
return true
}
}
return false
}
func toBookmarkBookFile(book BookmarkBook) BookmarkBookFile {
return BookmarkBookFile{
Version: bookmarkDocumentVersion,
Name: book.Name,
Groups: cloneBookmarkGroups(book.Groups),
Entries: cloneBookmarkEntries(book.Entries),
}
}
func decodeBookmarkJSON(data []byte, target any, label string) error {
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
if err := decoder.Decode(target); err != nil {
return fmt.Errorf("解析%s失败: %w", label, err)
}
if err := decoder.Decode(&struct{}{}); err != io.EOF {
if err == nil {
return fmt.Errorf("%s只能包含一个 JSON 文档", label)
}
return fmt.Errorf("解析%s失败: %w", label, err)
}
return nil
}
func marshalBookmarkJSON(value any) ([]byte, error) {
data, err := json.MarshalIndent(value, "", " ")
if err != nil {
return nil, fmt.Errorf("编码书签簿失败: %w", err)
}
return append(data, '\n'), nil
}
func writeBookmarkJSON(path string, value any, mode os.FileMode, pattern string) error {
data, err := marshalBookmarkJSON(value)
if err != nil {
return err
}
return atomicWriteBookmarkFile(path, data, mode, pattern)
}
func writeBuiltinBookmarkFile(path string, book BookmarkBookFile) error {
previous, err := os.ReadFile(path)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("读取内置书签配置备份失败: %w", err)
}
if err == nil {
if err := atomicWriteBookmarkFile(path+".bak", previous, 0o644, ".bookmarks-backup-*.tmp"); err != nil {
return fmt.Errorf("写入内置书签配置备份失败: %w", err)
}
}
data, err := marshalBookmarkJSON(book)
if err != nil {
return err
}
if err := atomicWriteBookmarkFile(path, data, 0o644, ".bookmarks-source-*.tmp"); err != nil {
return fmt.Errorf("写入内置书签配置失败: %w", err)
}
return nil
}
func atomicWriteBookmarkFile(path string, data []byte, mode os.FileMode, pattern string) error {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
temp, err := os.CreateTemp(filepath.Dir(path), pattern)
if err != nil {
return err
}
tempPath := temp.Name()
defer os.Remove(tempPath)
if err := temp.Chmod(mode); err != nil {
_ = temp.Close()
return err
}
if _, err := temp.Write(data); err != nil {
_ = temp.Close()
return err
}
if err := temp.Sync(); err != nil {
_ = temp.Close()
return err
}
if err := temp.Close(); err != nil {
return err
}
return os.Rename(tempPath, path)
}
func safeBookmarkFilename(name string) string {
name = strings.TrimSpace(name)
if name == "" {
return "bookmarks"
}
var builder strings.Builder
for _, r := range name {
switch r {
case '/', '\\', ':', '*', '?', '"', '<', '>', '|':
builder.WriteByte('_')
default:
builder.WriteRune(r)
}
}
result := strings.TrimSpace(builder.String())
if result == "" || result == "." || result == ".." {
return "bookmarks"
}
return result
}
func findBookmarkSourcePath() (string, bool) {
starts := make([]string, 0, 2)
if cwd, err := os.Getwd(); err == nil {
starts = append(starts, cwd)
}
if executable, err := os.Executable(); err == nil {
starts = append(starts, filepath.Dir(executable))
}
seen := make(map[string]struct{})
for _, start := range starts {
for dir := filepath.Clean(start); ; dir = filepath.Dir(dir) {
if _, ok := seen[dir]; !ok {
seen[dir] = struct{}{}
candidate := filepath.Join(dir, filepath.FromSlash(bookmarkSourcePath))
if bookmarkFileExists(filepath.Join(dir, "go.mod")) && bookmarkFileExists(candidate) {
return candidate, true
}
}
parent := filepath.Dir(dir)
if parent == dir {
break
}
}
}
return "", false
}
func bookmarkFileExists(path string) bool {
info, err := os.Stat(path)
return err == nil && !info.IsDir()
}