Files
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

116 lines
3.6 KiB
Go

package inventory
import (
"context"
"encoding/binary"
"errors"
"fmt"
"strings"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
)
var ErrLegacyPetMigrationUnavailable = errors.New("legacy pet inventory migration is unavailable")
// LegacyPetInventoryCandidate is a bridge-classified list-0 row. The owner
// revalidates the durable item id and target segment inside one character
// transaction before moving it.
type LegacyPetInventoryCandidate struct {
SourceKey string
ItemID int64
TargetFrom int16
TargetTo int16
}
type LegacyPetInventoryMigrationResult struct {
Migrated int
}
// LegacyPetMigrationOwner owns the one-time repair of pet artifacts and pet
// consumables that older builds placed in the main inventory.
type LegacyPetMigrationOwner struct {
assets dnfrepo.CharacterAssetUnitOfWork
}
func NewLegacyPetMigrationOwner(assets dnfrepo.CharacterAssetUnitOfWork) (*LegacyPetMigrationOwner, error) {
if assets == nil {
return nil, ErrLegacyPetMigrationUnavailable
}
return &LegacyPetMigrationOwner{assets: assets}, nil
}
func (o *LegacyPetMigrationOwner) Migrate(
ctx context.Context,
characterID string,
candidates []LegacyPetInventoryCandidate,
) (LegacyPetInventoryMigrationResult, error) {
if o == nil || o.assets == nil {
return LegacyPetInventoryMigrationResult{}, ErrLegacyPetMigrationUnavailable
}
characterID = strings.TrimSpace(characterID)
if characterID == "" || len(candidates) == 0 {
return LegacyPetInventoryMigrationResult{}, nil
}
for _, candidate := range candidates {
if !strings.HasPrefix(candidate.SourceKey, "0:") || candidate.ItemID <= 0 ||
!validLegacyPetTargetRange(candidate.TargetFrom, candidate.TargetTo) {
return LegacyPetInventoryMigrationResult{}, fmt.Errorf("%w: invalid candidate %+v", ErrLegacyPetMigrationUnavailable, candidate)
}
}
result := LegacyPetInventoryMigrationResult{}
err := dnfrepo.WithinCharacterAssetsScoped(ctx, o.assets, characterID, func(
_ dnfrepo.CharacterRepository,
inventoryRepo dnfrepo.InventoryRepository,
_ dnfrepo.EquipmentRepository,
) error {
inventory, found, err := inventoryRepo.Load(ctx, characterID)
if err != nil || !found {
return err
}
inventory = dnfrepo.CloneInventory(inventory)
if inventory.Slots == nil {
return nil
}
for _, candidate := range candidates {
stack, found := inventory.Slots[candidate.SourceKey]
if !found || stack.ItemID != candidate.ItemID || stack.Count <= 0 {
continue
}
targetSlot, found := firstFreeLegacyPetSlot(inventory.Slots, candidate.TargetFrom, candidate.TargetTo)
if !found {
return fmt.Errorf("%w: pet slots %d..%d full", ErrLegacyPetMigrationUnavailable, candidate.TargetFrom, candidate.TargetTo)
}
if len(stack.RawEntry) >= 2 {
stack.RawEntry = append([]byte(nil), stack.RawEntry...)
binary.LittleEndian.PutUint16(stack.RawEntry[:2], uint16(targetSlot))
}
inventory.Slots[slotKey(listTypePet, targetSlot)] = stack
delete(inventory.Slots, candidate.SourceKey)
result.Migrated++
}
if result.Migrated == 0 {
return nil
}
return dnfrepo.SaveInventoryFields(ctx, inventoryRepo, inventory, dnfrepo.InventoryFieldSlots)
})
if err != nil {
return LegacyPetInventoryMigrationResult{}, err
}
return result, nil
}
func validLegacyPetTargetRange(from, to int16) bool {
return (from == 140 && to == 188) || (from == 189 && to == 238)
}
func firstFreeLegacyPetSlot(slots map[string]dnfrepo.ItemStack, from, to int16) (int16, bool) {
for slot := from; slot <= to; slot++ {
stack, occupied := slots[slotKey(listTypePet, slot)]
if !occupied || stack.ItemID <= 0 || stack.Count <= 0 {
return slot, true
}
}
return 0, false
}