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

143 lines
6.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 (
"errors"
"math"
"sort"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
)
// currentGrantDisjointRewards places one resolved reward set per its fixed
// warehouse identity: the six cube fragment types stack into their fixed
// crystal warehouse slots (354..359) and the six soul types into their fixed
// soul warehouse slots (360..365); every other reward lands in the character
// backpack. Warehouse overflow falls back to the backpack so no reward is
// ever lost. It returns the ACK rows (slot/item/count/granted), whether the
// account warehouse record changed, and an error.
//
// overflow, when non-nil, collects the rows the main bag had no room for
// instead of failing the disassembly; the caller posts them as mail.
func currentGrantDisjointRewards(
inventory *dnfrepo.InventoryRecord,
account *dnfrepo.AccountInventoryRecord,
catalog *pvfDungeonDropCatalog,
rewards []currentDisjointReward,
overflow *[]currentDisjointReward,
mainSlotCount uint16,
) ([]currentDisjointRewardSlot, bool, error) {
if inventory == nil || account == nil || catalog == nil || len(rewards) == 0 {
return nil, false, errCurrentDisjointRewardInvalid
}
previousCounts := make(map[int16]int64)
for key, stack := range inventory.Slots {
listType, slot, ok := parseSceneInventorySlotKey(key)
if ok && listType == dnfrepo.MainInventoryListType && stack.Count > 0 {
previousCounts[slot] = stack.Count
}
}
for key, stack := range account.Slots {
listType, slot, ok := parseSceneInventorySlotKey(key)
if ok && listType == dnfrepo.MainInventoryListType && stack.Count > 0 {
previousCounts[slot] = stack.Count
}
}
changed := make(map[int16]uint32)
accountDirty := false
for _, reward := range rewards {
if reward.ItemID == 0 || reward.Count == 0 {
return nil, false, errCurrentDisjointRewardInvalid
}
definition, err := catalog.ResolveItem(reward.ItemID)
if err != nil || definition.Kind != dungeonDropItemStackable {
return nil, false, errCurrentDisjointRewardInvalid
}
remaining := int64(reward.Count)
// 统一入口:有固定格就先进账号共享仓,装不下的余量再交给下面的角色侧
// 溢出(含溢出邮件)。落点判定不再在这里手写一行。
shared := grantCurrentAccountSharedFirst(account, definition, reward.Count, "dungeon_pvf_disjoint")
if shared.Changed {
accountDirty = true
changed[shared.Slot] = reward.ItemID
}
remaining = int64(shared.Remainder)
if remaining > 0 {
slots, err := grantCurrentCeraShopProduct(inventory, definition, uint32(remaining), mainSlotCount)
if err != nil {
// A full bag used to abort the whole disassembly, so the
// source survived but the player got nothing and had to clear
// space first. Hand the row to the caller's overflow sink
// instead; it posts the rewards as mail inside this same
// transaction, so the source is never consumed by a commit
// whose mail did not land.
if overflow != nil && errors.Is(err, errDungeonPickupInventoryFull) {
*overflow = append(*overflow, currentDisjointReward{
ItemID: reward.ItemID,
Count: uint32(remaining),
})
continue
}
return nil, false, err
}
for _, slotValue := range slots {
slot := int16(slotValue)
stack, found := inventory.Slots[currentCeraShopInventorySlotKey(0, slot)]
if !found || stack.ItemID != int64(reward.ItemID) || stack.Count <= 0 {
return nil, false, errCurrentDisjointRewardInvalid
}
entry := currentItemListEntryFromStack(0, slot, stack)
stack.RawEntry = append([]byte(nil), entry.data[:]...)
inventory.Slots[currentCeraShopInventorySlotKey(0, slot)] = stack
changed[slot] = reward.ItemID
}
}
}
out := make([]currentDisjointRewardSlot, 0, len(changed))
for slot, itemID := range changed {
stack, found := inventory.Slots[currentCeraShopInventorySlotKey(0, slot)]
if !found {
stack, found = account.Slots[currentCeraShopInventorySlotKey(0, slot)]
}
if !found || stack.Count <= 0 || stack.Count > math.MaxUint32 {
return nil, false, errCurrentDisjointRewardInvalid
}
added := stack.Count - previousCounts[slot]
if added <= 0 || added > math.MaxUint32 {
return nil, false, errCurrentDisjointRewardInvalid
}
// Count 与 Granted 是两个不同口径,任何一条弹窗体路径都必须用 Granted:
// Count 是整堆终值(给 0x14 增量行),Granted 是本次新增量(给弹窗"获得 N 个")。
// 两者只在背包里本来没有该道具时数值相同 —— 这正是 op202 早先发错字段却测不出来的原因。
out = append(out, currentDisjointRewardSlot{Slot: slot, ItemID: itemID, Count: uint32(stack.Count), Granted: uint32(added)})
}
sort.Slice(out, func(left, right int) bool { return out[left].Slot < out[right].Slot })
return out, accountDirty, nil
}
// currentDisjointWarehouseFixedSlots 是**权威表**(dnfrepo.AccountSharedWarehouseFixedSlots)
// 的 uint32 视图:六个晶块占晶体仓库 354..359、六个灵魂占灵魂仓库 360..365,
// 一个道具 ID 一个固定格;元素结晶(3166/3167)与其它所有可叠加道具没有固定格,
// 留在角色背包。
//
// 这里过去是一份手写的 12 条硬编码表,与 inventory/owner.go 的
// upgradeAccountSharedMaterialSlot、equipment_regeneration_pvf.go 的
// equipmentRegenerationAccountSharedItem 是三份彼此独立的副本。现改为从权威表**派生**,
// 结构上不可能再各自漂移;保留 map 形态只是为了让既有调用点零改动。
// 权威表本身、以及"获取必须进 354..365 / 允许手动存进个人仓库与账号金库 /
// 取回仍回 354..365"的完整运营口径,见 repository/account_shared_materials.go。
var currentDisjointWarehouseFixedSlots = func() map[uint32]int16 {
canonical := dnfrepo.AccountSharedWarehouseFixedSlots()
out := make(map[uint32]int16, len(canonical))
for itemID, slot := range canonical {
out[uint32(itemID)] = slot
}
return out
}()
// currentAccountWarehouseFixedSlot 是权威表的只读查询入口,供 op19 落点规则使用:
// 晶体/灵魂类道具(六个晶块 + 六个灵魂)在自己账号里只有一个固定仓库格,
// 落在角色背包里的那一份要改回这个格子。
func currentAccountWarehouseFixedSlot(itemID int64) (int16, bool) {
return dnfrepo.AccountSharedWarehouseSlotForItem(itemID)
}