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

+191
View File
@@ -0,0 +1,191 @@
package script
import (
"fmt"
"math"
"sort"
"strconv"
"strings"
"unicode/utf8"
"github.com/dop251/goja"
"pvfine/internal/pvf"
)
// editUpgrade evaluates every callback before staging one complete edit.
func (b *gojaBindings) editUpgrade(file *FileHandle, call goja.FunctionCall) (goja.Value, error) {
options, err := objectArgument(call.Argument(0))
if err != nil {
return nil, err
}
if options == nil {
return nil, fmt.Errorf("editUpgrade 需要 options")
}
for _, key := range options.Keys() {
switch key {
case "equipment", "fromLevel", "toLevel", "successRate", "costCount":
default:
return nil, fmt.Errorf("未知强化选项 %s", key)
}
}
path := strings.ToLower(strings.ReplaceAll(file.Path(), "\\", "/"))
kind := "normal"
if path == "etc/amplifyupgrade.etc" {
kind = "amplify"
} else if path != "etc/upgrade.etc" {
return nil, fmt.Errorf("editUpgrade 仅支持强化/增幅文件")
}
equipment := "weapon"
if !isMissingValue(options.Get("equipment")) {
equipment, err = requiredString(options.Get("equipment"), "装备类型")
if err != nil {
return nil, err
}
}
if equipment != "weapon" && equipment != "armor" {
return nil, fmt.Errorf("equipment 必须是 weapon 或 armor")
}
text, err := file.Text()
if err != nil {
return nil, err
}
tokens, typed, err := pvf.SelectUpgradeTableTokens(text, kind)
if err != nil {
return nil, err
}
layout, err := pvf.DetectUpgradeTableLayout(tokens, typed)
if err != nil {
return nil, err
}
if equipment == "armor" && !layout.HasT2 {
return nil, fmt.Errorf("旧版表格没有独立防具数据")
}
rateColumn := layout.RateColumn
if equipment == "armor" {
rateColumn = layout.ColumnsT2[0]
}
countColumn := layout.CountColumn
width := layout.Width
for _, token := range tokens {
if token.TokenType != 0 && token.TokenType != 2 {
return nil, fmt.Errorf("表格包含非数值数据")
}
}
maximum := len(tokens) / width
if !typed && maximum > 31 {
maximum = 31
}
from, err := optionInteger(options, "fromLevel", 1)
if err != nil {
return nil, err
}
to, err := optionInteger(options, "toLevel", int64(maximum))
if err != nil {
return nil, err
}
if from < 1 || to < from || to > int64(maximum) {
return nil, fmt.Errorf("等级范围必须位于 1~%d", maximum)
}
type update struct {
name string
column int
value goja.Value
}
updates := []update{}
for _, item := range []update{{"successRate", rateColumn, options.Get("successRate")}, {"costCount", countColumn, options.Get("costCount")}} {
if !isMissingValue(item.value) {
updates = append(updates, item)
}
}
if len(updates) == 0 {
return nil, fmt.Errorf("请提供 successRate 或 costCount")
}
type patch struct {
start, end int
value string
}
patches := []patch{}
offsets := upgradeByteOffsets(text)
for level := int(from); level <= int(to); level++ {
if err := b.host.checkContext(); err != nil {
return nil, err
}
for _, item := range updates {
token := tokens[(level-1)*width+item.column]
current, err := strconv.ParseFloat(token.Value, 64)
if err != nil || token.TokenType != 0 {
return nil, fmt.Errorf("第 %d 级 %s 必须是整数", level, item.name)
}
if item.name == "successRate" {
if current < 0 || current > 100000 {
return nil, fmt.Errorf("原始失败概率超出范围")
}
current = 100 - current/1000
}
value := item.value
if fn, ok := goja.AssertFunction(value); ok {
value, err = fn(goja.Undefined(), b.vm.ToValue(level), b.vm.ToValue(current))
if err != nil {
return nil, err
}
}
switch value.Export().(type) {
case int64, float64, int, int32, float32:
default:
return nil, fmt.Errorf("第 %d 级 %s 计算结果必须是数字", level, item.name)
}
number := value.ToFloat()
if math.IsNaN(number) || math.IsInf(number, 0) || number < 0 {
return nil, fmt.Errorf("第 %d 级 %s 计算结果无效", level, item.name)
}
if item.name == "successRate" {
if number > 100 {
return nil, fmt.Errorf("成功率必须位于 0~100")
}
number = math.Round((100 - number) * 1000)
} else if number > math.MaxInt32 || math.Trunc(number) != number {
return nil, fmt.Errorf("消耗数量必须是非负 32 位整数")
}
if token.Start < 0 || token.End >= len(offsets) {
return nil, fmt.Errorf("表格位置无效")
}
patches = append(patches, patch{offsets[token.Start], offsets[token.End], strconv.FormatInt(int64(number), 10)})
}
}
sort.Slice(patches, func(i, j int) bool { return patches[i].start > patches[j].start })
updated := text
for _, patch := range patches {
updated = updated[:patch.start] + patch.value + updated[patch.end:]
}
latest, err := file.Text()
if err != nil {
return nil, err
}
if latest != text {
return nil, fmt.Errorf("计算期间文件发生变化,已拒绝覆盖")
}
if updated != text {
if err := file.SetText(updated); err != nil {
return nil, err
}
}
return b.vm.ToValue(to - from + 1), nil
}
// Translate normalized UTF-16 token offsets to original UTF-8 byte offsets,
// retaining CRLF and lone CR line endings in source patches.
func upgradeByteOffsets(text string) []int {
offsets := []int{0}
for i := 0; i < len(text); {
r, size := utf8.DecodeRuneInString(text[i:])
if r == '\r' && i+size < len(text) && text[i+size] == '\n' {
size++
}
if r > 0xffff {
offsets = append(offsets, i)
}
i += size
offsets = append(offsets, i)
}
return offsets
}