服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
289 lines
10 KiB
Go
289 lines
10 KiB
Go
package dnfbridge
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"strconv"
|
|
|
|
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
|
)
|
|
|
|
func currentGuardianGemFindTarget(
|
|
inventory dnfrepo.InventoryRecord,
|
|
equipment dnfrepo.EquipmentRecord,
|
|
equipmentFound bool,
|
|
request currentGuardianGemUseRequest,
|
|
) (currentGuardianGemTargetLocation, error) {
|
|
_ = inventory
|
|
candidates := make([]currentGuardianGemTargetLocation, 0, 1)
|
|
if equipmentFound {
|
|
for key, item := range equipment.Entries {
|
|
if item.ItemID != int64(request.TargetMedalItemID) || item.ItemID <= 0 {
|
|
continue
|
|
}
|
|
actorSlot, ok := currentEXEActorEquipmentSlot(item)
|
|
if !ok || actorSlot != currentGuildMedalActorSlot {
|
|
continue
|
|
}
|
|
candidates = append(candidates, currentGuardianGemTargetLocation{
|
|
Container: currentGuardianGemTargetEquipped,
|
|
ListType: currentSocketListEquipment,
|
|
Slot: item.SlotIndex,
|
|
Key: key,
|
|
})
|
|
}
|
|
}
|
|
switch len(candidates) {
|
|
case 0:
|
|
return currentGuardianGemTargetLocation{}, fmt.Errorf("%w: actor_slot=%d item=%d", errCurrentGuardianGemTargetMissing, currentGuildMedalActorSlot, request.TargetMedalItemID)
|
|
case 1:
|
|
return candidates[0], nil
|
|
default:
|
|
return currentGuardianGemTargetLocation{}, fmt.Errorf("%w: actor_slot=%d item=%d matches=%d", errCurrentGuardianGemTargetAmbiguous, currentGuildMedalActorSlot, request.TargetMedalItemID, len(candidates))
|
|
}
|
|
}
|
|
|
|
func currentGuardianGemWriteTarget(
|
|
inventory *dnfrepo.InventoryRecord,
|
|
equipment *dnfrepo.EquipmentRecord,
|
|
target currentGuardianGemTargetLocation,
|
|
request currentGuardianGemUseRequest,
|
|
) error {
|
|
if inventory == nil || equipment == nil {
|
|
return errCurrentGuardianGemInventoryMissing
|
|
}
|
|
switch target.Container {
|
|
case currentGuardianGemTargetInventory:
|
|
item, ok := inventory.Slots[target.Key]
|
|
if !ok {
|
|
return errCurrentGuardianGemTargetMissing
|
|
}
|
|
if err := currentGuardianGemWriteRawSocket(item.RawEntry, request.SocketIndex, request.GuardianGemItemID); err != nil {
|
|
return err
|
|
}
|
|
currentGuardianGemSyncRawSocketExtra(&item.Extra, item.RawEntry)
|
|
currentRefreshStackRawEntry(&item, target.ListType, target.Slot)
|
|
inventory.Slots[target.Key] = item
|
|
case currentGuardianGemTargetWarehouse:
|
|
item, ok := inventory.Warehouse[target.Key]
|
|
if !ok {
|
|
return errCurrentGuardianGemTargetMissing
|
|
}
|
|
if err := currentGuardianGemWriteRawSocket(item.RawEntry, request.SocketIndex, request.GuardianGemItemID); err != nil {
|
|
return err
|
|
}
|
|
currentGuardianGemSyncRawSocketExtra(&item.Extra, item.RawEntry)
|
|
currentRefreshStackRawEntry(&item, target.ListType, target.Slot)
|
|
inventory.Warehouse[target.Key] = item
|
|
case currentGuardianGemTargetEquipped:
|
|
item, ok := equipment.Entries[target.Key]
|
|
if !ok {
|
|
return errCurrentGuardianGemTargetMissing
|
|
}
|
|
if err := currentGuardianGemWriteRawSocket(item.RawEntry, request.SocketIndex, request.GuardianGemItemID); err != nil {
|
|
return err
|
|
}
|
|
currentGuardianGemSyncRawSocketExtra(&item.Extra, item.RawEntry)
|
|
if row, ok := currentItemListEntryFromEquipment(item); ok {
|
|
item.RawEntry = append([]byte(nil), row.data[:]...)
|
|
}
|
|
equipment.Entries[target.Key] = item
|
|
default:
|
|
return errCurrentGuardianGemTargetMissing
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func currentGuardianGemWriteRawSocket(raw []byte, socketIndex byte, gemItemID uint32) error {
|
|
if len(raw) != currentItemListEntryWireSize {
|
|
return fmt.Errorf("%w: raw_len=%d", errCurrentGuardianGemTargetRawMissing, len(raw))
|
|
}
|
|
value, err := currentGuardianGemSocketValue(gemItemID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
offset := currentGuardianGemRawSocketOffset + int(socketIndex)*currentGuardianGemRawSocketWidth
|
|
if offset < 0 || offset+currentGuardianGemRawSocketWidth > len(raw) {
|
|
return fmt.Errorf("%w: socket=%d offset=%d raw_len=%d", errCurrentGuardianGemTargetRawMissing, socketIndex, offset, len(raw))
|
|
}
|
|
// The current client confirms that an occupied guardian-gem slot cannot
|
|
// return its old gem. Reusing the same op829 request therefore replaces the
|
|
// stored word; the caller atomically consumes only the newly selected gem.
|
|
binary.LittleEndian.PutUint16(raw[offset:offset+currentGuardianGemRawSocketWidth], value)
|
|
return nil
|
|
}
|
|
|
|
// currentGuardianGemTargetRawEntry re-reads the target medal raw row using the
|
|
// same container rules as currentGuardianGemWriteTarget, so the duplicate
|
|
// check inspects exactly the bytes the write would modify.
|
|
func currentGuardianGemTargetRawEntry(
|
|
inventory *dnfrepo.InventoryRecord,
|
|
equipment *dnfrepo.EquipmentRecord,
|
|
target currentGuardianGemTargetLocation,
|
|
) ([]byte, error) {
|
|
if inventory == nil || equipment == nil {
|
|
return nil, errCurrentGuardianGemInventoryMissing
|
|
}
|
|
switch target.Container {
|
|
case currentGuardianGemTargetInventory:
|
|
item, ok := inventory.Slots[target.Key]
|
|
if !ok {
|
|
return nil, errCurrentGuardianGemTargetMissing
|
|
}
|
|
return item.RawEntry, nil
|
|
case currentGuardianGemTargetWarehouse:
|
|
item, ok := inventory.Warehouse[target.Key]
|
|
if !ok {
|
|
return nil, errCurrentGuardianGemTargetMissing
|
|
}
|
|
return item.RawEntry, nil
|
|
case currentGuardianGemTargetEquipped:
|
|
item, ok := equipment.Entries[target.Key]
|
|
if !ok {
|
|
return nil, errCurrentGuardianGemTargetMissing
|
|
}
|
|
return item.RawEntry, nil
|
|
default:
|
|
return nil, errCurrentGuardianGemTargetMissing
|
|
}
|
|
}
|
|
|
|
// currentGuardianGemRejectDuplicateAttribute enforces the guardian-gem rule
|
|
// that one medal cannot carry two gems of the same PVF enchant family. The
|
|
// client refuses that in its own socket table, but the C2S request carries
|
|
// nothing but a raw socket index, so the rule has to be enforced here before
|
|
// the socket word is written.
|
|
//
|
|
// A stored socket word that no longer resolves to a [flag gem] is skipped
|
|
// rather than treated as a conflict: the server cannot name its family, and
|
|
// refusing would strand a medal whose word predates this rule.
|
|
func currentGuardianGemRejectDuplicateAttribute(
|
|
inventory *dnfrepo.InventoryRecord,
|
|
equipment *dnfrepo.EquipmentRecord,
|
|
target currentGuardianGemTargetLocation,
|
|
request currentGuardianGemUseRequest,
|
|
catalog *pvfDungeonDropCatalog,
|
|
gem currentGuardianGemDefinition,
|
|
) error {
|
|
if gem.EnchantFamily == "" {
|
|
return nil
|
|
}
|
|
raw, err := currentGuardianGemTargetRawEntry(inventory, equipment, target)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(raw) != currentItemListEntryWireSize {
|
|
return fmt.Errorf("%w: raw_len=%d", errCurrentGuardianGemTargetRawMissing, len(raw))
|
|
}
|
|
for index := 0; index < currentGuardianGemSocketCount; index++ {
|
|
if byte(index) == request.SocketIndex {
|
|
continue
|
|
}
|
|
offset := currentGuardianGemRawSocketOffset + index*currentGuardianGemRawSocketWidth
|
|
if offset+currentGuardianGemRawSocketWidth > len(raw) {
|
|
continue
|
|
}
|
|
value := binary.LittleEndian.Uint16(raw[offset : offset+currentGuardianGemRawSocketWidth])
|
|
if value == 0 {
|
|
continue
|
|
}
|
|
occupiedItemID := currentGuardianGemItemIDBase + uint32(value)
|
|
occupied, resolveErr := resolveCurrentGuardianGem(catalog, occupiedItemID)
|
|
if resolveErr != nil {
|
|
continue
|
|
}
|
|
if occupied.EnchantFamily == gem.EnchantFamily {
|
|
return fmt.Errorf(
|
|
"%w: socket=%d family=%q occupied_socket=%d occupied_item=%d",
|
|
errCurrentGuardianGemAttributeDuplicate,
|
|
request.SocketIndex,
|
|
gem.EnchantFamily,
|
|
index,
|
|
occupiedItemID,
|
|
)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func currentGuardianGemSyncRawSocketExtra(extra *map[string]string, raw []byte) {
|
|
const rawSocketStateBytes = 9 // four u16 values plus the adjacent preserved byte.
|
|
if len(raw) < currentGuardianGemRawSocketOffset+rawSocketStateBytes {
|
|
return
|
|
}
|
|
currentEnsureExtra(extra)
|
|
currentSetHexExtra(*extra, "raw_data_65", raw[currentGuardianGemRawSocketOffset:currentGuardianGemRawSocketOffset+rawSocketStateBytes])
|
|
}
|
|
|
|
func currentGuardianGemFindMedalBagSource(items map[string]dnfrepo.ItemStack, sourceSlot uint16, gemItemID uint32) (string, int16, error) {
|
|
if sourceSlot > 32767 || !currentGuardianGemPageContains(int16(sourceSlot)) {
|
|
return "", 0, fmt.Errorf("%w: slot=%d", errCurrentGuardianGemSourceSlotRange, sourceSlot)
|
|
}
|
|
slot := int16(sourceSlot)
|
|
key := currentSocketInventoryKey(currentGuardianGemInventoryListType, slot)
|
|
item, found := items[key]
|
|
if !found || item.ItemID != int64(gemItemID) || item.Count <= 0 {
|
|
return "", 0, fmt.Errorf("%w: key=%s item=%d", errCurrentGuardianGemSourceMissing, key, gemItemID)
|
|
}
|
|
return key, slot, nil
|
|
}
|
|
|
|
func (s *Service) currentGuardianGemUseSnapshot(
|
|
session *gameSession,
|
|
request currentGuardianGemUseRequest,
|
|
) (currentGuardianGemUseSnapshot, error) {
|
|
if session == nil || session.selectedCharacterID == 0 {
|
|
return currentGuardianGemUseSnapshot{}, errCurrentGuardianGemCharacterMissing
|
|
}
|
|
repositories, ok := s.repositoryGroup()
|
|
if !ok || repositories.Inventory == nil || repositories.Equipment == nil {
|
|
return currentGuardianGemUseSnapshot{}, errCurrentGuardianGemRepositoryMissing
|
|
}
|
|
characterID := strconv.Itoa(int(session.selectedCharacterID))
|
|
inventory, inventoryFound, err := repositories.Inventory.Load(context.Background(), characterID)
|
|
if err != nil {
|
|
return currentGuardianGemUseSnapshot{}, fmt.Errorf("load guardian gem inventory: %w", err)
|
|
}
|
|
equipment, equipmentFound, err := repositories.Equipment.Load(context.Background(), characterID)
|
|
if err != nil {
|
|
return currentGuardianGemUseSnapshot{}, fmt.Errorf("load guardian gem equipment: %w", err)
|
|
}
|
|
var snapshot currentGuardianGemUseSnapshot
|
|
if inventoryFound {
|
|
currentGuardianGemInspectInventoryMap(&snapshot, inventory.Slots, request, false)
|
|
currentGuardianGemInspectInventoryMap(&snapshot, inventory.Warehouse, request, true)
|
|
}
|
|
if equipmentFound {
|
|
for _, item := range equipment.Entries {
|
|
actorSlot, ok := currentEXEActorEquipmentSlot(item)
|
|
if ok && actorSlot == currentGuildMedalActorSlot && item.ItemID == int64(request.TargetMedalItemID) {
|
|
snapshot.TargetEquippedMatches++
|
|
}
|
|
}
|
|
}
|
|
return snapshot, nil
|
|
}
|
|
|
|
func currentGuardianGemInspectInventoryMap(
|
|
snapshot *currentGuardianGemUseSnapshot,
|
|
items map[string]dnfrepo.ItemStack,
|
|
request currentGuardianGemUseRequest,
|
|
warehouse bool,
|
|
) {
|
|
if snapshot == nil {
|
|
return
|
|
}
|
|
for key, item := range items {
|
|
listType, slot, ok := parseSceneInventorySlotKey(key)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if !warehouse && listType == currentGuardianGemInventoryListType &&
|
|
slot == int16(request.GuardianGemSourceSlot) &&
|
|
item.ItemID == int64(request.GuardianGemItemID) && item.Count > 0 {
|
|
snapshot.GemMainStackCount += item.Count
|
|
}
|
|
}
|
|
}
|