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