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,277 @@
|
||||
package script
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FileSetEntry is one entry inside a file set. Only Path is meaningful to a
|
||||
// script; the remaining fields preserve the metadata the persistence layer
|
||||
// stores so a rewritten set does not lose names or ids.
|
||||
type FileSetEntry struct {
|
||||
Path string
|
||||
Name string
|
||||
IDs []string
|
||||
Size int32
|
||||
DataType int32
|
||||
}
|
||||
|
||||
// FileSet is an immutable snapshot of one file set.
|
||||
type FileSet struct {
|
||||
ID string
|
||||
Name string
|
||||
Entries []FileSetEntry
|
||||
}
|
||||
|
||||
// FileSetChange is one staged file set mutation. Name addresses the target set;
|
||||
// Created reports whether the script introduced a set absent from the baseline.
|
||||
// IDs are assigned by the persistence layer when the plan is applied, so a
|
||||
// change never carries one.
|
||||
type FileSetChange struct {
|
||||
Name string
|
||||
Created bool
|
||||
Entries []FileSetEntry
|
||||
}
|
||||
|
||||
// FileSetStage holds the file sets a run may read plus the mutations it stages.
|
||||
// It is name-addressed on purpose: ids belong to the persistence layer, and the
|
||||
// stage must not invent them.
|
||||
type FileSetStage struct {
|
||||
baseline map[string]FileSet
|
||||
current map[string]FileSet
|
||||
created map[string]struct{}
|
||||
order []string
|
||||
}
|
||||
|
||||
// NewFileSetStage builds a stage over the supplied baseline snapshot. Later sets
|
||||
// with a duplicate name are ignored so lookup stays deterministic.
|
||||
func NewFileSetStage(sets []FileSet) *FileSetStage {
|
||||
stage := &FileSetStage{
|
||||
baseline: make(map[string]FileSet, len(sets)),
|
||||
current: make(map[string]FileSet, len(sets)),
|
||||
created: make(map[string]struct{}),
|
||||
}
|
||||
for _, fileSet := range sets {
|
||||
name := NormalizeFileSetName(fileSet.Name)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := stage.baseline[name]; exists {
|
||||
continue
|
||||
}
|
||||
snapshot := FileSet{ID: fileSet.ID, Name: name, Entries: cloneFileSetEntries(fileSet.Entries)}
|
||||
stage.baseline[name] = snapshot
|
||||
stage.current[name] = snapshot
|
||||
stage.order = append(stage.order, name)
|
||||
}
|
||||
return stage
|
||||
}
|
||||
|
||||
// Lookup returns the current state of a named file set.
|
||||
func (s *FileSetStage) Lookup(name string) (FileSet, bool) {
|
||||
if s == nil {
|
||||
return FileSet{}, false
|
||||
}
|
||||
fileSet, ok := s.current[NormalizeFileSetName(name)]
|
||||
return fileSet, ok
|
||||
}
|
||||
|
||||
// Create adds a new file set. It fails when the name is already taken by the
|
||||
// baseline or by an earlier call in the same run.
|
||||
func (s *FileSetStage) Create(name string, paths []string) (FileSet, error) {
|
||||
if s == nil {
|
||||
return FileSet{}, fmt.Errorf("文件集暂存区不可用")
|
||||
}
|
||||
normalized, err := normalizeFileSetNameStrict(name)
|
||||
if err != nil {
|
||||
return FileSet{}, err
|
||||
}
|
||||
if _, exists := s.current[normalized]; exists {
|
||||
return FileSet{}, fmt.Errorf("文件集已存在: %s", normalized)
|
||||
}
|
||||
fileSet := FileSet{Name: normalized, Entries: buildFileSetEntries(paths, nil)}
|
||||
s.current[normalized] = fileSet
|
||||
s.created[normalized] = struct{}{}
|
||||
s.order = append(s.order, normalized)
|
||||
return fileSet, nil
|
||||
}
|
||||
|
||||
// Replace overwrites the entry list of an existing file set, keeping the
|
||||
// metadata of entries whose path survives.
|
||||
func (s *FileSetStage) Replace(name string, paths []string) (FileSet, error) {
|
||||
if s == nil {
|
||||
return FileSet{}, fmt.Errorf("文件集暂存区不可用")
|
||||
}
|
||||
normalized, err := normalizeFileSetNameStrict(name)
|
||||
if err != nil {
|
||||
return FileSet{}, err
|
||||
}
|
||||
existing, exists := s.current[normalized]
|
||||
if !exists {
|
||||
return FileSet{}, fmt.Errorf("文件集不存在: %s", normalized)
|
||||
}
|
||||
existing.Entries = buildFileSetEntries(paths, existing.Entries)
|
||||
s.current[normalized] = existing
|
||||
return existing, nil
|
||||
}
|
||||
|
||||
// Baseline returns the snapshot state of a named file set, reporting false when
|
||||
// the set did not exist before the run.
|
||||
func (s *FileSetStage) Baseline(name string) (FileSet, bool) {
|
||||
if s == nil {
|
||||
return FileSet{}, false
|
||||
}
|
||||
fileSet, ok := s.baseline[NormalizeFileSetName(name)]
|
||||
return fileSet, ok
|
||||
}
|
||||
|
||||
// Changes returns every staged mutation in stable name order, omitting sets
|
||||
// whose entry list is byte-for-byte equivalent to the baseline.
|
||||
func (s *FileSetStage) Changes() []FileSetChange {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
changes := make([]FileSetChange, 0)
|
||||
for name, current := range s.current {
|
||||
baseline, existed := s.baseline[name]
|
||||
if existed && fileSetEntriesEqual(baseline.Entries, current.Entries) {
|
||||
continue
|
||||
}
|
||||
_, isCreated := s.created[name]
|
||||
changes = append(changes, FileSetChange{
|
||||
Name: name,
|
||||
Created: isCreated || !existed,
|
||||
Entries: cloneFileSetEntries(current.Entries),
|
||||
})
|
||||
}
|
||||
sort.SliceStable(changes, func(left, right int) bool {
|
||||
return changes[left].Name < changes[right].Name
|
||||
})
|
||||
return changes
|
||||
}
|
||||
|
||||
// buildFileSetEntries normalizes a path list into entries, reusing the metadata
|
||||
// of a previous entry with the same path so a rewrite keeps names and ids.
|
||||
func buildFileSetEntries(paths []string, previous []FileSetEntry) []FileSetEntry {
|
||||
known := make(map[string]FileSetEntry, len(previous))
|
||||
for _, entry := range previous {
|
||||
key := NormalizeFileSetPath(entry.Path)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := known[key]; !exists {
|
||||
known[key] = entry
|
||||
}
|
||||
}
|
||||
entries := make([]FileSetEntry, 0, len(paths))
|
||||
seen := make(map[string]struct{}, len(paths))
|
||||
for _, raw := range paths {
|
||||
key := NormalizeFileSetPath(raw)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[key]; exists {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
entry, ok := known[key]
|
||||
if !ok {
|
||||
entry = FileSetEntry{Name: fileSetPathBase(key)}
|
||||
}
|
||||
entry.Path = key
|
||||
if entry.Name == "" {
|
||||
entry.Name = fileSetPathBase(key)
|
||||
}
|
||||
entry.IDs = uniqueFileSetStrings(entry.IDs)
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
// NormalizeFileSetName trims a set name for lookup. An empty result means the
|
||||
// name is unusable.
|
||||
func NormalizeFileSetName(name string) string {
|
||||
return strings.TrimSpace(name)
|
||||
}
|
||||
|
||||
func normalizeFileSetNameStrict(name string) (string, error) {
|
||||
normalized := NormalizeFileSetName(name)
|
||||
if normalized == "" {
|
||||
return "", fmt.Errorf("文件集名称不能为空")
|
||||
}
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
// NormalizeFileSetPath trims an entry path to its storage spelling, mirroring
|
||||
// the persistence layer's normalization.
|
||||
func NormalizeFileSetPath(path string) string {
|
||||
return strings.Trim(strings.ReplaceAll(strings.TrimSpace(path), "\\", "/"), "/")
|
||||
}
|
||||
|
||||
func fileSetPathBase(path string) string {
|
||||
if slash := strings.LastIndexByte(path, '/'); slash >= 0 {
|
||||
return path[slash+1:]
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// fileSetEntriesEqual reports whether a rewrite is semantically a no-op. Only
|
||||
// the path order and the ids decide that: name, size and data type are
|
||||
// archive-derived metadata the sidebar refreshes on its own, so a rebuild that
|
||||
// merely backfills them must not look like a script change.
|
||||
func fileSetEntriesEqual(left, right []FileSetEntry) bool {
|
||||
if len(left) != len(right) {
|
||||
return false
|
||||
}
|
||||
for index := range left {
|
||||
if left[index].Path != right[index].Path ||
|
||||
!fileSetStringsEqual(left[index].IDs, right[index].IDs) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func fileSetStringsEqual(left, right []string) bool {
|
||||
if len(left) != len(right) {
|
||||
return false
|
||||
}
|
||||
for index := range left {
|
||||
if left[index] != right[index] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func cloneFileSetEntries(entries []FileSetEntry) []FileSetEntry {
|
||||
if len(entries) == 0 {
|
||||
return nil
|
||||
}
|
||||
result := make([]FileSetEntry, len(entries))
|
||||
for index, entry := range entries {
|
||||
result[index] = entry
|
||||
result[index].IDs = append([]string(nil), entry.IDs...)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func uniqueFileSetStrings(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
|
||||
}
|
||||
Reference in new issue
Block a user