DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
hanfuit committed 2026-11-04 14:31:55 +08:00
commit 645b64fe21
4388 files changed
+1405457

No files matched your search

+349
View File
@@ -0,0 +1,349 @@
// Package version implements pvfine's local, logical-file version store.
//
// A version repository stores PVF entry payloads and paths. The packed PVF
// file remains an export artifact and is never used as a binary diff target.
package version
import (
"bytes"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"fmt"
"io"
"os"
"sort"
"strconv"
"strings"
"pvfine/internal/pvf"
)
// Operation describes how a path changed between two logical snapshots.
type Operation string
const (
OperationAdd Operation = "add"
OperationModify Operation = "modify"
OperationDelete Operation = "delete"
OperationRename Operation = "rename"
)
// Entry is the versioned state of one logical PVF file.
type Entry struct {
Path string
DataType int32
Hash string
}
// Snapshot is keyed by a canonical, case-insensitive PVF path.
type Snapshot map[string]Entry
// Content is an entry together with its logical payload. Script and Unicode
// files use stable canonical rendered text because script token bytes refer to
// PVF string pools; unknown types retain their raw bytes. It is used by the
// in-memory undo stack, while committed content is wrapped and stored in
// ObjectStore.
type Content struct {
Entry
Raw []byte
}
// ContentSnapshot contains only the paths affected by one mutation.
type ContentSnapshot map[string]Content
// FileChange is the compact change record stored in a commit.
type FileChange struct {
Path string
DisplayPath string
OldPath string
OldDisplayPath string
Operation Operation
BeforeHash string
AfterHash string
BeforeType int32
AfterType int32
}
// Commit is the public repository commit model.
type Commit struct {
ID string
ParentID string
Message string
CreatedAt int64
ChangeCount int
}
// ArtifactState identifies the last PVF artifact written by the editor.
// ArtifactCommitID is the HEAD at the time of that save; it may differ from
// the repository's current HEAD when a commit was created without saving.
type ArtifactState struct {
PVFHash string
TreeHash string
CommitID string
}
// Meta describes repository compatibility and recovery information.
type Meta struct {
Format int
BaseFile string
BaseHash string
HashAlgorithm string
Artifact ArtifactState
}
// CanonicalPath returns the path identity used by the repository. PVF path
// lookup is case-insensitive, so case-only differences are intentionally not
// treated as distinct files in V1.
func CanonicalPath(path string) string {
path = strings.ReplaceAll(strings.TrimSpace(path), "\\", "/")
path = strings.Trim(path, "/")
return strings.ToLower(path)
}
// DisplayPath normalizes separators while retaining the original casing for
// UI display and newly-created archive entries.
func DisplayPath(path string) string {
return strings.Trim(strings.ReplaceAll(strings.TrimSpace(path), "\\", "/"), "/")
}
// HashBytes returns the V1 content hash format used in the object store.
func HashBytes(raw []byte) string {
sum := sha256.Sum256(raw)
return hex.EncodeToString(sum[:])
}
// EncodeContent makes an object self-describing. The four-byte type prefix is
// part of the hashed object, so identical text in different PVF data types is
// never confused during checkout.
func EncodeContent(dataType int32, data []byte) []byte {
result := make([]byte, 4+len(data))
binary.LittleEndian.PutUint32(result, uint32(dataType))
copy(result[4:], data)
return result
}
// DecodeContent validates and unwraps an object payload.
func DecodeContent(raw []byte) (int32, []byte, error) {
if len(raw) < 4 {
return 0, nil, fmt.Errorf("版本对象内容过短")
}
return int32(binary.LittleEndian.Uint32(raw)), append([]byte(nil), raw[4:]...), nil
}
func contentFromArchive(a *pvf.Archive, index int32) (Content, error) {
file := a.File(index)
var data []byte
var err error
switch file.DataType {
case pvf.TypeScript, pvf.TypeUnicode:
var text string
text, err = a.CanonicalText(index)
data = []byte(text)
default:
var raw []byte
raw, err = a.RawBytes(index)
data = append([]byte(nil), raw...)
}
if err != nil {
return Content{}, err
}
return Content{
Entry: Entry{Path: DisplayPath(a.Path(index)), DataType: file.DataType},
Raw: data,
}, nil
}
func finalizeContent(content Content) Content {
content.Entry.Hash = HashBytes(EncodeContent(content.DataType, content.Raw))
content.Raw = append([]byte(nil), content.Raw...)
return content
}
// HashFile hashes a packed PVF without retaining a second copy in memory.
func HashFile(path string) (string, error) {
file, err := os.Open(path)
if err != nil {
return "", err
}
defer file.Close()
h := sha256.New()
if _, err := io.Copy(h, file); err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}
// SnapshotFromArchive reads raw payloads from an archive and returns a
// logical snapshot. It rejects duplicate canonical paths because an index is
// not a stable identity across PVF structural edits.
func SnapshotFromArchive(a *pvf.Archive) (Snapshot, error) {
if a == nil {
return nil, fmt.Errorf("版本快照的归档为空")
}
result := make(Snapshot, a.FileCount())
for index := int32(0); index < a.FileCount(); index++ {
path := DisplayPath(a.Path(index))
key := CanonicalPath(path)
if key == "" {
return nil, fmt.Errorf("文件 %d 的路径为空,无法建立版本身份", index)
}
if _, exists := result[key]; exists {
return nil, fmt.Errorf("归档包含重复路径 %q, V1 版本控制暂不支持", path)
}
content, err := contentFromArchive(a, index)
if err != nil {
return nil, fmt.Errorf("读取 %q 失败: %w", path, err)
}
content.Entry.Path = path
result[key] = finalizeContent(content).Entry
}
return result, nil
}
// ContentSnapshotFromArchive captures selected paths for an undoable
// mutation. Missing paths are intentionally omitted.
func ContentSnapshotFromArchive(a *pvf.Archive, paths []string) (ContentSnapshot, error) {
if a == nil {
return nil, fmt.Errorf("版本快照的归档为空")
}
result := make(ContentSnapshot, len(paths))
seen := make(map[string]struct{}, len(paths))
for _, rawPath := range paths {
key := CanonicalPath(rawPath)
if key == "" {
continue
}
if _, done := seen[key]; done {
continue
}
seen[key] = struct{}{}
index, ok := a.Find(key)
if !ok {
continue
}
path := DisplayPath(a.Path(index))
content, err := contentFromArchive(a, index)
if err != nil {
return nil, fmt.Errorf("读取 %q 失败: %w", path, err)
}
content.Entry.Path = path
result[key] = finalizeContent(content)
}
return result, nil
}
// ContentObject returns the self-describing object bytes for a content value.
func ContentObject(content Content) []byte {
return EncodeContent(content.DataType, content.Raw)
}
// SameContent reports whether two content values represent the same logical
// object, including the PVF data type.
func SameContent(left, right Content) bool {
return left.DataType == right.DataType && bytes.Equal(left.Raw, right.Raw)
}
// SnapshotFromContent converts a partial content snapshot to a regular
// snapshot, preserving only present entries.
func SnapshotFromContent(values ContentSnapshot) Snapshot {
result := make(Snapshot, len(values))
for key, value := range values {
result[key] = value.Entry
}
return result
}
// CloneSnapshot makes a detached snapshot copy.
func CloneSnapshot(values Snapshot) Snapshot {
result := make(Snapshot, len(values))
for key, value := range values {
result[key] = value
}
return result
}
// CloneContentSnapshot makes a detached content snapshot copy.
func CloneContentSnapshot(values ContentSnapshot) ContentSnapshot {
result := make(ContentSnapshot, len(values))
for key, value := range values {
result[key] = Content{Entry: value.Entry, Raw: append([]byte(nil), value.Raw...)}
}
return result
}
// TreeHash is a deterministic hash of a logical snapshot, independent of
// map iteration order and PVF binary layout.
func TreeHash(values Snapshot) string {
keys := make([]string, 0, len(values))
for key := range values {
keys = append(keys, key)
}
sort.Strings(keys)
h := sha256.New()
for _, key := range keys {
entry := values[key]
// Length-prefix strings so unusual PVF paths cannot create ambiguous
// concatenations.
_, _ = h.Write([]byte(strconv.Itoa(len(key))))
_, _ = h.Write([]byte(":"))
_, _ = h.Write([]byte(key))
_, _ = h.Write([]byte(":"))
_, _ = h.Write([]byte(strconv.FormatInt(int64(entry.DataType), 10)))
_, _ = h.Write([]byte(":"))
_, _ = h.Write([]byte(entry.Hash))
_, _ = h.Write([]byte("\n"))
}
return hex.EncodeToString(h.Sum(nil))
}
// Diff returns deterministic path-level changes from before to after.
func Diff(before, after Snapshot) []FileChange {
keys := make(map[string]struct{}, len(before)+len(after))
for key := range before {
keys[key] = struct{}{}
}
for key := range after {
keys[key] = struct{}{}
}
ordered := make([]string, 0, len(keys))
for key := range keys {
ordered = append(ordered, key)
}
sort.Strings(ordered)
changes := make([]FileChange, 0, len(ordered))
for _, key := range ordered {
oldEntry, hadOld := before[key]
newEntry, hasNew := after[key]
switch {
case !hadOld && hasNew:
changes = append(changes, FileChange{
Path: key,
DisplayPath: newEntry.Path,
Operation: OperationAdd,
AfterHash: newEntry.Hash,
AfterType: newEntry.DataType,
})
case hadOld && !hasNew:
changes = append(changes, FileChange{
Path: key,
DisplayPath: oldEntry.Path,
Operation: OperationDelete,
BeforeHash: oldEntry.Hash,
BeforeType: oldEntry.DataType,
})
case hadOld && hasNew && (oldEntry.Hash != newEntry.Hash || oldEntry.DataType != newEntry.DataType):
changes = append(changes, FileChange{
Path: key,
DisplayPath: newEntry.Path,
Operation: OperationModify,
BeforeHash: oldEntry.Hash,
AfterHash: newEntry.Hash,
BeforeType: oldEntry.DataType,
AfterType: newEntry.DataType,
})
}
}
return changes
}