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

@@ -0,0 +1,125 @@
package darkknightcombo
import (
"context"
"encoding/base64"
"fmt"
"sort"
"strings"
"time"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
)
const (
settingsFormatKey = "format"
settingsFormatValue = "dark-knight-combo-pages-v1"
settingsPagePrefix = "page_"
)
func SettingsScope(characterID string) string {
return "character:" + strings.TrimSpace(characterID) + ":dark_knight_combo"
}
func LoadPages(ctx context.Context, repo dnfrepo.SettingsRepository, characterID string) ([]Page, bool, error) {
characterID = strings.TrimSpace(characterID)
if repo == nil || characterID == "" {
return nil, false, fmt.Errorf("dark knight combo settings repository is unavailable")
}
record, found, err := repo.Load(ctx, SettingsScope(characterID))
if err != nil || !found {
return nil, found, err
}
if record.Values[settingsFormatKey] != settingsFormatValue {
return nil, false, fmt.Errorf("unsupported dark knight combo settings format")
}
pages := make([]Page, 0, MaximumPageCount)
for pageIndex := 0; pageIndex < MaximumPageCount; pageIndex++ {
encoded := strings.TrimSpace(record.Values[fmt.Sprintf("%s%d", settingsPagePrefix, pageIndex)])
if encoded == "" {
continue
}
block, decodeErr := base64.StdEncoding.Strict().DecodeString(encoded)
if decodeErr != nil {
return nil, false, fmt.Errorf("decode dark knight combo page %d: %w", pageIndex, decodeErr)
}
page, parseErr := ParsePageBlock(block)
if parseErr != nil {
return nil, false, fmt.Errorf("decode dark knight combo page %d: %w", pageIndex, parseErr)
}
if int(page.Index) != pageIndex {
return nil, false, fmt.Errorf("decode dark knight combo page %d: page index=%d", pageIndex, page.Index)
}
pages = append(pages, page)
}
if len(pages) == 0 {
return nil, false, nil
}
return pages, true, nil
}
func SavePages(ctx context.Context, repo dnfrepo.SettingsRepository, characterID string, pages []Page, updatedAt time.Time) error {
record, err := BuildSettingsRecord(characterID, pages, updatedAt)
if err != nil {
return err
}
if repo == nil {
return ErrPageBlockInvalid
}
return repo.Save(ctx, record)
}
// BuildSettingsRecord encodes both independent combo pages without writing
// them. Character creation uses this builder so the combo state is committed
// in the same aggregate transaction as the new character and its skill state.
func BuildSettingsRecord(characterID string, pages []Page, updatedAt time.Time) (dnfrepo.SettingsRecord, error) {
characterID = strings.TrimSpace(characterID)
if characterID == "" || len(pages) == 0 || len(pages) > MaximumPageCount {
return dnfrepo.SettingsRecord{}, ErrPageBlockInvalid
}
values := map[string]string{settingsFormatKey: settingsFormatValue}
sorted := append([]Page(nil), pages...)
sort.Slice(sorted, func(left, right int) bool { return sorted[left].Index < sorted[right].Index })
for _, page := range sorted {
block, err := EncodePageBlock(page)
if err != nil {
return dnfrepo.SettingsRecord{}, err
}
key := fmt.Sprintf("%s%d", settingsPagePrefix, page.Index)
if _, exists := values[key]; exists {
return dnfrepo.SettingsRecord{}, ErrPageBlockInvalid
}
values[key] = base64.StdEncoding.EncodeToString(block)
}
return dnfrepo.SettingsRecord{
Scope: SettingsScope(characterID),
Values: values,
UpdatedAt: updatedAt.UTC(),
}, nil
}
func SavePage(ctx context.Context, repo dnfrepo.SettingsRepository, characterID string, page Page, updatedAt time.Time) error {
page, err := NormalizePage(page)
if err != nil {
return err
}
pages, found, err := LoadPages(ctx, repo, characterID)
if err != nil {
return err
}
if !found {
pages = nil
}
replaced := false
for index := range pages {
if pages[index].Index == page.Index {
pages[index] = page
replaced = true
break
}
}
if !replaced {
pages = append(pages, page)
}
return SavePages(ctx, repo, characterID, pages, updatedAt)
}