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,211 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user