DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
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)
|
||||
}
|
||||
Reference in new issue
Block a user