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

+387
View File
@@ -0,0 +1,387 @@
package services
import (
"strings"
"pvfine/internal/pvf"
)
// searchIndexMutationImpact separates the cheap path snapshot from the
// semantic records that are backed by .lst relations. It is deliberately an
// internal service type: the Wails API continues to expose IndexStatus only.
type searchIndexMutationImpact struct {
pathOnly bool
semantic bool
force bool
dirtyFiles map[int32]struct{}
pendingList map[int32]struct{}
}
func (c *core) scheduleArchiveMutations(a *pvf.Archive, summary pvf.MutationSummary) error {
if a == nil || len(summary.Files) == 0 {
return nil
}
c.mu.Lock()
if c.archive == a {
if err := c.syncSQLiteMutationMetadataLocked(a, summary); err != nil {
c.mu.Unlock()
return err
}
}
if c.archive == a && c.advancedDisk != nil && c.advancedDisk.published.Load() {
c.markAdvancedSearchStaleLocked(a)
for _, mutation := range summary.Files {
if mutation.Identity != 0 {
c.advancedDisk.pendingIDs[mutation.Identity] = struct{}{}
}
}
}
if c.archive == a && c.diskIndex != nil {
c.diskIndex.queueSemanticMutations(summary)
c.mu.Unlock()
c.startSearchIndex()
return nil
}
c.mu.Unlock()
impact := c.classifyArchiveMutations(a, summary)
if !impact.pathOnly && !impact.semantic {
// An initial build may still be scanning the archive. Keep its
// candidate aligned with edits that happen during that scan without
// starting a second build for an otherwise irrelevant payload.
startInitialBuild := false
c.mu.Lock()
if c.archive == a && c.indexStatus.State == IndexStateBuilding {
if c.indexDirty == nil {
c.indexDirty = make(map[int32]struct{})
}
for _, mutation := range summary.Files {
if mutation.Kind != pvf.MutationModified {
continue
}
if index, ok := a.Find(mutation.Path); ok {
c.indexDirty[index] = struct{}{}
}
}
} else if c.archive == a && c.indexStatus.State == IndexStateIdle {
startInitialBuild = true
}
c.mu.Unlock()
if startInitialBuild {
c.startSearchIndex()
}
return nil
}
c.mu.Lock()
if c.archive != a {
c.mu.Unlock()
return nil
}
if impact.pathOnly || impact.semantic {
c.searchIndexDeltaPending = true
}
if len(impact.dirtyFiles) > 0 {
if c.indexDirty == nil {
c.indexDirty = make(map[int32]struct{})
}
for index := range impact.dirtyFiles {
c.indexDirty[index] = struct{}{}
}
}
if len(impact.pendingList) > 0 {
if c.searchIndexListPending == nil {
c.searchIndexListPending = make(map[int32]struct{})
}
for index := range impact.pendingList {
c.searchIndexListPending[index] = struct{}{}
}
}
if c.diskIndex != nil && (impact.pathOnly || impact.semantic) {
c.diskIndex.ready = false
c.diskIndex.dirty = true
}
c.mu.Unlock()
if impact.force {
c.startSearchIndexForced()
} else {
c.startSearchIndex()
}
return nil
}
func (c *core) classifyArchiveMutations(a *pvf.Archive, summary pvf.MutationSummary) searchIndexMutationImpact {
impact := searchIndexMutationImpact{
dirtyFiles: make(map[int32]struct{}),
pendingList: make(map[int32]struct{}),
}
if a == nil || len(summary.Files) == 0 {
return impact
}
c.mu.RLock()
defer c.mu.RUnlock()
if c.archive != a {
return impact
}
seen := make(map[string]struct{}, len(summary.Files))
for _, mutation := range summary.Files {
path := normalizeSearchPath(mutation.Path)
if path == "" {
continue
}
key := string(mutation.Kind) + "\x00" + path
if _, exists := seen[key]; exists {
continue
}
seen[key] = struct{}{}
switch mutation.Kind {
case pvf.MutationAdded, pvf.MutationRemoved:
// The directory/path snapshot and the file fallback records must
// follow structural changes even when no semantic list is touched.
impact.pathOnly = true
if strings.HasSuffix(path, ".lst") {
impact.semantic = true
impact.force = true
}
case pvf.MutationModified:
if strings.HasSuffix(path, ".lst") {
impact.semantic = true
if listIndex, ok := c.searchableListIndexLocked(a, path); ok {
impact.pendingList[listIndex] = struct{}{}
} else {
// String-table maps, NPC lists and unknown list layouts can
// affect more than one relation, so use a full candidate.
impact.force = true
}
continue
}
if strings.HasSuffix(path, ".str") {
if dirty := c.stringTableMutationFilesLocked(a, path); len(dirty) > 0 {
impact.semantic = true
for index := range dirty {
impact.dirtyFiles[index] = struct{}{}
}
}
}
if sameSearchPath(path, npcListPath) || isNPCEntryPath(path) {
if c.npcMutationAffectsSearchLocked(a) {
impact.semantic = true
impact.force = true
}
}
if index, ok := c.registeredMetadataIndexLocked(a, path); ok {
if c.registeredMetadataChangedLocked(a, index, path) {
impact.semantic = true
impact.dirtyFiles[index] = struct{}{}
}
}
}
}
return impact
}
func (c *core) searchableListIndexLocked(a *pvf.Archive, path string) (int32, bool) {
for _, spec := range c.searchableListSpecsLocked() {
index, ok := a.FindList(spec.listPath)
if !ok || !sameSearchPath(a.Path(index), path) {
continue
}
return index, true
}
return 0, false
}
func (c *core) registeredMetadataIndexLocked(a *pvf.Archive, path string) (int32, bool) {
if a == nil {
return 0, false
}
if index, ok := a.Find(path); ok {
if c.memoryMetadataForFileLocked(index) || c.diskMetadataForFileLocked(index) {
return index, true
}
}
for _, value := range c.searchMetadata {
if sameSearchPath(value.path, path) {
return value.fileIndex, true
}
}
return 0, false
}
func (c *core) memoryMetadataForFileLocked(index int32) bool {
for _, value := range c.searchMetadata {
if value.fileIndex == index && value.category != SearchCategoryFile {
return true
}
}
return false
}
func (c *core) diskMetadataForFileLocked(index int32) bool {
if c.diskIndex == nil {
return false
}
return c.diskIndex.hasSemanticFile(index)
}
func (c *core) registeredMetadataChangedLocked(a *pvf.Archive, index int32, path string) bool {
if index < 0 || index >= a.FileCount() {
return true
}
metadata, err := readIndexedMetadataFromArchive(a, index, path, nil)
if err != nil {
return true
}
name := metadata.Name
visuals := visualsFromMetadata(metadata)
matched := false
for _, value := range c.searchMetadata {
if value.fileIndex != index && !sameSearchPath(value.path, path) {
continue
}
if value.category == SearchCategoryFile {
continue
}
matched = true
if value.name != name || value.rarity != visuals.rarity || !imageReferencesEqual(value.icon, visuals.icon) || !imageReferencesEqual(value.fieldImage, visuals.fieldImage) {
return true
}
}
if c.diskIndex != nil && !matched {
oldNames, nameErr := c.diskIndex.indexedNameValues(index)
oldVisuals := c.diskIndex.visuals(index)
if nameErr == nil && (len(oldNames) > 0 || c.diskIndex.hasSemanticFile(index)) {
matched = true
for _, oldName := range oldNames {
if oldName != name {
return true
}
}
if oldVisuals.rarity != visuals.rarity || !imageReferencesEqual(oldVisuals.icon, visuals.icon) || !imageReferencesEqual(oldVisuals.fieldImage, visuals.fieldImage) {
return true
}
}
}
return false
}
func (c *core) stringTableMutationFilesLocked(a *pvf.Archive, path string) map[int32]struct{} {
result := make(map[int32]struct{})
indexes := make([]int32, 0)
seen := make(map[int32]struct{})
for _, value := range c.searchMetadata {
if _, exists := seen[value.fileIndex]; exists {
continue
}
seen[value.fileIndex] = struct{}{}
indexes = append(indexes, value.fileIndex)
}
if len(indexes) == 0 && c.diskIndex != nil {
indexes, _ = c.diskIndex.indexedFileIndexes()
}
for _, fileIndex := range indexes {
metadata, err := a.ScriptMetadata(fileIndex)
if err != nil {
continue
}
usesPath := false
for _, reference := range metadata.StringTableReferences {
for _, candidate := range a.StringTablePaths(reference.Index) {
if sameSearchPath(candidate, path) {
usesPath = true
break
}
}
if usesPath {
break
}
}
if !usesPath {
continue
}
if len(c.searchMetadata) > 0 {
var previous *indexedMetadata
for metadataIndex := range c.searchMetadata {
value := &c.searchMetadata[metadataIndex]
if value.fileIndex == fileIndex && value.category != SearchCategoryFile {
previous = value
break
}
}
if previous == nil {
continue
}
updated, err := readIndexedMetadataFromArchive(a, fileIndex, previous.listPath, nil)
if err != nil || updated.Name != previous.name ||
!imageReferencesEqual(imageReferenceFromPVF(updated.Icon), previous.icon) ||
!imageReferencesEqual(imageReferenceFromPVF(updated.FieldImage), previous.fieldImage) {
result[fileIndex] = struct{}{}
}
continue
}
if c.diskIndex != nil {
oldNames, _ := c.diskIndex.indexedNameValues(fileIndex)
updated, metadataErr := readIndexedMetadataFromArchive(a, fileIndex, a.Path(fileIndex), nil)
changed := metadataErr != nil
if !changed {
if len(oldNames) == 0 || len(oldNames) > 0 && oldNames[0] != updated.Name {
changed = true
}
oldVisuals := c.diskIndex.visuals(fileIndex)
changed = changed || !imageReferencesEqual(oldVisuals.icon, imageReferenceFromPVF(updated.Icon)) ||
!imageReferencesEqual(oldVisuals.fieldImage, imageReferenceFromPVF(updated.FieldImage))
}
if changed {
result[fileIndex] = struct{}{}
}
}
}
return result
}
func (c *core) npcMutationAffectsSearchLocked(a *pvf.Archive) bool {
if len(c.searchMetadata) == 0 && c.diskIndex != nil {
// The disk index does not keep a separate NPC dependency table. Compare
// the current resolved display name with the names actually indexed for
// each item-shop file instead of refreshing merely because an item-shop
// record exists.
indexes, err := c.diskIndex.indexedFileIndexes()
if err != nil {
return true
}
for _, fileIndex := range indexes {
if fileIndex < 0 || fileIndex >= a.FileCount() || !isItemShopEntry("", a.Path(fileIndex)) {
continue
}
metadata, metadataErr := readIndexedMetadataFromArchive(a, fileIndex, itemShopListPath, nil)
oldNames, namesErr := c.diskIndex.indexedNameValues(fileIndex)
if metadataErr != nil || namesErr != nil {
return true
}
matched := false
for _, oldName := range oldNames {
if oldName == metadata.Name {
matched = true
break
}
}
if !matched && (metadata.Name != "" || len(oldNames) > 0) {
return true
}
}
return false
}
for _, value := range c.searchMetadata {
if value.category == SearchCategoryFile || !isItemShopEntry("", value.path) {
continue
}
metadata, err := readIndexedMetadataFromArchive(a, value.fileIndex, itemShopListPath, nil)
if err == nil && metadata.Name != value.name {
return true
}
}
return false
}