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,180 @@
package itemgrade
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"time"
"longheng.io/server/internal/modules/dnf/itemquality"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
)
var (
ErrOwnerUnavailable = errors.New("item grade owner unavailable")
ErrCharacterRequired = errors.New("selected character id required")
ErrTargetMissing = errors.New("item grade target missing")
ErrMaterialMissing = errors.New("item grade material missing")
)
type ItemDefinition struct {
StackableType string
}
type ItemCatalog interface {
ResolveItem(uint32) (ItemDefinition, error)
}
type SeedGenerator func() (uint32, error)
type Command struct {
SelectedCharacterID uint16
TargetSlot int16
TargetItemID int32
MaterialSlot int16
UpdatedAt time.Time
}
type Result struct {
CharacterID string
TargetSlot int16
TargetItemID int32
TargetStack dnfrepo.ItemStack
MaterialSlot int16
MaterialItemID int64
MaterialRemaining int64
MaterialRemoved bool
NewSeed uint32
GoldKaleido bool
}
// Owner owns kaleido validation, quality selection, material consumption, and
// the durable inventory write.
type Owner struct {
inventory dnfrepo.InventoryRepository
characterItems dnfrepo.CharacterItemUnitOfWork
accountItems dnfrepo.AccountCharacterItemUnitOfWork
catalog ItemCatalog
newSeed SeedGenerator
}
func NewOwner(repos dnfrepo.Group, catalog ItemCatalog, newSeed SeedGenerator) (*Owner, error) {
if repos.Inventory == nil {
return nil, ErrOwnerUnavailable
}
if newSeed == nil {
newSeed = itemquality.NewRandomSeed
}
return &Owner{
inventory: repos.Inventory, characterItems: repos.CharacterItems,
accountItems: repos.AccountItems, catalog: catalog, newSeed: newSeed,
}, nil
}
func (o *Owner) Adjust(ctx context.Context, cmd Command) (Result, error) {
if o == nil || o.inventory == nil || o.newSeed == nil {
return Result{}, ErrOwnerUnavailable
}
if cmd.SelectedCharacterID == 0 {
return Result{}, ErrCharacterRequired
}
characterID := strconv.FormatUint(uint64(cmd.SelectedCharacterID), 10)
inventory, found, err := o.inventory.Load(ctx, characterID)
if err != nil {
return Result{}, err
}
if !found || strings.TrimSpace(inventory.CharacterID) != characterID {
return Result{}, ErrOwnerUnavailable
}
inventory = dnfrepo.CloneInventory(inventory)
targetKey := inventoryKey(cmd.TargetSlot)
target, found := inventory.Slots[targetKey]
if !found || target.ItemID != int64(cmd.TargetItemID) || target.ItemID <= 0 {
return Result{}, fmt.Errorf("%w: slot=%d item=%d", ErrTargetMissing, cmd.TargetSlot, cmd.TargetItemID)
}
materialKey := inventoryKey(cmd.MaterialSlot)
material, found := inventory.Slots[materialKey]
if !found || material.ItemID <= 0 || material.Count <= 0 {
return Result{}, fmt.Errorf("%w: slot=%d", ErrMaterialMissing, cmd.MaterialSlot)
}
// The current client sends the occupied box slot as the op81 material.
// Ordinary adjustment boxes in the active PVF are legacy [etc] items, so
// their stackable type is not a safe allow-list. Preserve the working op81
// contract: every non-empty client-selected material slot is an ordinary
// box unless the PVF positively identifies it as a golden kaleido box.
goldKaleido := o.isGoldKaleido(material.ItemID)
seed := itemquality.TopSeed
if !goldKaleido {
seed, err = o.newSeed()
if err != nil {
return Result{}, err
}
}
if err := itemquality.Apply(&target, seed); err != nil {
return Result{}, err
}
// A grade-adjust target is necessarily equipment. Normalize legacy rows so
// every later inventory projection keeps using the quality seed instead of
// falling back to the ordinary stack count.
target.Extra["item_kind"] = "equipment"
now := cmd.UpdatedAt
if now.IsZero() {
now = time.Now().UTC()
} else {
now = now.UTC()
}
target.Extra["last_grade_adjust"] = now.Format(time.RFC3339)
if goldKaleido {
target.Extra["grade_adjust_type"] = "gold_kaleido"
} else {
target.Extra["grade_adjust_type"] = "standard_kaleido"
}
inventory.Slots[targetKey] = target
material.Count--
materialRemoved := material.Count == 0
if materialRemoved {
delete(inventory.Slots, materialKey)
} else {
inventory.Slots[materialKey] = material
}
inventory.UpdatedAt = now
if err := dnfrepo.SaveInventoryFields(ctx, o.inventory, inventory, dnfrepo.InventoryFieldSlots); err != nil {
return Result{}, err
}
return Result{
CharacterID: characterID,
TargetSlot: cmd.TargetSlot,
TargetItemID: cmd.TargetItemID,
TargetStack: target,
MaterialSlot: cmd.MaterialSlot,
MaterialItemID: material.ItemID,
MaterialRemaining: material.Count,
MaterialRemoved: materialRemoved,
NewSeed: seed,
GoldKaleido: goldKaleido,
}, nil
}
func (o *Owner) isGoldKaleido(itemID int64) bool {
if o.catalog == nil || itemID <= 0 {
return false
}
definition, err := o.catalog.ResolveItem(uint32(itemID))
if err != nil {
return false
}
normalized := strings.ToLower(strings.ReplaceAll(definition.StackableType, "`", ""))
// Restore the known-good rule from internal.zip. The active PVF contains
// localized/special golden variants, so the proven gold marker selects the
// top-quality seed while every other box remains an ordinary random roll.
return strings.Contains(normalized, "gold")
}
func inventoryKey(slot int16) string {
return fmt.Sprintf("%d:%d", dnfrepo.MainInventoryListType, slot)
}