Files
90CNServer/pvf/services/script_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

212 lines
6.7 KiB
Go

package services
import (
"fmt"
"strings"
scriptengine "pvfine/internal/script"
)
const (
// fileSetChangeKey is the stable selection key for every staged file set
// mutation. Archive change keys are normalized paths, which can never look
// like this, so one checkbox can cover the whole file set group.
fileSetChangeKey = "fileset:*"
ScriptFileSetChanged = "changed"
ScriptFileSetAdded = "added"
// defaultFileSetName is the built-in set the sidebar always shows. The
// frontend reserves the id "default" for it and rewrites any other set that
// claims that id, so a scripted set with this name must own both the name and
// the id — otherwise reloading shows two sets with the same name.
defaultFileSetName = "默认文件集"
defaultFileSetID = "default"
)
// buildFileSetPreviewRows diffs the staged file sets against the baseline so the
// preview panel can show what each setAll actually changed.
func buildFileSetPreviewRows(stage *scriptengine.FileSetStage) []*ScriptFileSetPreview {
if stage == nil {
return nil
}
changes := stage.Changes()
if len(changes) == 0 {
return nil
}
rows := make([]*ScriptFileSetPreview, 0, len(changes))
for _, change := range changes {
row := &ScriptFileSetPreview{
ChangeKey: fileSetChangeKey,
Name: change.Name,
Status: ScriptFileSetChanged,
Count: len(change.Entries),
}
if change.Created {
row.Status = ScriptFileSetAdded
}
baseline, existed := stage.Baseline(change.Name)
if existed {
row.Added, row.Removed = diffFileSetPaths(baseline.Entries, change.Entries)
} else {
for _, entry := range change.Entries {
row.Added = append(row.Added, entry.Path)
}
}
rows = append(rows, row)
}
return rows
}
// diffFileSetPaths reports which paths the change introduced and removed,
// preserving the stored order of each list.
func diffFileSetPaths(before, after []scriptengine.FileSetEntry) (added, removed []string) {
beforePaths := make(map[string]struct{}, len(before))
for _, entry := range before {
beforePaths[entry.Path] = struct{}{}
}
afterPaths := make(map[string]struct{}, len(after))
for _, entry := range after {
afterPaths[entry.Path] = struct{}{}
if _, exists := beforePaths[entry.Path]; !exists {
added = append(added, entry.Path)
}
}
for _, entry := range before {
if _, exists := afterPaths[entry.Path]; !exists {
removed = append(removed, entry.Path)
}
}
return added, removed
}
// loadFileSetSnapshot reads the persisted file sets for one script run.
//
// The set list mirrors the sidebar exactly, so a name the user sees in the UI is
// the name a script reads and writes. The built-in 「默认文件集」 is exposed even
// when it was never saved (the sidebar also shows it empty), which makes
// pvf.fileset("默认文件集") meaningful instead of returning null. If that set was
// persisted under a different name, the persisted name wins and the built-in
// placeholder is not added — matching what the sidebar displays.
//
// A read failure still exposes the built-in default so a script can use it
// instead of the whole run failing.
func (s *ScriptService) loadFileSetSnapshot() []scriptengine.FileSet {
sets := make([]scriptengine.FileSet, 0)
defaultSet := scriptengine.FileSet{ID: defaultFileSetID, Name: defaultFileSetName}
if s == nil || s.fileSets == nil {
return append(sets, defaultSet)
}
document, err := s.fileSets.LoadFileSets()
if err != nil {
return append(sets, defaultSet)
}
for _, stored := range document.FileSets {
entries := make([]scriptengine.FileSetEntry, 0, len(stored.Entries))
for _, entry := range stored.Entries {
entries = append(entries, scriptengine.FileSetEntry{
Path: entry.Path,
Name: entry.Name,
IDs: append([]string(nil), entry.IDs...),
Size: entry.Size,
DataType: entry.DataType,
})
}
fileSet := scriptengine.FileSet{
ID: stored.ID,
Name: strings.TrimSpace(stored.Name),
Entries: entries,
}
// The reserved id belongs to the built-in slot, so the persisted record
// replaces the placeholder rather than appearing beside it.
if stored.ID == defaultFileSetID {
defaultSet = fileSet
continue
}
sets = append(sets, fileSet)
}
return append([]scriptengine.FileSet{defaultSet}, sets...)
}
// applyFileSetChanges merges the staged file set mutations into the persisted
// document and saves it. Changes are matched by name because the script cannot
// know the ids the persistence layer assigned.
func (s *ScriptService) applyFileSetChanges(changes []scriptengine.FileSetChange) (int, error) {
if s == nil || s.fileSets == nil {
if len(changes) == 0 {
return 0, nil
}
return 0, fmt.Errorf("文件集不可用")
}
if len(changes) == 0 {
return 0, nil
}
document, err := s.fileSets.LoadFileSets()
if err != nil {
return 0, err
}
byName := make(map[string]int, len(document.FileSets))
for index := range document.FileSets {
byName[strings.TrimSpace(document.FileSets[index].Name)] = index
}
applied := 0
for _, change := range changes {
name := strings.TrimSpace(change.Name)
if name == "" {
return 0, fmt.Errorf("文件集名称不能为空")
}
entries := make([]StoredFileSetEntry, 0, len(change.Entries))
for _, entry := range change.Entries {
entries = append(entries, StoredFileSetEntry{
Path: entry.Path,
Name: entry.Name,
IDs: append([]string(nil), entry.IDs...),
Size: entry.Size,
DataType: entry.DataType,
})
}
if index, exists := byName[name]; exists {
document.FileSets[index].Entries = entries
applied++
continue
}
document.FileSets = append(document.FileSets, StoredFileSet{
ID: fileSetIDForName(document.FileSets, name),
Name: name,
Entries: entries,
})
byName[name] = len(document.FileSets) - 1
applied++
}
if document.ActiveSetID == "" && len(document.FileSets) > 0 {
document.ActiveSetID = document.FileSets[0].ID
}
if err := s.fileSets.SaveFileSets(document); err != nil {
return 0, err
}
return applied, nil
}
// fileSetIDForName picks an id the document does not use yet. The sidebar
// reserves the id "default" for its built-in set and remaps a persisted set that
// claims it, so a scripted "默认文件集" takes that id and reappears as the
// built-in set instead of duplicating its name. Ids are opaque to scripts.
func fileSetIDForName(sets []StoredFileSet, name string) string {
used := make(map[string]struct{}, len(sets))
for _, fileSet := range sets {
used[fileSet.ID] = struct{}{}
}
if name == defaultFileSetName {
if _, exists := used[defaultFileSetID]; !exists {
return defaultFileSetID
}
}
for number := 1; ; number++ {
candidate := fmt.Sprintf("script-set-%d", number)
if _, exists := used[candidate]; !exists {
return candidate
}
}
}