Files
90CNServer/pvf/services/independent_drop.go
T
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

345 lines
10 KiB
Go

package services
import (
"fmt"
"math"
"path"
"strconv"
"strings"
"unicode/utf16"
"pvfine/internal/pvf"
)
const independentDropPath = "etc/independent_drop.etc"
type IndependentDropList struct {
ID string `json:"id"`
Path string `json:"path"`
FileIndex int32 `json:"fileIndex"`
}
type IndependentDropItem struct {
Fields []SkillNumber `json:"fields"`
Name string `json:"name"`
}
type IndependentDropRow struct {
ListTagStart int `json:"listTagStart"`
Fields []SkillNumber `json:"fields"`
Items []IndependentDropItem `json:"items"`
MonsterName string `json:"monsterName"`
ItemName string `json:"itemName"`
Start int `json:"start"`
End int `json:"end"`
ListStart int `json:"listStart"`
ListEnd int `json:"listEnd"`
HasExtensions bool `json:"hasExtensions"`
}
type IndependentDropDocument struct {
Version string `json:"version"`
Rows []IndependentDropRow `json:"rows"`
Lists []IndependentDropList `json:"lists"`
Balance []SkillNumber `json:"balance"`
BodyEnd int `json:"bodyEnd"`
IsList bool `json:"isList"`
Issues []string `json:"issues"`
}
// Positions use the editor's normalized UTF-16 coordinate system. Unknown
// sections are never reserialized; the GUI changes only selected source spans.
func parseIndependentDrop(text string, listOnly bool) (*IndependentDropDocument, error) {
view := pvf.ParseScriptViewWithNestedSections(text, func(parent, child string) bool {
return parent == "independent drop" && (child == "list" || child == "replace list")
})
normalized := strings.ReplaceAll(strings.ReplaceAll(text, "\r\n", "\n"), "\r", "\n")
units := utf16.Encode([]rune(normalized))
closing := func(start int, tag string) (int, error) {
needle := utf16.Encode([]rune(tag))
for at := start; at+len(needle) <= len(units); at++ {
if units[at] == '#' {
for at < len(units) && units[at] != '\n' {
at++
}
continue
}
if units[at] == '`' {
at++
for at < len(units) {
if units[at] == '`' {
if at+1 < len(units) && units[at+1] == '`' {
at += 2
continue
}
break
}
at++
}
continue
}
if units[at] != '[' {
continue
}
match := true
for j, want := range needle {
got := units[at+j]
if got >= 'A' && got <= 'Z' {
got += 'a' - 'A'
}
if got != want {
match = false
break
}
}
if match {
return at, nil
}
}
return 0, fmt.Errorf("缺少 %s,无法安全编辑", tag)
}
doc := &IndependentDropDocument{Rows: []IndependentDropRow{}, Lists: []IndependentDropList{}, Balance: []SkillNumber{}, Issues: []string{}, IsList: listOnly, BodyEnd: -1}
pending := []SkillNumber{}
listTokens := []SkillNumber{}
currentList := -1
found := false
flushList := func() error {
if currentList < 0 {
return nil
}
r := &doc.Rows[currentList]
mode := float64(1)
if !listOnly {
mode = r.Fields[16].Value
}
if mode == 2 {
if len(listTokens) != 1 {
return fmt.Errorf("外部列表引用需要一个列表 ID")
}
r.Items = append(r.Items, IndependentDropItem{Fields: listTokens})
return nil
}
if len(listTokens)%2 != 0 {
return fmt.Errorf("[list] 的物品 ID 与权重不成对")
}
for i := 0; i < len(listTokens); i += 2 {
r.Items = append(r.Items, IndependentDropItem{Fields: append([]SkillNumber(nil), listTokens[i:i+2]...)})
}
return nil
}
for _, e := range view.Elements {
if e.Kind == pvf.ScriptElementSection {
if e.Section == "independent drop" && len(e.SectionPath) == 1 && !listOnly {
if found {
return nil, fmt.Errorf("重复 [independent drop]")
}
found = true
end, err := closing(e.End, "[/independent drop]")
if err != nil {
return nil, err
}
doc.BodyEnd = end
}
if e.Section == "list" && ((listOnly && len(e.SectionPath) == 1) || (!listOnly && len(e.SectionPath) == 2 && e.SectionPath[0] == "independent drop")) {
if len(pending) > 0 {
return nil, fmt.Errorf("独立掉落记录不足 17 项")
}
if err := flushList(); err != nil {
return nil, err
}
if listOnly {
if found {
return nil, fmt.Errorf("重复 [list]")
}
found = true
doc.Rows = append(doc.Rows, IndependentDropRow{Fields: []SkillNumber{}, Items: []IndependentDropItem{}, ListStart: -1, ListEnd: -1})
}
if len(doc.Rows) == 0 {
return nil, fmt.Errorf("[list] 前没有独立掉落记录")
}
currentList = len(doc.Rows) - 1
listTokens = []SkillNumber{}
r := &doc.Rows[currentList]
if r.ListStart >= 0 {
return nil, fmt.Errorf("同一记录包含多个 [list]")
}
r.ListTagStart = e.Start
r.ListStart = e.End
end, err := closing(e.End, "[/list]")
if err != nil {
return nil, err
}
r.ListEnd = end
if listOnly {
doc.BodyEnd = end
}
} else if len(e.SectionPath) > 1 && e.SectionPath[0] == "independent drop" && len(doc.Rows) > 0 {
doc.Rows[len(doc.Rows)-1].HasExtensions = true
}
continue
}
if e.Kind != pvf.ScriptElementToken {
continue
}
isMain := !listOnly && len(e.SectionPath) == 1 && e.Section == "independent drop"
isList := e.Section == "list" && ((listOnly && len(e.SectionPath) == 1) || (!listOnly && len(e.SectionPath) == 2 && e.SectionPath[0] == "independent drop"))
isBalance := !listOnly && e.Section == "dungeon drop rate balance" && len(e.SectionPath) == 1
if !isMain && !isList && !isBalance {
continue
}
n, err := strconv.ParseFloat(e.Value, 64)
if err != nil || math.IsNaN(n) || math.IsInf(n, 0) || (e.TokenType != 0 && e.TokenType != 2) {
return nil, fmt.Errorf("[%s] 包含非数值字段", e.Section)
}
field := SkillNumber{Value: n, Start: e.Start, End: e.End, TokenType: e.TokenType}
if isBalance {
doc.Balance = append(doc.Balance, field)
continue
}
if isList {
if currentList < 0 {
return nil, fmt.Errorf("列表结构无效")
}
listTokens = append(listTokens, field)
continue
}
if currentList >= 0 {
if err := flushList(); err != nil {
return nil, err
}
currentList = -1
listTokens = nil
}
pending = append(pending, field)
if len(pending) == 17 {
doc.Rows = append(doc.Rows, IndependentDropRow{Fields: pending, Items: []IndependentDropItem{}, Start: pending[0].Start, ListStart: -1, ListEnd: -1})
pending = []SkillNumber{}
}
}
if err := flushList(); err != nil {
return nil, err
}
if !found {
return nil, fmt.Errorf("未找到可编辑的独立掉落 section")
}
if len(pending) > 0 {
return nil, fmt.Errorf("独立掉落记录不足 17 项(余 %d 项)", len(pending))
}
for i := range doc.Rows {
r := &doc.Rows[i]
r.End = doc.BodyEnd
if i+1 < len(doc.Rows) {
r.End = doc.Rows[i+1].Start
}
if listOnly {
continue
}
mode := r.Fields[16].Value
if mode == 0 && r.ListStart >= 0 {
return nil, fmt.Errorf("第 %d 条直接掉落记录包含列表", i+1)
}
if (mode == 1 || mode == 2) && r.ListStart < 0 {
return nil, fmt.Errorf("第 %d 条记录缺少 [list]", i+1)
}
if mode != 0 && mode != 1 && mode != 2 {
doc.Issues = append(doc.Issues, fmt.Sprintf("第 %d 条使用扩展模式 %g;保留原始模式与扩展条件,仅开放已确认字段", i+1, mode))
}
}
return doc, nil
}
func (s *FileGUIService) ReadIndependentDrop(fileIndex int32, text string) (*IndependentDropDocument, error) {
s.c.mu.Lock()
defer s.c.mu.Unlock()
a := s.c.archive
if a == nil {
return nil, ErrNoArchive
}
if err := validateAnnotationIndex(a, fileIndex); err != nil {
return nil, err
}
p := strings.ToLower(strings.ReplaceAll(a.Path(fileIndex), "\\", "/"))
listOnly := p != independentDropPath
if listOnly && !strings.HasPrefix(p, "etc/independentdrop/") {
return nil, fmt.Errorf("独立掉落 GUI 不支持此路径")
}
if len(text) > int(s.c.editableByteLimit()) {
return nil, fmt.Errorf("独立掉落文本过大")
}
doc, err := parseIndependentDrop(text, listOnly)
if err != nil {
return nil, err
}
doc.Version = a.ClientVersion()
if i, ok := a.Find("etc/independentdrop.lst"); ok {
lst, err := a.Text(i)
if err != nil {
doc.Issues = append(doc.Issues, "外部列表索引读取失败")
} else if pairs, err := pvf.ParseListText(lst); err != nil {
doc.Issues = append(doc.Issues, "外部列表索引解析失败")
} else {
for _, pair := range pairs {
p := path.Join("etc", strings.ReplaceAll(pair.Path, "\\", "/"))
idx, exists := a.Find(p)
if !exists {
idx = -1
}
doc.Lists = append(doc.Lists, IndependentDropList{ID: pair.ID, Path: p, FileIndex: idx})
}
}
}
// Resolve APCs through their list because not every client's semantic index
// registers an APC relation. Cache IDs and names once per document.
apcs := map[string]int32{}
if index, ok := a.FindList("aicharacter/aicharacter.lst"); ok {
if pairs, err := a.ListPairs(index); err == nil {
for _, pair := range pairs {
if _, index, ok := findListTargetInArchive(a, a.Path(index), pair.Path); ok {
apcs[pair.ID] = index
}
}
}
}
names := map[string]string{}
resolve := func(relation string, value float64) string {
id := strconv.FormatInt(int64(value), 10)
key := relation + ":" + id
if name, ok := names[key]; ok {
return name
}
name := ""
if s.c.annotationEngine != nil {
if ref, ok := s.c.resolveAnnotationReferenceLocked(relation, id); ok {
name = resolvePreviewText(a, ref.Name)
}
}
if name == "" && relation == "APC" {
if index, ok := apcs[id]; ok {
if metadata, err := a.ScriptMetadata(index); err == nil && metadata.HasName {
name = resolvePreviewText(a, markedName(metadata))
}
}
}
names[key] = name
return name
}
for i := range doc.Rows {
r := &doc.Rows[i]
if !listOnly {
relation := "怪物"
if r.Fields[0].Value == 1 {
relation = "APC"
}
r.MonsterName = resolve(relation, r.Fields[1].Value)
if r.Fields[16].Value == 0 {
r.ItemName = resolve("物品", r.Fields[2].Value)
}
}
for j := range r.Items {
if len(r.Items[j].Fields) == 2 {
r.Items[j].Name = resolve("物品", r.Items[j].Fields[0].Value)
}
}
}
return doc, nil
}