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

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
}
}
}