DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,320 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// CacheService reports and clears the caches the application keeps for itself:
|
||||
// the semantic search index, the SQLite archive index, the advanced search
|
||||
// index, the NPK icon index and the timed workspace backup. Everything below
|
||||
// the cache root is either rebuilt on demand or disposable, so clearing can
|
||||
// never remove configuration, bookmarks or file sets (those live in the user
|
||||
// config directory).
|
||||
type CacheService struct {
|
||||
maintenanceMu sync.Mutex
|
||||
cancel context.CancelFunc
|
||||
done chan struct{}
|
||||
c *core
|
||||
settings *SettingsService
|
||||
|
||||
// rootOverride is only set by tests.
|
||||
rootOverride string
|
||||
}
|
||||
|
||||
// NewCacheService creates the cache maintenance service.
|
||||
func NewCacheService(c *core, settings *SettingsService) *CacheService {
|
||||
return &CacheService{c: c, settings: settings}
|
||||
}
|
||||
|
||||
func newCacheServiceWithRoot(c *core, settings *SettingsService, root string) *CacheService {
|
||||
return &CacheService{c: c, settings: settings, rootOverride: root}
|
||||
}
|
||||
|
||||
// CacheUsage is the disk footprint reported to the settings page.
|
||||
type CacheUsage struct {
|
||||
LimitBytes int64 `json:"limitBytes"`
|
||||
IndexBytes int64 `json:"indexBytes"`
|
||||
BackupBytes int64 `json:"backupBytes"`
|
||||
ReclaimableBytes int64 `json:"reclaimableBytes"`
|
||||
OverBudget bool `json:"overBudget"`
|
||||
Path string `json:"path"`
|
||||
Files int `json:"files"`
|
||||
TotalBytes int64 `json:"totalBytes"`
|
||||
// InUseBytes covers the indexes the running session holds open. They are
|
||||
// rebuilt once the archive is closed, so a clear pass has to skip them.
|
||||
InUseBytes int64 `json:"inUseBytes"`
|
||||
}
|
||||
|
||||
// CacheClearResult reports what one clear pass removed.
|
||||
type CacheClearResult struct {
|
||||
FreedBytes int64 `json:"freedBytes"`
|
||||
Usage CacheUsage `json:"usage"`
|
||||
}
|
||||
|
||||
// Usage sums the cache without changing anything.
|
||||
func (s *CacheService) Usage() (CacheUsage, error) {
|
||||
root, err := s.cacheRoot()
|
||||
if err != nil {
|
||||
return CacheUsage{}, err
|
||||
}
|
||||
usage := CacheUsage{Path: root}
|
||||
inUse := s.inUsePaths()
|
||||
cacheLeases.Lock()
|
||||
inUse = append(inUse, leasedCachePathsLocked()...)
|
||||
cacheLeases.Unlock()
|
||||
backups := s.autosaveSlotFiles()
|
||||
if err := walkCacheRoot(root, func(path string, info fs.FileInfo) {
|
||||
usage.Files++
|
||||
usage.TotalBytes += info.Size()
|
||||
if pathInUse(path, backups) {
|
||||
usage.BackupBytes += info.Size()
|
||||
}
|
||||
if pathInUse(path, inUse) {
|
||||
usage.InUseBytes += info.Size()
|
||||
}
|
||||
}); err != nil {
|
||||
return CacheUsage{}, err
|
||||
}
|
||||
// The backup slot can be configured outside the cache root; it still counts
|
||||
// as application cache.
|
||||
for _, path := range s.outerAutosaveFiles(root) {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil || info.IsDir() {
|
||||
continue
|
||||
}
|
||||
usage.Files++
|
||||
usage.TotalBytes += info.Size()
|
||||
usage.BackupBytes += info.Size()
|
||||
}
|
||||
usage.IndexBytes = usage.TotalBytes - usage.BackupBytes
|
||||
if s.settings != nil {
|
||||
settings, err := s.settings.GetSettings()
|
||||
if err != nil {
|
||||
return CacheUsage{}, err
|
||||
}
|
||||
usage.LimitBytes = int64(settings.CacheLimitGiB) << 30
|
||||
}
|
||||
usage.OverBudget = usage.LimitBytes > 0 && usage.TotalBytes > usage.LimitBytes
|
||||
protected := append(inUse, backups...)
|
||||
for _, unit := range collectCacheUnits(root) {
|
||||
if !unitProtected(unit, protected) {
|
||||
usage.ReclaimableBytes += unit.size
|
||||
}
|
||||
}
|
||||
return usage, nil
|
||||
}
|
||||
|
||||
// Clear deletes every cache entry it can, leaving indexes that are still in use
|
||||
// by the running session in place.
|
||||
func (s *CacheService) Clear() (CacheClearResult, error) {
|
||||
s.maintenanceMu.Lock()
|
||||
defer s.maintenanceMu.Unlock()
|
||||
root, err := s.cacheRoot()
|
||||
if err != nil {
|
||||
return CacheClearResult{}, err
|
||||
}
|
||||
outerBackups := s.outerAutosaveFiles(root)
|
||||
inUse := s.inUsePaths()
|
||||
cacheLeases.Lock()
|
||||
inUse = append(inUse, leasedCachePathsLocked()...)
|
||||
freed := int64(0)
|
||||
if err := walkCacheRoot(root, func(path string, info fs.FileInfo) {
|
||||
if pathInUse(path, inUse) {
|
||||
return
|
||||
}
|
||||
if err := os.Remove(path); err == nil {
|
||||
freed += info.Size()
|
||||
}
|
||||
}); err != nil {
|
||||
cacheLeases.Unlock()
|
||||
return CacheClearResult{}, err
|
||||
}
|
||||
pruneEmptyCacheDirs(root, inUse)
|
||||
for _, path := range outerBackups {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil || info.IsDir() {
|
||||
continue
|
||||
}
|
||||
if err := os.Remove(path); err == nil {
|
||||
freed += info.Size()
|
||||
}
|
||||
}
|
||||
cacheLeases.Unlock()
|
||||
usage, err := s.Usage()
|
||||
if err != nil {
|
||||
return CacheClearResult{}, err
|
||||
}
|
||||
return CacheClearResult{FreedBytes: freed, Usage: usage}, nil
|
||||
}
|
||||
|
||||
func (s *CacheService) cacheRoot() (string, error) {
|
||||
if s == nil {
|
||||
return "", fmt.Errorf("缓存服务不可用")
|
||||
}
|
||||
if s.rootOverride != "" {
|
||||
return s.rootOverride, nil
|
||||
}
|
||||
root, err := os.UserCacheDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("获取用户缓存目录失败: %w", err)
|
||||
}
|
||||
return filepath.Join(root, "pvfine"), nil
|
||||
}
|
||||
|
||||
// inUsePaths lists the cache files the live session keeps open. SQLite sidecars
|
||||
// are listed as well: removing a -wal file under an open connection would
|
||||
// corrupt that database.
|
||||
func (s *CacheService) inUsePaths() []string {
|
||||
if s == nil || s.c == nil {
|
||||
return nil
|
||||
}
|
||||
s.c.mu.RLock()
|
||||
defer s.c.mu.RUnlock()
|
||||
var paths []string
|
||||
add := func(path string) {
|
||||
if path == "" {
|
||||
return
|
||||
}
|
||||
paths = append(paths, path, path+"-wal", path+"-shm", path+"-journal")
|
||||
}
|
||||
if s.c.diskIndex != nil {
|
||||
add(s.c.diskIndex.path)
|
||||
}
|
||||
if s.c.advancedDisk != nil {
|
||||
add(s.c.advancedDisk.dir)
|
||||
add(s.c.advancedDisk.cachePath)
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
// outerAutosaveFiles returns the backup payload and metadata files that sit
|
||||
// outside the cache root, so a custom location is reported and cleared too.
|
||||
func (s *CacheService) outerAutosaveFiles(root string) []string {
|
||||
var result []string
|
||||
for _, path := range s.autosaveSlotFiles() {
|
||||
if !pathWithin(root, path) {
|
||||
result = append(result, path)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// autosaveSlotFiles returns the backup payload and its metadata file.
|
||||
func (s *CacheService) autosaveSlotFiles() []string {
|
||||
if s == nil || s.settings == nil {
|
||||
return nil
|
||||
}
|
||||
settings, err := s.settings.GetSettings()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
path := strings.TrimSpace(settings.AutosavePath)
|
||||
if path == "" {
|
||||
if path, err = DefaultAutosavePath(); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return []string{path, path + ".json"}
|
||||
}
|
||||
|
||||
// webviewCacheEntries are directories under the cache root that belong to the
|
||||
// embedded webview instead of the application. They hold live webview state
|
||||
// (localStorage, IndexedDB), so their size is not reported and a clear pass must
|
||||
// never delete them while the window is running.
|
||||
var webviewCacheEntries = map[string]struct{}{"WebKit": {}}
|
||||
|
||||
// walkCacheRoot calls fn for every regular file below root. A missing root is
|
||||
// not an error: it just means nothing has been cached yet.
|
||||
func walkCacheRoot(root string, fn func(path string, info fs.FileInfo)) error {
|
||||
err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
if path == root && os.IsNotExist(err) {
|
||||
return filepath.SkipAll
|
||||
}
|
||||
// A cache entry that cannot be read is not worth failing the whole
|
||||
// report for.
|
||||
return nil
|
||||
}
|
||||
if entry.IsDir() {
|
||||
if isWebviewCacheEntry(root, path) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
info, infoErr := entry.Info()
|
||||
if infoErr != nil {
|
||||
return nil
|
||||
}
|
||||
fn(path, info)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("统计缓存占用失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isWebviewCacheEntry reports whether path is a webview-owned directory directly
|
||||
// below the cache root.
|
||||
func isWebviewCacheEntry(root, path string) bool {
|
||||
relative, err := filepath.Rel(root, path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_, skip := webviewCacheEntries[relative]
|
||||
return skip
|
||||
}
|
||||
|
||||
func pathInUse(path string, inUse []string) bool {
|
||||
for _, candidate := range inUse {
|
||||
if pathWithin(candidate, path) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// pathWithin reports whether candidate is root itself or sits inside it.
|
||||
func pathWithin(root, candidate string) bool {
|
||||
if root == "" || candidate == "" {
|
||||
return false
|
||||
}
|
||||
cleanedRoot := filepath.Clean(root)
|
||||
cleanedCandidate := filepath.Clean(candidate)
|
||||
if cleanedRoot == cleanedCandidate {
|
||||
return true
|
||||
}
|
||||
return strings.HasPrefix(cleanedCandidate, cleanedRoot+string(filepath.Separator))
|
||||
}
|
||||
|
||||
// pruneEmptyCacheDirs removes the directories a clear pass emptied, keeping the
|
||||
// cache root and anything that still holds files (in-use indexes).
|
||||
func pruneEmptyCacheDirs(root string, inUse []string) {
|
||||
directories := make([]string, 0, 8)
|
||||
_ = filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if entry.IsDir() && path != root {
|
||||
if isWebviewCacheEntry(root, path) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
directories = append(directories, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
// Deepest first so a parent can become empty by its children leaving.
|
||||
for i := len(directories) - 1; i >= 0; i-- {
|
||||
path := directories[i]
|
||||
if pathInUse(path, inUse) {
|
||||
continue
|
||||
}
|
||||
_ = os.Remove(path) // fails while the directory still has entries
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user