Files
90CNServer/pvf/services/filesets.go
T
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

208 lines
5.6 KiB
Go

package services
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
)
const fileSetDocumentVersion = 1
// StoredFileSetEntry 是文件集落盘格式。fileIndex 不持久化,因为它只对当前 PVF 有效。
type StoredFileSetEntry struct {
Path string `json:"path"`
Name string `json:"name"`
IDs []string `json:"ids,omitempty"`
Size int32 `json:"size"`
DataType int32 `json:"dataType"`
}
type StoredFileSet struct {
ID string `json:"id"`
Name string `json:"name"`
Entries []StoredFileSetEntry `json:"entries"`
}
// FileSetDocument 是全部文件集的持久化文档,与当前打开的 PVF 无关。
type FileSetDocument struct {
Version int `json:"version"`
FileSets []StoredFileSet `json:"fileSets"`
ActiveSetID string `json:"activeSetId"`
}
type FileSetService struct {
mu sync.Mutex
path string
initErr error
}
func NewFileSetService() *FileSetService {
configDir, err := os.UserConfigDir()
if err != nil {
return &FileSetService{initErr: fmt.Errorf("获取用户配置目录失败: %w", err)}
}
return newFileSetService(filepath.Join(configDir, "pvfine", "file-sets.json"))
}
func newFileSetService(path string) *FileSetService {
return &FileSetService{path: path}
}
// LoadFileSets 从用户配置目录读取已保存的文件集。文件不存在时返回空文档。
func (s *FileSetService) LoadFileSets() (FileSetDocument, error) {
s.mu.Lock()
defer s.mu.Unlock()
if s.initErr != nil {
return FileSetDocument{}, s.initErr
}
data, err := os.ReadFile(s.path)
if os.IsNotExist(err) {
return emptyFileSetDocument(), nil
}
if err != nil {
return FileSetDocument{}, fmt.Errorf("读取文件集失败: %w", err)
}
var document FileSetDocument
if err := json.Unmarshal(data, &document); err != nil {
return FileSetDocument{}, fmt.Errorf("解析文件集失败: %w", err)
}
if err := normalizeFileSetDocument(&document); err != nil {
return FileSetDocument{}, err
}
return document, nil
}
// SaveFileSets 将全部文件集以原子方式保存到用户配置目录。
func (s *FileSetService) SaveFileSets(document FileSetDocument) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.initErr != nil {
return s.initErr
}
if err := normalizeFileSetDocument(&document); err != nil {
return err
}
if len(document.FileSets) == 0 {
if err := os.Remove(s.path); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("删除空文件集文件失败: %w", err)
}
return nil
}
data, err := json.MarshalIndent(document, "", " ")
if err != nil {
return fmt.Errorf("编码文件集失败: %w", err)
}
data = append(data, '\n')
if err := os.MkdirAll(filepath.Dir(s.path), 0o755); err != nil {
return fmt.Errorf("创建文件集目录失败: %w", err)
}
temp, err := os.CreateTemp(filepath.Dir(s.path), ".file-sets-*.tmp")
if err != nil {
return fmt.Errorf("创建文件集临时文件失败: %w", err)
}
tempPath := temp.Name()
defer os.Remove(tempPath)
if err := temp.Chmod(0o600); err != nil {
temp.Close()
return err
}
if _, err := temp.Write(data); err != nil {
temp.Close()
return fmt.Errorf("写入文件集失败: %w", err)
}
if err := temp.Sync(); err != nil {
temp.Close()
return fmt.Errorf("同步文件集失败: %w", err)
}
if err := temp.Close(); err != nil {
return fmt.Errorf("关闭文件集文件失败: %w", err)
}
if err := os.Rename(tempPath, s.path); err != nil {
return fmt.Errorf("替换文件集文件失败: %w", err)
}
return nil
}
func emptyFileSetDocument() FileSetDocument {
return FileSetDocument{Version: fileSetDocumentVersion}
}
func normalizeFileSetDocument(document *FileSetDocument) error {
if document.Version == 0 {
document.Version = fileSetDocumentVersion
}
if document.Version != fileSetDocumentVersion {
return fmt.Errorf("不支持的文件集版本: %d", document.Version)
}
document.ActiveSetID = strings.TrimSpace(document.ActiveSetID)
seenIDs := make(map[string]struct{}, len(document.FileSets))
activeExists := false
for i := range document.FileSets {
fileSet := &document.FileSets[i]
fileSet.ID = strings.TrimSpace(fileSet.ID)
fileSet.Name = strings.TrimSpace(fileSet.Name)
if fileSet.ID == "" || fileSet.Name == "" {
return fmt.Errorf("文件集名称或 id 为空")
}
if _, exists := seenIDs[fileSet.ID]; exists {
return fmt.Errorf("文件集 id 重复: %q", fileSet.ID)
}
seenIDs[fileSet.ID] = struct{}{}
if fileSet.ID == document.ActiveSetID {
activeExists = true
}
entries := fileSet.Entries[:0]
seenPaths := make(map[string]struct{}, len(fileSet.Entries))
for _, entry := range fileSet.Entries {
entry.Path = normalizeFileSetPath(entry.Path)
if entry.Path == "" {
continue
}
if _, exists := seenPaths[entry.Path]; exists {
continue
}
seenPaths[entry.Path] = struct{}{}
entry.IDs = uniqueStrings(entry.IDs)
entries = append(entries, entry)
}
fileSet.Entries = entries
}
if document.ActiveSetID != "" && !activeExists {
document.ActiveSetID = ""
}
if document.ActiveSetID == "" && len(document.FileSets) > 0 {
document.ActiveSetID = document.FileSets[0].ID
}
return nil
}
func normalizeFileSetPath(path string) string {
return strings.Trim(strings.ReplaceAll(path, "\\", "/"), "/")
}
func uniqueStrings(values []string) []string {
if len(values) == 0 {
return nil
}
result := make([]string, 0, len(values))
seen := make(map[string]struct{}, len(values))
for _, value := range values {
if value == "" {
continue
}
if _, exists := seen[value]; exists {
continue
}
seen[value] = struct{}{}
result = append(result, value)
}
return result
}