服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
212 lines
6.7 KiB
Go
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
|
|
}
|
|
}
|
|
}
|