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,501 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"pvfine/internal/pvf"
|
||||
)
|
||||
|
||||
var errIndexDeltaStale = errors.New("索引增量的基础修订已变化")
|
||||
|
||||
func (i *sqliteArchiveIndex) queueSemanticMutations(summary pvf.MutationSummary) {
|
||||
if i.pendingPaths == nil {
|
||||
i.pendingPaths = make(map[string]struct{})
|
||||
i.pendingIDs = make(map[uint64]struct{})
|
||||
}
|
||||
i.pendingStructural = i.pendingStructural || summary.Structural
|
||||
for _, mutation := range summary.Files {
|
||||
if mutation.Kind != pvf.MutationModified {
|
||||
i.pendingStructural = true
|
||||
}
|
||||
i.pendingPaths[normalizeSearchPath(mutation.Path)] = struct{}{}
|
||||
if mutation.Identity != 0 {
|
||||
i.pendingIDs[mutation.Identity] = struct{}{}
|
||||
}
|
||||
}
|
||||
i.semanticEpoch++
|
||||
i.ready, i.dirty = false, true
|
||||
}
|
||||
|
||||
type semanticDeltaRecord struct {
|
||||
index int32
|
||||
path, id, listPath, category string
|
||||
metadata pvf.ScriptMetadata
|
||||
}
|
||||
|
||||
type semanticRelation struct{ listPath, category string }
|
||||
|
||||
// buildSemanticDelta reads list registrations, but parses script metadata only
|
||||
// for affected files. Publication is a single WAL transaction against the live
|
||||
// projection; no full candidate database is copied or indexes recreated.
|
||||
func (i *sqliteArchiveIndex) buildSemanticDelta(ctx context.Context, c *core, a *pvf.Archive, specs []searchableListSpec, gen uint64) (int, int, error) {
|
||||
c.mu.RLock()
|
||||
if !c.indexIsCurrentLocked(a, gen) || ctx.Err() != nil {
|
||||
c.mu.RUnlock()
|
||||
return 0, 0, context.Canceled
|
||||
}
|
||||
epoch, revision := i.semanticEpoch, c.batchRevision
|
||||
paths, ids := make(map[string]struct{}), make(map[uint64]struct{})
|
||||
for path := range i.pendingPaths {
|
||||
paths[path] = struct{}{}
|
||||
}
|
||||
for id := range i.pendingIDs {
|
||||
ids[id] = struct{}{}
|
||||
}
|
||||
// Ordinary payload edits preserve registrations. Query only their existing
|
||||
// rows and read metadata under the archive lock; no mutable batch clone is
|
||||
// needed. Structural/list/dependency edits retain the general resolver.
|
||||
fast := !i.pendingStructural
|
||||
for path := range paths {
|
||||
if strings.HasSuffix(path, ".lst") || strings.HasSuffix(path, ".str") || isNPCEntryPath(path) {
|
||||
fast = false
|
||||
}
|
||||
}
|
||||
matchedIDs := make(map[uint64]struct{})
|
||||
for path := range paths {
|
||||
if index, found := a.Find(path); found {
|
||||
matchedIDs[a.FileIdentity(index)] = struct{}{}
|
||||
}
|
||||
}
|
||||
for id := range ids {
|
||||
if _, found := matchedIDs[id]; !found {
|
||||
fast = false // duplicate paths require the identity-aware resolver
|
||||
}
|
||||
}
|
||||
i.dbMu.RLock()
|
||||
var fingerprint string
|
||||
if err := i.db.QueryRowContext(ctx, `SELECT value FROM meta WHERE key='specs'`).Scan(&fingerprint); err != nil || fingerprint != searchIndexSpecFingerprint(specs) {
|
||||
fast = false
|
||||
}
|
||||
i.dbMu.RUnlock()
|
||||
if fast {
|
||||
affected := make(map[int32]struct{})
|
||||
var replacements []semanticDeltaRecord
|
||||
for path := range paths {
|
||||
if index, found := a.Find(path); found {
|
||||
affected[index] = struct{}{}
|
||||
}
|
||||
}
|
||||
i.dbMu.RLock()
|
||||
var err error
|
||||
for index := range affected {
|
||||
rows, queryErr := i.db.QueryContext(ctx, `SELECT path,record_id,list_path,category FROM records WHERE file_index=? AND category<>?`, index, SearchCategoryFile)
|
||||
if queryErr != nil {
|
||||
err = queryErr
|
||||
break
|
||||
}
|
||||
for rows.Next() {
|
||||
record := semanticDeltaRecord{index: index}
|
||||
if err = rows.Scan(&record.path, &record.id, &record.listPath, &record.category); err != nil {
|
||||
break
|
||||
}
|
||||
replacements = append(replacements, record)
|
||||
}
|
||||
if err == nil {
|
||||
err = rows.Err()
|
||||
}
|
||||
_ = rows.Close()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
i.dbMu.RUnlock()
|
||||
if err == nil {
|
||||
npcNames := make(map[string]string)
|
||||
for _, record := range replacements {
|
||||
if isItemShopEntry(record.listPath, record.path) {
|
||||
npcNames = buildNPCNameIndexFromArchive(a)
|
||||
break
|
||||
}
|
||||
}
|
||||
for n := range replacements {
|
||||
record := &replacements[n]
|
||||
record.metadata, _ = readIndexedMetadataFromArchive(a, record.index, record.listPath, npcNames)
|
||||
}
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return i.publishSemanticDelta(ctx, c, a, a, specs, gen, epoch, revision, affected, replacements, 0)
|
||||
}
|
||||
snapshot := a.CloneForBatch()
|
||||
knownLists := make(map[string]struct{})
|
||||
for _, spec := range specs {
|
||||
knownLists[normalizeSearchPath(spec.listPath)] = struct{}{}
|
||||
if index, ok := snapshot.FindList(spec.listPath); ok {
|
||||
knownLists[normalizeSearchPath(snapshot.Path(index))] = struct{}{}
|
||||
}
|
||||
}
|
||||
affected := make(map[int32]struct{})
|
||||
for index := int32(0); index < snapshot.FileCount(); index++ {
|
||||
if _, changed := ids[snapshot.FileIdentity(index)]; changed {
|
||||
affected[index] = struct{}{}
|
||||
}
|
||||
}
|
||||
i.dbMu.RLock()
|
||||
oldRelations := make(map[semanticRelation]struct{})
|
||||
rows, err := i.db.QueryContext(ctx, `SELECT DISTINCT list_path,category FROM records WHERE category<>?`, SearchCategoryFile)
|
||||
if err == nil {
|
||||
for rows.Next() {
|
||||
var r semanticRelation
|
||||
if err = rows.Scan(&r.listPath, &r.category); err != nil {
|
||||
break
|
||||
}
|
||||
oldRelations[r] = struct{}{}
|
||||
}
|
||||
if err == nil {
|
||||
err = rows.Err()
|
||||
}
|
||||
_ = rows.Close()
|
||||
}
|
||||
if err == nil {
|
||||
for path := range paths {
|
||||
if strings.HasSuffix(path, ".str") || strings.HasSuffix(path, ".lst") {
|
||||
query, args := `SELECT DISTINCT file_index FROM semantic_dependencies WHERE dependency_path=?`, []any{path}
|
||||
if strings.HasSuffix(path, ".lst") {
|
||||
if _, known := knownLists[path]; known && !pvf.IsStringTableMappingPath(path) {
|
||||
continue
|
||||
}
|
||||
query, args = `SELECT DISTINCT file_index FROM semantic_dependencies`, nil
|
||||
}
|
||||
var depRows *sql.Rows
|
||||
depRows, err = i.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
for depRows.Next() {
|
||||
var index int32
|
||||
if err = depRows.Scan(&index); err != nil {
|
||||
break
|
||||
}
|
||||
affected[index] = struct{}{}
|
||||
}
|
||||
if err == nil {
|
||||
err = depRows.Err()
|
||||
}
|
||||
_ = depRows.Close()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
i.dbMu.RUnlock()
|
||||
c.mu.RUnlock()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
currentRelations := make(map[semanticRelation]struct{})
|
||||
var registrations []semanticDeltaRecord
|
||||
skipped := 0
|
||||
npcChanged := false
|
||||
for path := range paths {
|
||||
if isNPCEntryPath(path) || sameSearchPath(path, npcListPath) {
|
||||
npcChanged = true
|
||||
}
|
||||
}
|
||||
for _, spec := range specs {
|
||||
if err = ctx.Err(); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
listIndex, found := snapshot.FindList(spec.listPath)
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
listPath := snapshot.Path(listIndex)
|
||||
r := semanticRelation{listPath, spec.category}
|
||||
currentRelations[r] = struct{}{}
|
||||
_, previouslyPresent := oldRelations[r]
|
||||
_, listChanged := paths[normalizeSearchPath(listPath)]
|
||||
_, configuredListChanged := paths[normalizeSearchPath(spec.listPath)]
|
||||
pairs, pairErr := snapshot.ListPairs(listIndex)
|
||||
if pairErr != nil {
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
for _, pair := range pairs {
|
||||
target, index, found := findListTargetInArchive(snapshot, listPath, pair.Path)
|
||||
if !found {
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
changed := !previouslyPresent || listChanged || configuredListChanged || (npcChanged && sameSearchPath(spec.listPath, itemShopListPath))
|
||||
candidates, _ := listPathCandidates(listPath, pair.Path)
|
||||
for _, candidate := range candidates {
|
||||
if _, exists := paths[normalizeSearchPath(candidate)]; exists {
|
||||
changed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
affected[index] = struct{}{}
|
||||
}
|
||||
registrations = append(registrations, semanticDeltaRecord{index: index, path: target, id: pair.ID, listPath: listPath, category: spec.category})
|
||||
}
|
||||
}
|
||||
// Old relation targets need fallback records and tag cleanup when their list
|
||||
// is deleted, changed or removed by configuration.
|
||||
c.mu.RLock()
|
||||
if !c.indexIsCurrentLocked(a, gen) || i.semanticEpoch != epoch || c.batchRevision != revision {
|
||||
c.mu.RUnlock()
|
||||
return 0, 0, errIndexDeltaStale
|
||||
}
|
||||
i.dbMu.RLock()
|
||||
for r := range oldRelations {
|
||||
_, exists := currentRelations[r]
|
||||
_, changed := paths[normalizeSearchPath(r.listPath)]
|
||||
if exists && !changed {
|
||||
continue
|
||||
}
|
||||
rows, err = i.db.QueryContext(ctx, `SELECT file_index FROM records WHERE list_path=? AND category=?`, r.listPath, r.category)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
for rows.Next() {
|
||||
var index int32
|
||||
if err = rows.Scan(&index); err != nil {
|
||||
break
|
||||
}
|
||||
affected[index] = struct{}{}
|
||||
}
|
||||
if err == nil {
|
||||
err = rows.Err()
|
||||
}
|
||||
_ = rows.Close()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
i.dbMu.RUnlock()
|
||||
c.mu.RUnlock()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
npcNames := make(map[string]string)
|
||||
for _, record := range registrations {
|
||||
if _, needed := affected[record.index]; needed && isItemShopEntry(record.listPath, record.path) {
|
||||
npcNames = buildNPCNameIndexFromArchive(snapshot)
|
||||
break
|
||||
}
|
||||
}
|
||||
metadataCache := make(map[int32]pvf.ScriptMetadata)
|
||||
var replacements []semanticDeltaRecord
|
||||
for _, record := range registrations {
|
||||
if _, needed := affected[record.index]; !needed {
|
||||
continue
|
||||
}
|
||||
if err = ctx.Err(); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
metadata, cached := metadataCache[record.index]
|
||||
if !cached || isItemShopEntry(record.listPath, record.path) {
|
||||
metadata, err = readIndexedMetadataFromArchive(snapshot, record.index, record.listPath, npcNames)
|
||||
if err != nil {
|
||||
metadata = pvf.ScriptMetadata{}
|
||||
}
|
||||
if !isItemShopEntry(record.listPath, record.path) {
|
||||
metadataCache[record.index] = metadata
|
||||
}
|
||||
}
|
||||
record.metadata = metadata
|
||||
replacements = append(replacements, record)
|
||||
}
|
||||
return i.publishSemanticDelta(ctx, c, a, snapshot, specs, gen, epoch, revision, affected, replacements, skipped)
|
||||
}
|
||||
|
||||
// The revision guard allows the fast path to reuse the live archive safely.
|
||||
// A concurrent mutation is retried before reading any file or dependencies.
|
||||
func (i *sqliteArchiveIndex) publishSemanticDelta(ctx context.Context, c *core, a, snapshot *pvf.Archive, specs []searchableListSpec, gen, epoch, revision uint64, affected map[int32]struct{}, replacements []semanticDeltaRecord, skipped int) (int, int, error) {
|
||||
var err error
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if !c.indexIsCurrentLocked(a, gen) || ctx.Err() != nil {
|
||||
return 0, 0, context.Canceled
|
||||
}
|
||||
if i.semanticEpoch != epoch || c.batchRevision != revision {
|
||||
return 0, 0, errIndexDeltaStale
|
||||
}
|
||||
i.dbMu.Lock()
|
||||
defer i.dbMu.Unlock()
|
||||
if err = i.detachCacheLocked(); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
tx, err := i.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
if _, err = tx.ExecContext(ctx, `CREATE TEMP TABLE semantic_affected(file_index INTEGER PRIMARY KEY)`); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
insertAffected, err := tx.PrepareContext(ctx, `INSERT INTO semantic_affected VALUES(?)`)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
defer insertAffected.Close()
|
||||
for index := range affected {
|
||||
file := snapshot.File(index)
|
||||
if _, err = tx.ExecContext(ctx, `UPDATE files SET size=?,data_type=?,change_kind=? WHERE file_index=?`, file.DataSize, file.DataType, archiveChangeKind(snapshot, index), index); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
if _, err = insertAffected.ExecContext(ctx, index); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
}
|
||||
for _, table := range []string{"records", "tags", "visuals", "semantic_dependencies"} {
|
||||
if _, err = tx.ExecContext(ctx, `DELETE FROM `+table+` WHERE file_index IN (SELECT file_index FROM semantic_affected)`); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
}
|
||||
visualsWritten := make(map[int32]struct{})
|
||||
stmt, err := tx.PrepareContext(ctx, `INSERT INTO records(file_index,name,lower_name,record_id,lower_id,path,lower_path,category,list_path,size,data_type) VALUES(?,?,?,?,?,?,?,?,?,?,?)`)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
dependencies, err := newSemanticDependencyWriter(ctx, tx)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
defer dependencies.close()
|
||||
for _, record := range replacements {
|
||||
file, metadata := snapshot.File(record.index), record.metadata
|
||||
name := markedName(metadata)
|
||||
if _, err = stmt.ExecContext(ctx, record.index, name, strings.ToLower(name), record.id, strings.ToLower(record.id), record.path, strings.ToLower(record.path), record.category, record.listPath, file.DataSize, file.DataType); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
if _, err = tx.ExecContext(ctx, `INSERT OR REPLACE INTO tags VALUES(?,?,?,?)`, record.index, record.id, name, record.category); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
if _, written := visualsWritten[record.index]; !written {
|
||||
icon, field := visualsFromMetadata(metadata).icon, visualsFromMetadata(metadata).fieldImage
|
||||
iconPath, fieldPath := "", ""
|
||||
iconIndex, fieldIndex := int32(-1), int32(-1)
|
||||
if icon != nil {
|
||||
iconPath, iconIndex = icon.Path, icon.Index
|
||||
}
|
||||
if field != nil {
|
||||
fieldPath, fieldIndex = field.Path, field.Index
|
||||
}
|
||||
if _, err = tx.ExecContext(ctx, `INSERT INTO visuals VALUES(?,?,?,?,?,?)`, record.index, iconPath, iconIndex, fieldPath, fieldIndex, metadata.RarityValue()); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
if err = dependencies.write(ctx, snapshot, record.index, metadata); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
visualsWritten[record.index] = struct{}{}
|
||||
}
|
||||
}
|
||||
if err = dependencies.flush(ctx); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
if _, err = tx.ExecContext(ctx, `UPDATE meta SET value='1' WHERE key='complete'; INSERT OR REPLACE INTO meta VALUES('specs',?); DROP TABLE semantic_affected`, searchIndexSpecFingerprint(specs)); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
var total int
|
||||
if err = tx.QueryRowContext(ctx, `SELECT value FROM meta WHERE key='semantic_count'`).Scan(&total); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
i.pendingPaths, i.pendingIDs = nil, nil
|
||||
i.pendingStructural = false
|
||||
i.ready, i.dirty = true, false
|
||||
i.semanticPublishedEpoch, i.semanticPublishedRevision = epoch, revision
|
||||
developmentLog("[pvfine:index] sqlite delta files=%d metadata=%d specs=%d", len(affected), len(replacements), len(specs))
|
||||
return total, skipped, nil
|
||||
}
|
||||
|
||||
// semanticDependencyWriter reuses SQL and mapping expansions for one build.
|
||||
// A file can reference the same table through many tokens; emit each dependency once.
|
||||
type semanticDependencyWriter struct {
|
||||
tx *sql.Tx
|
||||
stmts map[int]*sql.Stmt
|
||||
args []any
|
||||
paths map[int][]string
|
||||
}
|
||||
|
||||
func newSemanticDependencyWriter(ctx context.Context, tx *sql.Tx) (*semanticDependencyWriter, error) {
|
||||
w := &semanticDependencyWriter{tx: tx, stmts: make(map[int]*sql.Stmt), args: make([]any, 0, 256), paths: make(map[int][]string)}
|
||||
if _, err := w.statement(ctx, 128); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return w, nil
|
||||
}
|
||||
|
||||
func (w *semanticDependencyWriter) statement(ctx context.Context, rows int) (*sql.Stmt, error) {
|
||||
if stmt := w.stmts[rows]; stmt != nil {
|
||||
return stmt, nil
|
||||
}
|
||||
query := "INSERT OR IGNORE INTO semantic_dependencies VALUES" + strings.TrimSuffix(strings.Repeat("(?,?),", rows), ",")
|
||||
stmt, err := w.tx.PrepareContext(ctx, query)
|
||||
if err == nil {
|
||||
w.stmts[rows] = stmt
|
||||
}
|
||||
return stmt, err
|
||||
}
|
||||
|
||||
func (w *semanticDependencyWriter) flush(ctx context.Context) error {
|
||||
if len(w.args) == 0 {
|
||||
return nil
|
||||
}
|
||||
stmt, err := w.statement(ctx, len(w.args)/2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = stmt.ExecContext(ctx, w.args...); err != nil {
|
||||
return err
|
||||
}
|
||||
clear(w.args)
|
||||
w.args = w.args[:0]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *semanticDependencyWriter) close() {
|
||||
for _, stmt := range w.stmts {
|
||||
_ = stmt.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (w *semanticDependencyWriter) write(ctx context.Context, a *pvf.Archive, index int32, metadata pvf.ScriptMetadata) error {
|
||||
seen := make(map[string]struct{})
|
||||
for _, reference := range metadata.StringTableReferences {
|
||||
paths, ok := w.paths[reference.Index]
|
||||
if !ok {
|
||||
paths = []string{fmt.Sprintf("@string-table:%d", reference.Index)}
|
||||
for _, path := range a.StringTableDependencyPaths(reference.Index) {
|
||||
paths = append(paths, normalizeSearchPath(path))
|
||||
}
|
||||
w.paths[reference.Index] = paths
|
||||
}
|
||||
for _, path := range paths {
|
||||
if _, exists := seen[path]; exists {
|
||||
continue
|
||||
}
|
||||
seen[path] = struct{}{}
|
||||
w.args = append(w.args, index, path)
|
||||
if len(w.args) == cap(w.args) {
|
||||
if err := w.flush(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user