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