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

+298
View File
@@ -0,0 +1,298 @@
package annotations
import (
"fmt"
"path"
"sort"
"strings"
"pvfine/internal/pvf"
)
// PreviewFieldValue is one occurrence of a shared field in an editor view.
// Values contains the selected token(s). For a repeated record target it
// contains the complete record so consumers can use context and neighboring
// values (for example, skill context + skill id + level).
type PreviewFieldValue struct {
Field FieldDefinition
Values []string
Context string
Start int
End int
}
// resolveFieldRule lets legacy rules opt into the shared field definition
// without changing the existing rule JSON shape. A rule's id, description and
// group remain local; non-empty target/match/annotation values override the
// field when supplied.
func resolveFieldRule(rule Rule, fields []FieldDefinition) (Rule, error) {
fieldID := strings.TrimSpace(rule.Field)
if fieldID == "" {
return rule, nil
}
for _, field := range fields {
if field.ID != fieldID {
continue
}
resolved := Rule{
ID: rule.ID,
PVFVersions: rule.PVFVersions,
Description: rule.Description,
Field: rule.Field,
Match: field.Match,
Target: field.Target,
Annotation: field.Annotation,
Group: rule.Group,
}
if len(rule.Match.Extensions) > 0 || rule.Match.Glob != "" {
resolved.Match = rule.Match
}
if rule.Target.Kind != "" {
resolved.Target = rule.Target
}
if rule.Annotation.Title != "" || rule.Annotation.Type != "" ||
rule.Annotation.Content != "" || len(rule.Annotation.Values) > 0 ||
rule.Annotation.Relation != "" || rule.Annotation.PathRoot != "" || rule.Annotation.InlineImage {
resolved.Annotation = rule.Annotation
}
return resolved, nil
}
return Rule{}, fmt.Errorf("引用了不存在的共享字段: %s", fieldID)
}
// PreviewFields returns the provider-specific shared fields that match a
// path, in stable display order. The returned definitions are copies of the
// immutable engine document and are safe for callers to retain.
func (e *Engine) PreviewFields(filePath, provider string) []FieldDefinition {
if e == nil {
return nil
}
items := make([]FieldDefinition, 0)
for _, field := range e.document.Fields {
if field.Preview == nil || !previewSupportsProvider(field.Preview, provider) {
continue
}
if !fieldMatches(field.Match, filePath, false) {
continue
}
items = append(items, field)
}
sort.SliceStable(items, func(i, j int) bool {
left, right := items[i].Preview, items[j].Preview
if left.Order != right.Order {
return left.Order < right.Order
}
return items[i].ID < items[j].ID
})
return items
}
func previewSupportsProvider(preview *PreviewSpec, provider string) bool {
provider = strings.TrimSpace(provider)
if preview == nil || provider == "" {
return false
}
for _, candidate := range preview.providerNames() {
if strings.EqualFold(candidate, provider) {
return true
}
}
return false
}
// ExtractPreviewFields projects a ScriptView through provider field targets.
// It deliberately shares the same target semantics as annotations, including
// repeated records, offsets and dynamic record widths.
func (e *Engine) ExtractPreviewFields(filePath string, view pvf.ScriptView, provider string) []PreviewFieldValue {
fields := e.PreviewFields(filePath, provider)
values := make([]PreviewFieldValue, 0)
for _, field := range fields {
for _, occurrence := range extractField(field, view) {
values = append(values, occurrence)
}
}
return values
}
func extractField(field FieldDefinition, view pvf.ScriptView) []PreviewFieldValue {
target := field.Target
if target.Kind == "section" {
result := make([]PreviewFieldValue, 0)
for _, sectionID := range sectionIDs(view, target.Section) {
tokens := sectionTokens(view, target.Section, sectionID)
if len(tokens) == 0 {
continue
}
result = append(result, PreviewFieldValue{
Field: field, Values: valuesOf(tokens),
Start: tokens[0].Start, End: tokens[len(tokens)-1].End,
})
}
return result
}
if target.Kind != "token" {
return nil
}
result := make([]PreviewFieldValue, 0)
for _, sectionID := range sectionIDs(view, target.Section) {
tokens := sectionTokens(view, target.Section, sectionID)
if target.Index != nil && target.RecordTokens > 0 {
recordTokens := target.RecordTokens
if target.TokensPerLineIndex != nil {
recordTokens = repeatedRecordTokens(target, tokens)
}
if recordTokens <= 0 {
continue
}
for _, group := range repeatedTokenGroups(target, tokens) {
for start := group.start; start+recordTokens <= group.end; start += recordTokens {
offset := *target.Index
if offset < 0 || offset >= recordTokens {
continue
}
record := tokens[start : start+recordTokens]
context := ""
if target.ContextIndex != nil && *target.ContextIndex >= 0 && *target.ContextIndex < len(record) {
context = record[*target.ContextIndex].Value
}
result = append(result, PreviewFieldValue{
Field: field, Values: valuesOf(record), Context: context,
Start: record[offset].Start, End: record[offset].End,
})
}
}
continue
}
if target.Index != nil {
index := *target.Index
if index >= 0 && index < len(tokens) {
token := tokens[index]
result = append(result, PreviewFieldValue{Field: field, Values: []string{token.Value}, Start: token.Start, End: token.End})
}
continue
}
if target.Range == nil {
continue
}
start := maxInt(0, target.Range.Start)
end := minInt(len(tokens), target.Range.EndExclusive)
if start >= end {
continue
}
result = append(result, PreviewFieldValue{
Field: field, Values: valuesOf(tokens[start:end]),
Start: tokens[start].Start, End: tokens[end-1].End,
})
}
return result
}
func fieldMatches(match MatchSpec, filePath string, isDir bool) bool {
filePath = normalizeFieldPath(filePath)
if len(match.Extensions) > 0 {
if isDir {
return false
}
matched := false
for _, extension := range match.Extensions {
if strings.EqualFold(path.Ext(filePath), strings.TrimSpace(extension)) {
matched = true
break
}
}
if !matched {
return false
}
}
if match.Glob == "" {
return true
}
return globFieldMatch(match.Glob, filePath)
}
func globFieldMatch(pattern, value string) bool {
patternParts := strings.Split(normalizeFieldPath(pattern), "/")
valueParts := strings.Split(normalizeFieldPath(value), "/")
type state struct{ pattern, value int }
memo := make(map[state]bool)
seen := make(map[state]bool)
var match func(int, int) bool
match = func(pi, vi int) bool {
key := state{pi, vi}
if seen[key] {
return memo[key]
}
seen[key] = true
if pi == len(patternParts) {
memo[key] = vi == len(valueParts)
return memo[key]
}
if patternParts[pi] == "**" {
memo[key] = match(pi+1, vi) || (vi < len(valueParts) && match(pi, vi+1))
return memo[key]
}
if vi >= len(valueParts) {
return false
}
matched, err := path.Match(patternParts[pi], valueParts[vi])
if err == nil && matched {
memo[key] = match(pi+1, vi+1)
}
return memo[key]
}
return match(0, 0)
}
func normalizeFieldPath(value string) string {
return strings.ToLower(strings.Trim(strings.ReplaceAll(value, "\\", "/"), "/"))
}
func sectionIDs(view pvf.ScriptView, name string) []int {
result := make([]int, 0)
seen := make(map[int]struct{})
for _, element := range view.Elements {
if element.Kind != pvf.ScriptElementSection || !strings.EqualFold(element.Section, name) {
continue
}
if _, ok := seen[element.SectionID]; ok {
continue
}
seen[element.SectionID] = struct{}{}
result = append(result, element.SectionID)
}
return result
}
func sectionTokens(view pvf.ScriptView, name string, sectionID int) []pvf.ScriptElement {
result := make([]pvf.ScriptElement, 0)
for _, element := range view.Elements {
if element.Kind == pvf.ScriptElementToken && element.SectionID == sectionID && strings.EqualFold(element.Section, name) {
result = append(result, element)
}
}
return result
}
func valuesOf(elements []pvf.ScriptElement) []string {
result := make([]string, len(elements))
for i, element := range elements {
result[i] = element.Value
}
return result
}
func maxInt(a, b int) int {
if a > b {
return a
}
return b
}
func minInt(a, b int) int {
if a < b {
return a
}
return b
}