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,286 @@
|
||||
package dungeon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
)
|
||||
|
||||
const tutorialRewardMarkerValue = int64(1)
|
||||
|
||||
var (
|
||||
ErrTutorialRewardInvalid = errors.New("dungeon tutorial reward is invalid")
|
||||
ErrTutorialInventoryFull = errors.New("dungeon tutorial reward inventory is full")
|
||||
ErrTutorialAssetsMissing = errors.New("dungeon tutorial reward assets are unavailable")
|
||||
)
|
||||
|
||||
type TutorialItemReward struct {
|
||||
Progress uint32
|
||||
ItemID uint32
|
||||
Count uint32
|
||||
Consumable bool
|
||||
StackLimit int64
|
||||
SlotStart int16
|
||||
SlotEnd int16
|
||||
ExpireAt time.Time
|
||||
PVFPath string
|
||||
StackableType string
|
||||
}
|
||||
|
||||
type TutorialRewardCommand struct {
|
||||
CharacterID string
|
||||
Progress uint32
|
||||
Rewards []TutorialItemReward
|
||||
UpdatedAt time.Time
|
||||
Project StackProjector
|
||||
}
|
||||
|
||||
type TutorialRewardRow struct {
|
||||
Slot uint16
|
||||
ItemID uint32
|
||||
Count uint32
|
||||
}
|
||||
|
||||
type TutorialRewardResult struct {
|
||||
Granted bool
|
||||
Rows []TutorialRewardRow
|
||||
}
|
||||
|
||||
func (o *Owner) GrantTutorialReward(ctx context.Context, cmd TutorialRewardCommand) (TutorialRewardResult, error) {
|
||||
if o == nil || o.assets == nil || strings.TrimSpace(cmd.CharacterID) == "" ||
|
||||
cmd.Progress == 0 || len(cmd.Rewards) == 0 {
|
||||
return TutorialRewardResult{}, ErrTutorialAssetsMissing
|
||||
}
|
||||
ctx = contextOrBackground(ctx)
|
||||
now := updatedAtOrNow(cmd.UpdatedAt)
|
||||
marker := TutorialRewardMarker(cmd.Progress)
|
||||
|
||||
var result TutorialRewardResult
|
||||
err := o.assets.WithinCharacterAssets(ctx, cmd.CharacterID, func(
|
||||
characters dnfrepo.CharacterRepository,
|
||||
inventories dnfrepo.InventoryRepository,
|
||||
_ dnfrepo.EquipmentRepository,
|
||||
) error {
|
||||
character, found, err := characters.Load(ctx, cmd.CharacterID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found {
|
||||
return ErrTutorialAssetsMissing
|
||||
}
|
||||
character = dnfrepo.CloneCharacter(character)
|
||||
if character.Stats == nil {
|
||||
character.Stats = make(map[string]int64)
|
||||
}
|
||||
if character.Stats[marker] == tutorialRewardMarkerValue {
|
||||
return nil
|
||||
}
|
||||
|
||||
inventory, found, err := inventories.Load(ctx, cmd.CharacterID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found {
|
||||
inventory = dnfrepo.InventoryRecord{CharacterID: cmd.CharacterID}
|
||||
}
|
||||
inventory = dnfrepo.CloneInventory(inventory)
|
||||
if inventory.Slots == nil {
|
||||
inventory.Slots = make(map[string]dnfrepo.ItemStack)
|
||||
}
|
||||
rows := make([]TutorialRewardRow, 0, len(cmd.Rewards))
|
||||
for _, reward := range cmd.Rewards {
|
||||
if reward.Progress != cmd.Progress {
|
||||
return fmt.Errorf(
|
||||
"%w: requested_progress=%d row_progress=%d",
|
||||
ErrTutorialRewardInvalid,
|
||||
cmd.Progress,
|
||||
reward.Progress,
|
||||
)
|
||||
}
|
||||
slot, err := PlaceTutorialReward(&inventory, cmd.Progress, reward, cmd.Project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows = append(rows, TutorialRewardRow{
|
||||
Slot: slot, ItemID: reward.ItemID, Count: reward.Count,
|
||||
})
|
||||
}
|
||||
character.Stats[marker] = tutorialRewardMarkerValue
|
||||
character.UpdatedAt = now
|
||||
inventory.UpdatedAt = now
|
||||
if err := dnfrepo.SaveCharacterFields(ctx, characters, character, dnfrepo.CharacterFieldStats); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := dnfrepo.SaveInventoryFields(ctx, inventories, inventory, dnfrepo.InventoryFieldSlots); err != nil {
|
||||
return err
|
||||
}
|
||||
result.Granted = true
|
||||
result.Rows = rows
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return TutorialRewardResult{}, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func TutorialRewardMarker(progress uint32) string {
|
||||
return "tutorial_reward_progress_" + strconv.FormatUint(uint64(progress), 10)
|
||||
}
|
||||
|
||||
func PlaceTutorialReward(
|
||||
record *dnfrepo.InventoryRecord,
|
||||
progress uint32,
|
||||
reward TutorialItemReward,
|
||||
project StackProjector,
|
||||
) (uint16, error) {
|
||||
if record == nil || reward.Progress != progress || reward.ItemID == 0 || reward.Count == 0 ||
|
||||
!reward.Consumable || reward.SlotStart < 0 || reward.SlotEnd < reward.SlotStart {
|
||||
return 0, ErrTutorialRewardInvalid
|
||||
}
|
||||
if reward.StackLimit > 0 && uint64(reward.Count) > uint64(reward.StackLimit) {
|
||||
return 0, ErrTutorialRewardInvalid
|
||||
}
|
||||
if !reward.ExpireAt.IsZero() && project == nil {
|
||||
return 0, ErrStackProjectorRequired
|
||||
}
|
||||
if record.Slots == nil {
|
||||
record.Slots = make(map[string]dnfrepo.ItemStack)
|
||||
}
|
||||
// PVF [stack limit] 的"每容器总量"口径本轮只接在邮件/商城/NPC 商店三条链路上;
|
||||
// 教学奖励这条落点先保持旧行为,等它单独验收时再接。
|
||||
newRowStackLimit := int64(0)
|
||||
space := int64(math.MaxInt32)
|
||||
canOpenRow := func() bool {
|
||||
return space > 0 && dnfrepo.CanOpenContainerItemRow(
|
||||
record.Slots,
|
||||
dnfrepo.MainInventoryListType,
|
||||
int64(reward.ItemID),
|
||||
newRowStackLimit,
|
||||
)
|
||||
}
|
||||
if slot, ok, err := stackTutorialRewardInRange(record, reward, project, 3, 8, space); err != nil {
|
||||
return 0, err
|
||||
} else if ok {
|
||||
return uint16(slot), nil
|
||||
}
|
||||
// 【合并说明 2026-09-18,来自服务端仓库 fefab3b】他们给 firstEmptyMainSlot 加了
|
||||
// listType 参数(教程奖励固定进主背包,这里显式传 MainInventoryListType);
|
||||
// 我们这一侧的 canOpenRow()(PVF 开行闸门)原样保留。
|
||||
// 末位 nil:教程奖励不在副本拾取那条链路上,没有弃置物格位保留。
|
||||
if slot, ok := firstEmptyMainSlot(record, dnfrepo.MainInventoryListType, 3, 8, nil); ok && canOpenRow() {
|
||||
return insertTutorialReward(record, slot, progress, reward, project)
|
||||
}
|
||||
if slot, ok, err := stackTutorialRewardInRange(
|
||||
record,
|
||||
reward,
|
||||
project,
|
||||
reward.SlotStart,
|
||||
reward.SlotEnd,
|
||||
space,
|
||||
); err != nil {
|
||||
return 0, err
|
||||
} else if ok {
|
||||
return uint16(slot), nil
|
||||
}
|
||||
// 同上:回退区间也显式用主背包列表,并保留我们的开行闸门。
|
||||
if slot, ok := firstEmptyMainSlot(record, dnfrepo.MainInventoryListType, reward.SlotStart, reward.SlotEnd, nil); ok && canOpenRow() {
|
||||
return insertTutorialReward(record, slot, progress, reward, project)
|
||||
}
|
||||
return 0, fmt.Errorf(
|
||||
"%w: progress=%d item=%d quick=3..8 fallback=%d..%d",
|
||||
ErrTutorialInventoryFull,
|
||||
progress,
|
||||
reward.ItemID,
|
||||
reward.SlotStart,
|
||||
reward.SlotEnd,
|
||||
)
|
||||
}
|
||||
|
||||
func stackTutorialRewardInRange(
|
||||
record *dnfrepo.InventoryRecord,
|
||||
reward TutorialItemReward,
|
||||
project StackProjector,
|
||||
start int16,
|
||||
end int16,
|
||||
space int64,
|
||||
) (int16, bool, error) {
|
||||
for slot := start; slot <= end; slot++ {
|
||||
key := mainSlotKey(uint16(slot))
|
||||
stack, ok := record.Slots[key]
|
||||
if !ok || !canStackTutorialReward(stack, reward) {
|
||||
continue
|
||||
}
|
||||
if int64(reward.Count) > space {
|
||||
continue
|
||||
}
|
||||
if !reward.ExpireAt.IsZero() {
|
||||
var err error
|
||||
stack, err = project(stack, reward.ExpireAt)
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
}
|
||||
stack.Count += int64(reward.Count)
|
||||
updateDungeonRewardRawAmount(&stack)
|
||||
record.Slots[key] = stack
|
||||
return slot, true, nil
|
||||
}
|
||||
return 0, false, nil
|
||||
}
|
||||
|
||||
func canStackTutorialReward(stack dnfrepo.ItemStack, reward TutorialItemReward) bool {
|
||||
if stack.ItemID != int64(reward.ItemID) || stack.Bind || stack.Count < 0 {
|
||||
return false
|
||||
}
|
||||
if reward.ExpireAt.IsZero() && !stack.ExpireAt.IsZero() {
|
||||
return false
|
||||
}
|
||||
amount := int64(reward.Count)
|
||||
if stack.Count > math.MaxInt64-amount {
|
||||
return false
|
||||
}
|
||||
return reward.StackLimit <= 0 || stack.Count <= reward.StackLimit-amount
|
||||
}
|
||||
|
||||
func insertTutorialReward(
|
||||
record *dnfrepo.InventoryRecord,
|
||||
slot int16,
|
||||
progress uint32,
|
||||
reward TutorialItemReward,
|
||||
project StackProjector,
|
||||
) (uint16, error) {
|
||||
if slot <= 0 {
|
||||
return 0, ErrTutorialRewardInvalid
|
||||
}
|
||||
extra := map[string]string{
|
||||
"source": "tutorial_pvf_reward",
|
||||
"reward_progress": strconv.FormatUint(uint64(progress), 10),
|
||||
"item_kind": "stackable",
|
||||
"pvf_path": reward.PVFPath,
|
||||
"stackable_type": reward.StackableType,
|
||||
}
|
||||
if reward.StackLimit > 0 {
|
||||
extra["stack_limit"] = strconv.FormatInt(reward.StackLimit, 10)
|
||||
}
|
||||
stack := dnfrepo.ItemStack{
|
||||
ItemID: int64(reward.ItemID),
|
||||
Count: int64(reward.Count),
|
||||
Extra: extra,
|
||||
}
|
||||
if !reward.ExpireAt.IsZero() {
|
||||
var err error
|
||||
stack, err = project(stack, reward.ExpireAt)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
record.Slots[mainSlotKey(uint16(slot))] = stack
|
||||
return uint16(slot), nil
|
||||
}
|
||||
Reference in new issue
Block a user