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,350 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
modpkg "pvfine/internal/mod"
|
||||
"pvfine/internal/pvf"
|
||||
pvfversion "pvfine/internal/version"
|
||||
)
|
||||
|
||||
type exportContent struct {
|
||||
path string
|
||||
dataType int32
|
||||
data []byte
|
||||
}
|
||||
|
||||
type exportChange struct{ before, after *exportContent }
|
||||
|
||||
type exportDiagnostics struct {
|
||||
skipped int
|
||||
}
|
||||
|
||||
func (d *exportDiagnostics) skip(format string, args ...any) {
|
||||
d.skipped++
|
||||
log.Printf("[pvfine:export] skipped: "+format, args...)
|
||||
}
|
||||
|
||||
func isExportMergeTable(p string) bool {
|
||||
ext := strings.ToLower(path.Ext(p))
|
||||
return ext == ".lst" || ext == ".str"
|
||||
}
|
||||
|
||||
// Historical reads resolve only requested logical objects, never checkout a
|
||||
// commit into the live archive or re-encode an entire PVF.
|
||||
type exportSource struct {
|
||||
archive *pvf.Archive
|
||||
repo *pvfversion.Repository
|
||||
base *pvf.Archive
|
||||
snapshot pvfversion.Snapshot
|
||||
canonical bool
|
||||
skippedTables int
|
||||
diagnostics exportDiagnostics
|
||||
}
|
||||
|
||||
func (s *exportSource) lookup(p string) (string, bool) {
|
||||
if s.snapshot != nil {
|
||||
e, ok := s.snapshot[pvfversion.CanonicalPath(p)]
|
||||
return e.Path, ok
|
||||
}
|
||||
i, ok := s.archive.Find(p)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
return s.archive.Path(i), true
|
||||
}
|
||||
|
||||
func (s *exportSource) read(p string) (*exportContent, error) {
|
||||
if s.snapshot != nil {
|
||||
e, ok := s.snapshot[pvfversion.CanonicalPath(p)]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("导出文件不存在: %s", p)
|
||||
}
|
||||
return s.versionContent(e)
|
||||
}
|
||||
i, ok := s.archive.Find(p)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("导出文件不存在: %s", p)
|
||||
}
|
||||
t := s.archive.File(i).DataType
|
||||
var data []byte
|
||||
if t == pvf.TypeScript || t == pvf.TypeUnicode {
|
||||
var text string
|
||||
var err error
|
||||
if s.canonical {
|
||||
text, err = s.archive.CanonicalText(i)
|
||||
} else {
|
||||
text, err = s.archive.Text(i)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data = []byte(text)
|
||||
} else {
|
||||
raw, err := s.archive.RawBytes(i)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data = append([]byte(nil), raw...)
|
||||
}
|
||||
return &exportContent{path: s.archive.Path(i), dataType: t, data: data}, nil
|
||||
}
|
||||
|
||||
func (s *exportSource) versionContent(e pvfversion.Entry) (*exportContent, error) {
|
||||
content, err := readVersionContent(s.repo, s.base, e)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &exportContent{path: e.Path, dataType: e.DataType, data: append([]byte(nil), content.Raw...)}, nil
|
||||
}
|
||||
|
||||
func (s *ExportService) exportSourceLocked(r ExportRequest) (*exportSource, []exportChange, error) {
|
||||
a := s.c.archive
|
||||
source := &exportSource{archive: a}
|
||||
changes := []exportChange{}
|
||||
if r.Source == "archive-diff" {
|
||||
return s.archiveDiffExportSource(r)
|
||||
}
|
||||
if r.Source == "memory" {
|
||||
for index := int32(0); index < a.FileCount(); index++ {
|
||||
if !a.IsModified(index) {
|
||||
continue
|
||||
}
|
||||
if r.Mode == "mod" && isExportMergeTable(a.Path(index)) {
|
||||
source.skippedTables++
|
||||
developmentLog("[pvfine:export] skip table without change baseline: %s", a.Path(index))
|
||||
continue
|
||||
}
|
||||
content, err := source.read(a.Path(index))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if r.Mode == "direct" && content.dataType != pvf.TypeScript && content.dataType != pvf.TypeUnicode {
|
||||
content.data = nil
|
||||
}
|
||||
changes = append(changes, exportChange{after: content})
|
||||
}
|
||||
return source, changes, nil
|
||||
}
|
||||
if r.Source == "selection" {
|
||||
var selections []exportSelection
|
||||
if s.c.diskIndex != nil {
|
||||
selections = collectDiskExportSelections(a, s.c.diskIndex, r.Scopes)
|
||||
} else {
|
||||
selections = collectExportSelections(a, s.c.sortedPaths, r.Scopes)
|
||||
}
|
||||
for _, selection := range selections {
|
||||
if r.Mode == "mod" && isExportMergeTable(selection.path) {
|
||||
source.skippedTables++
|
||||
developmentLog("[pvfine:export] skip table without change baseline: %s", selection.path)
|
||||
continue
|
||||
}
|
||||
content, err := source.read(selection.path)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
// Existing selection export renders unknown types as empty text.
|
||||
if r.Mode == "direct" && content.dataType != pvf.TypeScript && content.dataType != pvf.TypeUnicode {
|
||||
content.data = nil
|
||||
}
|
||||
changes = append(changes, exportChange{after: content})
|
||||
}
|
||||
return source, changes, nil
|
||||
}
|
||||
if r.Source != "working" && r.Source != "commit" {
|
||||
return nil, nil, fmt.Errorf("未知导出来源: %s", r.Source)
|
||||
}
|
||||
if err := s.c.ensureVersionReadyLocked(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
source.repo, source.base, source.canonical = s.c.versionRepo, s.c.versionBaseArchive, true
|
||||
if source.repo == nil {
|
||||
return nil, nil, ErrVersionNotEnabled
|
||||
}
|
||||
if source.base == nil {
|
||||
var err error
|
||||
source.base, err = pvf.Open(source.repo.BasePath())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
fileChanges := s.c.versionChanges
|
||||
if r.Source == "commit" {
|
||||
if strings.TrimSpace(r.CommitID) == "" {
|
||||
return nil, nil, fmt.Errorf("提交 ID 不能为空")
|
||||
}
|
||||
var err error
|
||||
fileChanges, err = source.repo.Changes(r.CommitID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if r.IncludeDependencies && r.Mode == "mod" {
|
||||
source.snapshot, err = source.repo.Snapshot(r.CommitID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, change := range fileChanges {
|
||||
item := exportChange{}
|
||||
readState := func(hash string, typ int32, p string) (*exportContent, error) {
|
||||
if hash == "" {
|
||||
return nil, nil
|
||||
}
|
||||
return source.versionContent(pvfversion.Entry{Path: p, Hash: hash, DataType: typ})
|
||||
}
|
||||
var err error
|
||||
beforePath := change.DisplayPath
|
||||
if change.OldDisplayPath != "" {
|
||||
beforePath = change.OldDisplayPath
|
||||
}
|
||||
if r.Mode == "mod" && change.AfterHash == "" {
|
||||
item.before = &exportContent{path: beforePath, dataType: change.BeforeType}
|
||||
} else if (r.Mode == "mod" && isExportMergeTable(change.DisplayPath)) || change.AfterHash == "" {
|
||||
item.before, err = readState(change.BeforeHash, change.BeforeType, beforePath)
|
||||
if err != nil {
|
||||
if r.Mode == "mod" && isExportMergeTable(change.DisplayPath) {
|
||||
source.diagnostics.skip("无法读取合并表基线 %s: %v", beforePath, err)
|
||||
continue
|
||||
}
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
if change.AfterHash != "" {
|
||||
if r.Source == "working" {
|
||||
item.after, err = source.read(change.DisplayPath)
|
||||
} else {
|
||||
item.after, err = readState(change.AfterHash, change.AfterType, change.DisplayPath)
|
||||
}
|
||||
if err != nil {
|
||||
if r.Mode == "mod" && isExportMergeTable(change.DisplayPath) {
|
||||
source.diagnostics.skip("无法读取合并表 %s: %v", change.DisplayPath, err)
|
||||
continue
|
||||
}
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
changes = append(changes, item)
|
||||
}
|
||||
return source, changes, nil
|
||||
}
|
||||
|
||||
func canonicalExportDestination(p string) string { return strings.ToLower(filepath.Clean(p)) }
|
||||
|
||||
func exportModEntry(after exportContent, before *exportContent) (*modpkg.Entry, int, error) {
|
||||
return exportModEntryReported(after, before, &exportDiagnostics{})
|
||||
}
|
||||
|
||||
func exportModEntryReported(after exportContent, before *exportContent, diagnostics *exportDiagnostics) (*modpkg.Entry, int, error) {
|
||||
entry := modpkg.Entry{
|
||||
Path: after.path, Operation: modpkg.ReplaceFile, Encoding: modpkg.Text,
|
||||
DataType: after.dataType, Data: bytes.TrimPrefix(after.data, []byte("\xef\xbb\xbf")),
|
||||
}
|
||||
switch strings.ToLower(path.Ext(after.path)) {
|
||||
case ".lst":
|
||||
entry.Operation = modpkg.MergeList
|
||||
case ".str":
|
||||
entry.Operation = modpkg.MergeStrings
|
||||
default:
|
||||
return &entry, 0, nil
|
||||
}
|
||||
pairs, err := parseExportPairsReported(after, entry.Operation, diagnostics)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
entry.Data = nil
|
||||
if before == nil {
|
||||
entry.Pairs = pairs
|
||||
if len(pairs) == 0 {
|
||||
return nil, 0, nil
|
||||
}
|
||||
return &entry, 0, nil
|
||||
}
|
||||
oldPairs, err := parseExportPairsReported(*before, entry.Operation, diagnostics)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
old, current := pairMap(oldPairs), pairMap(pairs)
|
||||
for _, p := range pairs {
|
||||
if v, ok := old[p.Key]; !ok || v != p.Value {
|
||||
entry.Pairs = append(entry.Pairs, p)
|
||||
}
|
||||
}
|
||||
removed := 0
|
||||
for key := range old {
|
||||
if _, exists := current[key]; !exists {
|
||||
removed++
|
||||
}
|
||||
}
|
||||
if len(entry.Pairs) == 0 {
|
||||
return nil, removed, nil
|
||||
}
|
||||
return &entry, removed, nil
|
||||
}
|
||||
|
||||
func pairMap(pairs []modpkg.Pair) map[string]string {
|
||||
m := make(map[string]string, len(pairs))
|
||||
for _, p := range pairs {
|
||||
m[p.Key] = p.Value
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func parseExportPairs(content exportContent, operation modpkg.Operation) ([]modpkg.Pair, error) {
|
||||
return parseExportPairsReported(content, operation, &exportDiagnostics{})
|
||||
}
|
||||
|
||||
func parseExportPairsReported(content exportContent, operation modpkg.Operation, diagnostics *exportDiagnostics) ([]modpkg.Pair, error) {
|
||||
if !utf8.Valid(content.data) || (content.dataType != pvf.TypeScript && content.dataType != pvf.TypeUnicode) {
|
||||
return nil, fmt.Errorf("合并表的内容类型或编码不支持: %s", content.path)
|
||||
}
|
||||
text := strings.TrimPrefix(string(content.data), "\ufeff")
|
||||
pairs := []modpkg.Pair{}
|
||||
if operation == modpkg.MergeStrings {
|
||||
if content.dataType != pvf.TypeUnicode {
|
||||
return nil, fmt.Errorf("文字表不是文本类型: %s", content.path)
|
||||
}
|
||||
positions := map[string]int{}
|
||||
number := 0
|
||||
for line := range strings.SplitSeq(text, "\n") {
|
||||
number++
|
||||
line = strings.TrimSuffix(line, "\r")
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "//") {
|
||||
continue
|
||||
}
|
||||
sep := strings.IndexByte(line, '>')
|
||||
if sep <= 0 || strings.ContainsRune(line, '\x00') {
|
||||
diagnostics.skip("无法解析文字表 %s 第 %d 行", content.path, number)
|
||||
continue
|
||||
}
|
||||
key, value := line[:sep], line[sep+1:]
|
||||
// Retain the first position and last value, without building a
|
||||
// second full table or allocating an array for every source line.
|
||||
if i, exists := positions[key]; exists {
|
||||
pairs[i].Value = value
|
||||
} else {
|
||||
positions[key] = len(pairs)
|
||||
pairs = append(pairs, modpkg.Pair{Key: key, Value: value})
|
||||
}
|
||||
}
|
||||
return pairs, nil
|
||||
}
|
||||
if content.dataType != pvf.TypeScript {
|
||||
return nil, fmt.Errorf("列表不是脚本类型: %s", content.path)
|
||||
}
|
||||
list, err := pvf.ParseListText(text)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("无法解析列表 %s: %w", content.path, err)
|
||||
}
|
||||
for _, pair := range list {
|
||||
pairs = append(pairs, modpkg.Pair{Key: pair.ID, Value: strings.ReplaceAll(pair.Path, "\\", "/")})
|
||||
}
|
||||
return pairs, nil
|
||||
}
|
||||
Reference in new issue
Block a user