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,288 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user