Files
90CNServer/server/internal/services/dnfbridge/avatar_bind_plus_owner.go
T
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

187 lines
7.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package dnfbridge
import (
"context"
"errors"
"fmt"
"math"
"strings"
dnfavatarcompound "longheng.io/server/internal/modules/dnf/avatarcompound"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
)
// commitCurrentAvatarBindPlus 落地一次 op1119(梦想/绝对绑定装扮合成器):
//
// 扣 8 件材料装扮(请求体里那 8 个 (槽, 道具))+ 扣 1 个该套装的绝对绑定合成器
// → 发出玩家点名的输出装扮([absolute bind plus rare][职业][部位])
//
// 全程走 op99 同一套 dnfavatarcompound.Owner 事务:任何一步校验失败都整体回滚,
// 不会出现"扣了材料没发奖"。
//
// 合成器本身**不在请求体里**(两次现场取证的 144 字节里都找不到合成器字段),
// 所以这里用唯一自洽的口径反查:请求点名的输出 → chn_absolute_bind_cube.etc 里的
// 套装序号 → 背包里 [action type] [absolute bind cube] <同一套装> 的道具。这样
// 既不需要新协议字段,也不可能出现"没带合成器白嫖"。
func (s *Service) commitCurrentAvatarBindPlus(
ctx context.Context,
session *gameSession,
catalog *pvfDungeonDropCatalog,
request currentAvatarBindPlusRequest,
) (currentAvatarBindPlusOutcome, error) {
if catalog == nil || catalog.source == nil {
return currentAvatarBindPlusOutcome{}, errCurrentAvatarBindPlusUnavailable
}
accountID, characterID, repositories, owner, err := s.currentAvatarCompoundOwner(session)
if err != nil {
return currentAvatarBindPlusOutcome{}, err
}
character, found, err := repositories.Character.Load(ctx, characterID)
if err != nil || !found || strings.TrimSpace(character.AccountID) != accountID {
return currentAvatarBindPlusOutcome{}, errors.Join(errCurrentAvatarBindPlusUnavailable, err)
}
jobValue := numericCharacterStat(character.Job)
if jobValue < 0 || jobValue > math.MaxUint8 {
return currentAvatarBindPlusOutcome{}, fmt.Errorf("%w: character job=%q", errCurrentAvatarBindPlusUnavailable, character.Job)
}
config, err := loadCurrentAvatarBindPlusConfig(catalog.source)
if err != nil {
return currentAvatarBindPlusOutcome{}, err
}
jobKey, err := currentAvatarBindPlusJobKey(catalog.source, byte(jobValue))
if err != nil {
return currentAvatarBindPlusOutcome{}, err
}
output, err := currentAvatarBindPlusResolveOutput(config, jobKey, request.RequestedItemID)
if err != nil {
return currentAvatarBindPlusOutcome{}, err
}
// 输出道具必须是真实装扮(PVF 只读解析,放在事务外)。
outputDefinition, _, err := currentAvatarCompoundDefinition(catalog, request.RequestedItemID)
if err != nil {
return currentAvatarBindPlusOutcome{}, err
}
var outcome currentAvatarBindPlusOutcome
err = owner.Compound(ctx, dnfavatarcompound.Command{
AccountID: accountID,
CharacterID: characterID,
Project: func(assets *dnfavatarcompound.Assets) error {
if assets == nil || assets.Character == nil || assets.Inventory == nil || assets.Inventory.Slots == nil ||
numericCharacterStat(assets.Character.Job) != jobValue {
return errCurrentAvatarBindPlusUnavailable
}
inventory := assets.Inventory
// 1. 8 件材料:槽位、道具 ID、数量、锁、PVF 装扮身份,逐条校验。
materialKeys := make([]string, 0, len(request.Materials))
for index, material := range request.Materials {
key := currentCeraShopInventorySlotKey(currentAvatarInventoryListType, material.Slot)
stack, exists := inventory.Slots[key]
if !exists || stack.ItemID != int64(material.ItemID) || stack.Count != 1 || currentNPCShopItemLocked(stack) {
return fmt.Errorf("%w: index=%d slot=%d item=%d found=%t stack=%+v",
errCurrentAvatarBindPlusMaterial, index, material.Slot, material.ItemID, exists, stack)
}
if _, _, resolveErr := currentAvatarCompoundDefinition(catalog, material.ItemID); resolveErr != nil {
return fmt.Errorf("%w: index=%d item=%d: %v",
errCurrentAvatarBindPlusMaterial, index, material.ItemID, resolveErr)
}
materialKeys = append(materialKeys, key)
}
// 2. 合成器:按套装序号反查。
cubeKey, cubeStack, cubeErr := currentAvatarBindPlusFindCube(catalog, inventory, output.SetIndex)
if cubeErr != nil {
return cubeErr
}
cubeListType, cubeSlot, _ := dnfrepo.ParseInventorySlotKey(cubeKey)
cubeItemID := uint32(cubeStack.ItemID)
// 3. 扣材料(8 件各 1,装扮不堆叠,直接删槽)。
for _, key := range materialKeys {
delete(inventory.Slots, key)
}
// 4. 扣合成器 1 个。
cubeStack.Count--
cubeRemain := cubeStack.Count
if cubeStack.Count <= 0 {
delete(inventory.Slots, cubeKey)
} else {
entry := currentItemListEntryFromStack(cubeListType, cubeSlot, cubeStack)
cubeStack.RawEntry = append([]byte(nil), entry.data[:]...)
inventory.Slots[cubeKey] = cubeStack
}
// 5. 发输出装扮:落在第一个材料槽(刚空出来、必然是合法的装扮栏槽位)。
outputSlot := request.Materials[0].Slot
stack := currentAvatarCompoundOutputStack(outputDefinition, request.AbilityNo)
entry := currentItemListEntryFromStack(currentAvatarInventoryListType, outputSlot, stack)
stack.RawEntry = append([]byte(nil), entry.data[:]...)
inventory.Slots[currentCeraShopInventorySlotKey(currentAvatarInventoryListType, outputSlot)] = stack
consumed := make([]int16, 0, currentAvatarBindPlusMaterialCount)
for _, material := range request.Materials {
consumed = append(consumed, material.Slot)
}
outcome = currentAvatarBindPlusOutcome{
OutputItemID: request.RequestedItemID,
OutputSlot: outputSlot,
AbilityNo: request.AbilityNo,
ConsumedSlots: consumed,
SetIndex: output.SetIndex,
Part: output.Part,
CubeItemID: cubeItemID,
CubeListType: cubeListType,
CubeSlot: cubeSlot,
CubeRemain: cubeRemain,
}
return nil
},
})
if err != nil {
return currentAvatarBindPlusOutcome{}, currentAvatarCompoundOwnerError(err)
}
return outcome, nil
}
// currentAvatarBindPlusFindCube 在背包里找该套装序号的绝对绑定合成器。
// 多个候选时优先主背包(list 0),保证"帐号金库里的合成器"不会被优先消耗。
func currentAvatarBindPlusFindCube(
catalog *pvfDungeonDropCatalog,
inventory *dnfrepo.InventoryRecord,
setIndex int64,
) (string, dnfrepo.ItemStack, error) {
if catalog == nil || inventory == nil {
return "", dnfrepo.ItemStack{}, errCurrentAvatarBindPlusCube
}
bestKey := ""
var bestStack dnfrepo.ItemStack
bestListType := byte(0xFF)
for key, stack := range inventory.Slots {
if stack.ItemID <= 0 || stack.ItemID > math.MaxUint32 || stack.Count <= 0 || currentNPCShopItemLocked(stack) {
continue
}
definition, resolveErr := catalog.ResolveItem(uint32(stack.ItemID))
if resolveErr != nil || !currentAvatarBindPlusDeclaresCube(definition) {
continue
}
cubeSet, ok := currentAvatarBindPlusCubeSet(definition)
if !ok || cubeSet != setIndex {
continue
}
listType, _, parsed := dnfrepo.ParseInventorySlotKey(key)
if !parsed {
continue
}
if bestKey != "" && listType >= bestListType {
continue
}
bestKey, bestStack, bestListType = key, stack, listType
}
if bestKey == "" {
return "", dnfrepo.ItemStack{}, fmt.Errorf("%w: set=%d 背包里没有对应的绝对绑定合成器", errCurrentAvatarBindPlusCube, setIndex)
}
return bestKey, bestStack, nil
}