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,301 @@
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/alignedcmd"
|
||||
dnfcargo "longheng.io/server/internal/modules/dnf/cargo"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
)
|
||||
|
||||
// 扩容券(物品栏 / 金库 / 帐号金库)的服务端接线。
|
||||
//
|
||||
// 现场 2026-10-03(服主报,角色 测试x = char 11):
|
||||
//
|
||||
// 490701690 物品栏扩展券 [action type] `[upgrade inventory]` 背包槽 66
|
||||
// 490701691 金库扩展券 (1阶段) [action type] `[upgrade cargo]` 背包槽 68
|
||||
// 490701692 帐号金库升级工具. [action type] `[upgrade account cargo]` 背包槽 67
|
||||
//
|
||||
// 三件"使用后没效果"。加密包日志(conn game-000124,2026-10-03)证明客户端**有发包**:
|
||||
//
|
||||
// 09:56:57 RECV type=430 ENUM_CMDPACKET_UPGRADE_INVENTORY body=`4200` ×11
|
||||
// 09:58:42 RECV type=408 ENUM_CMDPACKET_UPGRADE_CARGO body=`4400`
|
||||
// 09:58:44 RECV type=515 ENUM_CMDPACKET_USE_STACKABLE_ACTION
|
||||
// body=`4300000000000092…`(槽 0x43=67 + action index 0x92=146)
|
||||
// 服务端当场的真实回包是 op515 错误 17:
|
||||
// "use stackable action index 146 unsupported"
|
||||
//
|
||||
// 前两个 opcode 在服务端**完全没有路由**(不在 aligned 表里、没有 handler,
|
||||
// DispatchUpper 对未注册 opcode 直接 return false ⇒ 静默),第三个被 action 白名单拒绝。
|
||||
//
|
||||
// 官方服务端(同目录 df_game_r(1),带 .symtab)给出了三种券的完整业务判据,
|
||||
// 见 _investigation_顶号_official/upgrade_dis.txt:
|
||||
//
|
||||
// Dispatcher_UpgradeInventory::dispatch_sig 读 PacketBuf::get_short 槽位 →
|
||||
// 校验槽上物品数量>0 → 校验物品(官方硬编码 2660298/8251 两个 id)→
|
||||
// get_inventory_capacity()>15 则拒绝(0x13) → delete_item(1,slot,1,reason3) →
|
||||
// 发 class0/op14 空行清槽 → capacity+=8 → op66{u16 11,u16 capacity}
|
||||
// Dispatcher_UpgradeCargo::dispatch_sig 同样读槽位 → 数量>0 →
|
||||
// verifyStackableItemActionPlace(StackableAction::Type 0x2b, itemID)(**PVF 判定**)→
|
||||
// CCargo::isUpgradable(capacity+16) → delete_item → op14 空行 →
|
||||
// CCargo::UpgradeCargo(capacity+16) → op66{u16 1,u16 capacity}
|
||||
//
|
||||
// 我们**不照抄官方的硬编码物品 id**:那是官服自己的白名单,本服的道具是自制的
|
||||
// (490701690/691/692),所以判据取 PVF 自己的 `[action type]` —— 与官方金库那条
|
||||
// `verifyStackableItemActionPlace` 同一个思路,而且覆盖全库 100 件同类道具
|
||||
// (40 / 47 / 13 件),换一件券也照样能用。
|
||||
//
|
||||
// 容量推进口径(三处都与既有代码同一张表):
|
||||
//
|
||||
// 物品栏 main_list_param16 0..24 步长 8(repository.currentMainInventoryExpansion)
|
||||
// —— 券自己的说明:"物品栏的空间增加8格。物品栏最多可以增加到56格。"(32+24)
|
||||
// 金库 personal_cargo_list_param16 8..200 步长 16(currentPersonalCargoSlotCount)
|
||||
// 帐号金库 account_cargo_level 走 dnfcargo.NextAccountCargoTier 的 16 档阶梯
|
||||
//
|
||||
// 帐号金库这一档**不收**金币/无色小晶块:券本身就是代价(金库窗口那条 op306 才收费)。
|
||||
|
||||
const (
|
||||
// accountCargoUpgradeActionIndex 是 `[action type] [upgrade account cargo]`
|
||||
// 在 op515 里的 action index。现场实测 0x92=146(见文件头日志),与
|
||||
// [rent point]=105、[add fatigue]=58、任务完成券=167 同一张编号表。
|
||||
accountCargoUpgradeActionIndex uint32 = 146
|
||||
|
||||
// containerUpgradeMainStep / containerUpgradeMaxMainExpansion 见文件头。
|
||||
containerUpgradeMainStep = 8
|
||||
containerUpgradeMaxMainExpansion = 24
|
||||
// containerUpgradeCargoStep / containerUpgradeMaxCargoSlots 见文件头。
|
||||
containerUpgradeCargoStep = 16
|
||||
containerUpgradeMaxCargoSlots = 200
|
||||
)
|
||||
|
||||
var (
|
||||
ErrContainerUpgradeContract = errors.New("inventory container-upgrade contract is invalid")
|
||||
ErrContainerUpgradeItemMismatch = errors.New("container-upgrade voucher's PVF action type does not match the requested container")
|
||||
ErrContainerUpgradeMaxStage = errors.New("container is already at its maximum upgrade stage")
|
||||
ErrContainerUpgradeStateMissing = errors.New("character container state is missing")
|
||||
ErrContainerUpgradeAccountMissing = errors.New("account cargo has not been created")
|
||||
ErrContainerUpgradeAccountBroken = errors.New("account cargo metadata is invalid")
|
||||
)
|
||||
|
||||
// UpgradeContainerFromVoucher 用一张扩容券推进一个容器档位。
|
||||
//
|
||||
// cmd.SourceSlotIndex 是客户端声明的券槽位(op430/op408 的 i16 body,或 op515 的槽位);
|
||||
// 券的身份一律从**持久行 + 运行时 PVF** 反查:请求体里没有 item id,客户端说什么不算数。
|
||||
// wantKind 由调用方按 opcode(或 action index)给出,PVF 解析出的 kind 必须与它一致,
|
||||
// 所以"拿金库券去点物品栏"这类错位组合会被当场拒掉。
|
||||
func (o *Owner) UpgradeContainerFromVoucher(
|
||||
ctx context.Context,
|
||||
cmd Command,
|
||||
resolver alignedcmd.StackableUseResolver,
|
||||
wantKind alignedcmd.ContainerUpgradeKind,
|
||||
now time.Time,
|
||||
) (UseStackableResult, error) {
|
||||
if o == nil || o.settlements == nil || o.settings == nil || resolver == nil {
|
||||
return UseStackableResult{}, ErrContainerUpgradeContract
|
||||
}
|
||||
if cmd.SelectedCharacterID == 0 || cmd.SourceListType != listTypeMain || cmd.SourceSlotIndex < 0 {
|
||||
return UseStackableResult{}, ErrContainerUpgradeContract
|
||||
}
|
||||
switch wantKind {
|
||||
case alignedcmd.ContainerUpgradeInventory, alignedcmd.ContainerUpgradeCargo, alignedcmd.ContainerUpgradeAccountCargo:
|
||||
default:
|
||||
return UseStackableResult{}, ErrContainerUpgradeContract
|
||||
}
|
||||
if now.IsZero() {
|
||||
now = time.Now().UTC()
|
||||
} else {
|
||||
now = now.UTC()
|
||||
}
|
||||
characterID := strconv.FormatUint(uint64(cmd.SelectedCharacterID), 10)
|
||||
var result UseStackableResult
|
||||
err := o.settlements.WithinCharacterSettlement(ctx, characterID, func(tx dnfrepo.Group) error {
|
||||
if tx.Inventory == nil || tx.Character == nil {
|
||||
return ErrContainerUpgradeContract
|
||||
}
|
||||
character, found, err := tx.Character.Load(ctx, characterID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found || strings.TrimSpace(character.CharacterID) != characterID {
|
||||
return ErrCharacterRequired
|
||||
}
|
||||
if cmd.AccountID != "" && strings.TrimSpace(character.AccountID) != strings.TrimSpace(cmd.AccountID) {
|
||||
return fmt.Errorf("%w: character=%s account=%q want=%q",
|
||||
ErrContainerUpgradeContract, characterID, character.AccountID, cmd.AccountID)
|
||||
}
|
||||
inventory, found, err := tx.Inventory.Load(ctx, characterID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found {
|
||||
return ErrInventoryNotFound
|
||||
}
|
||||
inventory = dnfrepo.CloneInventory(inventory)
|
||||
if inventory.Slots == nil {
|
||||
inventory.Slots = make(map[string]dnfrepo.ItemStack)
|
||||
}
|
||||
key := slotKey(cmd.SourceListType, cmd.SourceSlotIndex)
|
||||
stack, found := inventory.Slots[key]
|
||||
if !found || stack.Count <= 0 || stack.ItemID <= 0 {
|
||||
return fmt.Errorf("%w: list=%d slot=%d is empty", ErrSlotNotFound, cmd.SourceListType, cmd.SourceSlotIndex)
|
||||
}
|
||||
resolution, err := resolver(stack.ItemID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: item=%d: %v", ErrContainerUpgradeContract, stack.ItemID, err)
|
||||
}
|
||||
upgrade := resolution.ContainerUpgrade
|
||||
if resolution.ItemID != stack.ItemID || upgrade == nil || upgrade.ItemID != stack.ItemID || upgrade.Kind != wantKind {
|
||||
return fmt.Errorf("%w: item=%d want=%s resolution=%+v pvf_path=%q",
|
||||
ErrContainerUpgradeItemMismatch, stack.ItemID, wantKind, resolution.ContainerUpgrade, resolution.PVFPath)
|
||||
}
|
||||
|
||||
before, after := int64(0), int64(0)
|
||||
listType := listTypeMain
|
||||
cargoPad := false
|
||||
switch wantKind {
|
||||
case alignedcmd.ContainerUpgradeInventory, alignedcmd.ContainerUpgradeCargo:
|
||||
if tx.Settings == nil {
|
||||
return ErrContainerUpgradeContract
|
||||
}
|
||||
record, found, err := tx.Settings.Load(ctx, dnfrepo.CharacterContainerStateScope(characterID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("%w: character=%s", ErrContainerUpgradeStateMissing, characterID)
|
||||
}
|
||||
state, err := dnfrepo.CharacterContainerStateFromSettings(record, characterID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
record = dnfrepo.CloneSettings(record)
|
||||
if record.Values == nil {
|
||||
record.Values = make(map[string]string)
|
||||
}
|
||||
switch wantKind {
|
||||
case alignedcmd.ContainerUpgradeInventory:
|
||||
before = int64(state.MainSlotCount)
|
||||
if before+containerUpgradeMainStep > containerUpgradeMaxMainExpansion {
|
||||
return fmt.Errorf("%w: main_list_param16=%d", ErrContainerUpgradeMaxStage, before)
|
||||
}
|
||||
after = before + containerUpgradeMainStep
|
||||
record.Values["main_list_param16"] = strconv.FormatInt(after, 10)
|
||||
record.Values["main_inventory_last_upgrade_commodity"] = strconv.FormatInt(stack.ItemID, 10)
|
||||
record.Values["main_inventory_last_upgrade_from"] = strconv.FormatInt(before, 10)
|
||||
record.Values["main_inventory_last_upgrade_to"] = strconv.FormatInt(after, 10)
|
||||
record.Values["source"] = "current_exe_container_upgrade_voucher"
|
||||
listType = listTypeMain
|
||||
default:
|
||||
before = int64(state.PersonalCargoSlotCount)
|
||||
if before+containerUpgradeCargoStep > containerUpgradeMaxCargoSlots {
|
||||
return fmt.Errorf("%w: personal_cargo_list_param16=%d", ErrContainerUpgradeMaxStage, before)
|
||||
}
|
||||
after = before + containerUpgradeCargoStep
|
||||
record.Values["personal_cargo_list_param16"] = strconv.FormatInt(after, 10)
|
||||
record.Values["personal_cargo_last_upgrade_commodity"] = strconv.FormatInt(stack.ItemID, 10)
|
||||
record.Values["personal_cargo_last_upgrade_from"] = strconv.FormatInt(before, 10)
|
||||
record.Values["personal_cargo_last_upgrade_to"] = strconv.FormatInt(after, 10)
|
||||
record.Values["source"] = "current_exe_container_upgrade_voucher"
|
||||
listType = listTypePersonalCargo
|
||||
cargoPad = true
|
||||
}
|
||||
record.UpdatedAt = now
|
||||
// 落库之前先用仓储自己的校验器审一遍:仓储是这几张合法表的唯一权威
|
||||
// (背包 0..24 步长 8、金库 8..200 步长 16),写坏了这里就会先炸。
|
||||
if _, err := dnfrepo.CharacterContainerStateFromSettings(record, characterID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Settings.Save(ctx, record); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
if tx.Account == nil {
|
||||
return ErrContainerUpgradeContract
|
||||
}
|
||||
accountID := strings.TrimSpace(character.AccountID)
|
||||
if accountID == "" {
|
||||
return fmt.Errorf("%w: character=%s", ErrContainerUpgradeContract, characterID)
|
||||
}
|
||||
account, found, err := tx.Account.Load(ctx, accountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found || strings.TrimSpace(account.AccountID) != accountID {
|
||||
return fmt.Errorf("%w: account=%q", ErrContainerUpgradeContract, accountID)
|
||||
}
|
||||
account = dnfrepo.CloneAccount(account)
|
||||
if account.Metadata == nil {
|
||||
account.Metadata = make(map[string]string, 4)
|
||||
}
|
||||
raw := strings.TrimSpace(account.Metadata["account_cargo_level"])
|
||||
if raw == "" {
|
||||
return fmt.Errorf("%w: account=%q", ErrContainerUpgradeAccountMissing, accountID)
|
||||
}
|
||||
level, parseErr := strconv.ParseInt(raw, 10, 64)
|
||||
if parseErr != nil {
|
||||
return fmt.Errorf("%w: account=%q account_cargo_level=%q", ErrContainerUpgradeAccountBroken, accountID, raw)
|
||||
}
|
||||
if level <= 0 {
|
||||
return fmt.Errorf("%w: account=%q level=%d", ErrContainerUpgradeAccountMissing, accountID, level)
|
||||
}
|
||||
next, ok := dnfcargo.NextAccountCargoTier(level)
|
||||
if !ok || next <= level {
|
||||
return fmt.Errorf("%w: account_cargo_level=%d", ErrContainerUpgradeMaxStage, level)
|
||||
}
|
||||
// 券付账:这里刻意**不调** applyCargoUpgradeCost 的那三档收费
|
||||
// (1->8 金币、16/24 无色小晶块、其余点券),否则等于让玩家付两次。
|
||||
account.Metadata["account_cargo_created"] = "true"
|
||||
account.Metadata["account_cargo_level"] = strconv.FormatInt(next, 10)
|
||||
if strings.TrimSpace(account.Metadata["account_cargo_gold"]) == "" {
|
||||
account.Metadata["account_cargo_gold"] = "0"
|
||||
}
|
||||
account.UpdatedAt = now
|
||||
if err := tx.Account.Save(ctx, account); err != nil {
|
||||
return err
|
||||
}
|
||||
before, after = level, next
|
||||
listType = listTypeAccountCargo
|
||||
cargoPad = true
|
||||
}
|
||||
|
||||
remaining := stack.Count - 1
|
||||
if remaining == 0 {
|
||||
delete(inventory.Slots, key)
|
||||
} else {
|
||||
stack = cloneStack(stack)
|
||||
stack.Count = remaining
|
||||
inventory.Slots[key] = stack
|
||||
}
|
||||
inventory.UpdatedAt = now
|
||||
if err := dnfrepo.SaveInventoryFields(ctx, tx.Inventory, inventory, dnfrepo.InventoryFieldSlots); err != nil {
|
||||
return err
|
||||
}
|
||||
result = UseStackableResult{
|
||||
CharacterID: characterID,
|
||||
AccountID: strings.TrimSpace(character.AccountID),
|
||||
ListType: cmd.SourceListType,
|
||||
SlotIndex: cmd.SourceSlotIndex,
|
||||
ItemID: stack.ItemID,
|
||||
RemainingCount: remaining,
|
||||
Changed: true,
|
||||
PVFPath: strings.TrimSpace(resolution.PVFPath),
|
||||
StackableType: resolution.StackableType,
|
||||
ContainerUpgradeApplied: true,
|
||||
ContainerUpgradeKind: wantKind,
|
||||
ContainerUpgradeBefore: before,
|
||||
ContainerUpgradeAfter: after,
|
||||
ContainerUpgradeListType: listType,
|
||||
ContainerUpgradeNeedsCargoPad: cargoPad,
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return UseStackableResult{}, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
Reference in new issue
Block a user