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,326 @@
|
||||
package pvf
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
platformpvf "longheng.io/server/internal/platform/pvf"
|
||||
)
|
||||
|
||||
// Source 表示可以按路径读取 PVF 文本的内存来源。
|
||||
type Source interface {
|
||||
ReadText(relativePath string) (string, error)
|
||||
}
|
||||
|
||||
// BuildOptions 控制 DNF PVF 内存索引的预加载范围。
|
||||
type BuildOptions struct {
|
||||
All bool
|
||||
Paths []string
|
||||
Lists []string
|
||||
Prefixes []string
|
||||
}
|
||||
|
||||
// Index 保存已解析的 DNF PVF 文档和 `.lst` 引用索引。
|
||||
type Index struct {
|
||||
docs map[string]*Document
|
||||
lists map[string][]ListEntry
|
||||
refs map[string]map[int64]string
|
||||
paths []string
|
||||
}
|
||||
|
||||
type fileLister interface {
|
||||
Files() []platformpvf.File
|
||||
}
|
||||
|
||||
type fileFinder interface {
|
||||
FindFile(relativePath string) (platformpvf.File, bool)
|
||||
}
|
||||
|
||||
type filePathRanger interface {
|
||||
RangePaths(func(string) bool)
|
||||
}
|
||||
|
||||
type sourcePathIndex struct {
|
||||
paths map[string]string
|
||||
finder fileFinder
|
||||
}
|
||||
|
||||
func (i sourcePathIndex) find(value string) (string, bool) {
|
||||
clean := cleanPath(value)
|
||||
if clean == "" {
|
||||
return "", false
|
||||
}
|
||||
if actual, ok := i.paths[pathKey(clean)]; ok {
|
||||
return actual, true
|
||||
}
|
||||
if i.finder == nil {
|
||||
return "", false
|
||||
}
|
||||
file, ok := i.finder.FindFile(clean)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
actual := cleanPath(file.ArchivePath)
|
||||
return actual, actual != ""
|
||||
}
|
||||
|
||||
// Build 从已加载的 PVF source 中构建项目层内存索引。
|
||||
func Build(ctx context.Context, source Source, options BuildOptions) (*Index, error) {
|
||||
if source == nil {
|
||||
return nil, ErrSourceRequired
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
sourcePaths := sourcePathSet(source)
|
||||
paths := collectPaths(source, options, sourcePaths)
|
||||
listKeys := listKeySet(options.Lists)
|
||||
listPaths := append([]string(nil), options.Lists...)
|
||||
index := &Index{
|
||||
docs: make(map[string]*Document, len(paths)),
|
||||
lists: make(map[string][]ListEntry, len(options.Lists)),
|
||||
refs: make(map[string]map[int64]string, len(options.Lists)),
|
||||
paths: make([]string, 0, len(paths)),
|
||||
}
|
||||
for pos := 0; pos < len(paths); pos++ {
|
||||
docPath := paths[pos]
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
text, err := source.ReadText(docPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read dnf pvf %q: %w", docPath, err)
|
||||
}
|
||||
doc, err := Parse(docPath, text)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse dnf pvf %q: %w", docPath, err)
|
||||
}
|
||||
key := pathKey(docPath)
|
||||
index.docs[key] = doc
|
||||
index.paths = append(index.paths, doc.Path)
|
||||
if _, ok := listKeys[key]; ok {
|
||||
for _, entry := range ParseList(doc) {
|
||||
resolved := resolveListRef(doc.Path, entry.Path, sourcePaths)
|
||||
addPath(&paths, index.docs, resolved)
|
||||
if strings.EqualFold(path.Ext(resolved), ".lst") {
|
||||
resolvedKey := pathKey(resolved)
|
||||
if _, exists := listKeys[resolvedKey]; !exists {
|
||||
listKeys[resolvedKey] = struct{}{}
|
||||
listPaths = append(listPaths, resolved)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(index.paths)
|
||||
index.buildLists(listPaths, sourcePaths)
|
||||
return index, nil
|
||||
}
|
||||
|
||||
// Document 按路径返回已解析文档,不会重新读取 PVF。
|
||||
func (i *Index) Document(docPath string) (*Document, bool) {
|
||||
if i == nil {
|
||||
return nil, false
|
||||
}
|
||||
doc, ok := i.docs[pathKey(docPath)]
|
||||
return doc, ok
|
||||
}
|
||||
|
||||
// Paths 返回当前索引中的文档路径副本。
|
||||
func (i *Index) Paths() []string {
|
||||
if i == nil || len(i.paths) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, len(i.paths))
|
||||
copy(out, i.paths)
|
||||
return out
|
||||
}
|
||||
|
||||
// List 返回指定 `.lst` 文档的引用列表副本。
|
||||
func (i *Index) List(listPath string) []ListEntry {
|
||||
if i == nil {
|
||||
return nil
|
||||
}
|
||||
entries := i.lists[pathKey(listPath)]
|
||||
out := make([]ListEntry, len(entries))
|
||||
copy(out, entries)
|
||||
return out
|
||||
}
|
||||
|
||||
// Resolve 从指定 `.lst` 索引中按 id 查找 PVF 路径。
|
||||
func (i *Index) Resolve(listPath string, id int64) (string, bool) {
|
||||
if i == nil {
|
||||
return "", false
|
||||
}
|
||||
refs := i.refs[pathKey(listPath)]
|
||||
if refs == nil {
|
||||
return "", false
|
||||
}
|
||||
value, ok := refs[id]
|
||||
return value, ok
|
||||
}
|
||||
|
||||
// Snapshot 返回当前内存索引的文档、列表和引用数量。
|
||||
func (i *Index) Snapshot() Snapshot {
|
||||
if i == nil {
|
||||
return Snapshot{}
|
||||
}
|
||||
refs := 0
|
||||
for _, entries := range i.lists {
|
||||
refs += len(entries)
|
||||
}
|
||||
return Snapshot{
|
||||
Documents: len(i.docs),
|
||||
Lists: len(i.lists),
|
||||
Refs: refs,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Index) buildLists(lists []string, sourcePaths sourcePathIndex) {
|
||||
for _, listPath := range lists {
|
||||
key := pathKey(listPath)
|
||||
doc := i.docs[key]
|
||||
if doc == nil {
|
||||
continue
|
||||
}
|
||||
entries := ParseList(doc)
|
||||
for idx := range entries {
|
||||
entries[idx].Path = resolveListRef(doc.Path, entries[idx].Path, sourcePaths)
|
||||
}
|
||||
i.lists[key] = entries
|
||||
refMap := make(map[int64]string, len(entries))
|
||||
for _, entry := range entries {
|
||||
if _, exists := refMap[entry.ID]; !exists {
|
||||
refMap[entry.ID] = entry.Path
|
||||
}
|
||||
}
|
||||
i.refs[key] = refMap
|
||||
}
|
||||
}
|
||||
|
||||
func collectPaths(source Source, options BuildOptions, sourcePaths sourcePathIndex) []string {
|
||||
pathSet := make(map[string]struct{})
|
||||
paths := make([]string, 0, len(options.Paths)+len(options.Lists))
|
||||
addPath := func(value string) {
|
||||
clean := cleanPath(value)
|
||||
if clean == "" {
|
||||
return
|
||||
}
|
||||
key := pathKey(clean)
|
||||
if _, exists := pathSet[key]; exists {
|
||||
return
|
||||
}
|
||||
pathSet[key] = struct{}{}
|
||||
if actual, ok := sourcePaths.find(clean); ok {
|
||||
paths = append(paths, actual)
|
||||
return
|
||||
}
|
||||
paths = append(paths, clean)
|
||||
}
|
||||
for _, value := range options.Paths {
|
||||
addPath(value)
|
||||
}
|
||||
for _, value := range options.Lists {
|
||||
addPath(value)
|
||||
}
|
||||
if ranger, ok := source.(filePathRanger); ok && (options.All || len(options.Prefixes) > 0) {
|
||||
ranger.RangePaths(func(archivePath string) bool {
|
||||
if options.All || matchPrefix(archivePath, options.Prefixes) {
|
||||
addPath(archivePath)
|
||||
}
|
||||
return true
|
||||
})
|
||||
} else if lister, ok := source.(fileLister); ok && (options.All || len(options.Prefixes) > 0) {
|
||||
for _, file := range lister.Files() {
|
||||
if options.All || matchPrefix(file.ArchivePath, options.Prefixes) {
|
||||
addPath(file.ArchivePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(paths)
|
||||
return paths
|
||||
}
|
||||
|
||||
func sourcePathSet(source Source) sourcePathIndex {
|
||||
if finder, ok := source.(fileFinder); ok {
|
||||
return sourcePathIndex{finder: finder}
|
||||
}
|
||||
lister, ok := source.(fileLister)
|
||||
if !ok {
|
||||
return sourcePathIndex{}
|
||||
}
|
||||
files := lister.Files()
|
||||
if len(files) == 0 {
|
||||
return sourcePathIndex{}
|
||||
}
|
||||
out := make(map[string]string, len(files))
|
||||
for _, file := range files {
|
||||
clean := cleanPath(file.ArchivePath)
|
||||
key := pathKey(clean)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := out[key]; !exists {
|
||||
out[key] = clean
|
||||
}
|
||||
}
|
||||
return sourcePathIndex{paths: out}
|
||||
}
|
||||
|
||||
func resolveListRef(listPath, refPath string, sourcePaths sourcePathIndex) string {
|
||||
ref := cleanPath(refPath)
|
||||
if ref == "" {
|
||||
return ""
|
||||
}
|
||||
if actual, ok := sourcePaths.find(ref); ok {
|
||||
return actual
|
||||
}
|
||||
listDir := cleanPath(path.Dir(cleanPath(listPath)))
|
||||
if listDir == "." {
|
||||
listDir = ""
|
||||
}
|
||||
if listDir != "" {
|
||||
candidate := cleanPath(path.Join(listDir, ref))
|
||||
if actual, ok := sourcePaths.find(candidate); ok {
|
||||
return actual
|
||||
}
|
||||
listDirKey := pathKey(listDir)
|
||||
refKey := pathKey(ref)
|
||||
if listDirKey != "" && strings.HasPrefix(refKey, listDirKey+"/") {
|
||||
return ref
|
||||
}
|
||||
return candidate
|
||||
}
|
||||
return ref
|
||||
}
|
||||
|
||||
func addPath(paths *[]string, docs map[string]*Document, value string) {
|
||||
clean := cleanPath(value)
|
||||
if clean == "" {
|
||||
return
|
||||
}
|
||||
key := pathKey(clean)
|
||||
if _, exists := docs[key]; exists {
|
||||
return
|
||||
}
|
||||
for _, existing := range *paths {
|
||||
if pathKey(existing) == key {
|
||||
return
|
||||
}
|
||||
}
|
||||
*paths = append(*paths, clean)
|
||||
}
|
||||
|
||||
func listKeySet(lists []string) map[string]struct{} {
|
||||
out := make(map[string]struct{}, len(lists))
|
||||
for _, value := range lists {
|
||||
key := pathKey(value)
|
||||
if key != "" {
|
||||
out[key] = struct{}{}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in new issue
Block a user