DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
hanfuit committed 2026-11-04 14:31:55 +08:00
commit 645b64fe21
4388 files changed
+1405457

No files matched your search

@@ -0,0 +1,238 @@
package dnfbridge
import (
"context"
"encoding/binary"
"fmt"
"strconv"
"strings"
"time"
"longheng.io/server/internal/modules/dnf/dnfenum"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
)
// sendSelectedEquippedCreatureItemRefresh replays only the existing creature
// object in actor equipment slot 26. Full mode1 and op13 create that object,
// but the current EXE applies a creature enchant's dynamic card state only on
// its same-template op14 path. Never widen this to generic list-3 equipment:
// ordinary equipment updates have different native lifetime requirements.
func (s *Service) sendSelectedEquippedCreatureItemRefresh(session *gameSession, source string) error {
if session == nil || session.selectedCharacterID == 0 {
return fmt.Errorf("equipped creature item refresh requires an active character")
}
repositories, ok := s.repositoryGroup()
if !ok || repositories.Equipment == nil {
return fmt.Errorf("equipped creature item refresh repository is unavailable")
}
ctx, cancel := context.WithTimeout(context.Background(), currentRepositorySnapshotTimeout)
defer cancel()
characterID := strconv.Itoa(int(session.selectedCharacterID))
record, found, err := repositories.Equipment.Load(ctx, characterID)
if err != nil {
return fmt.Errorf("load equipped creature item refresh: %w", err)
}
equipped, worn := currentEquippedCreatureEquipmentEntry(record)
if !found || !worn {
s.logPacketEvent("game-upper-current-equipped-creature-item-refresh-skipped",
"conn_id", session.connID,
"channel_id", session.channel.ID,
"source", source,
"char_id", session.selectedCharacterID,
"reason", "equipment_slot26_absent")
return nil
}
creature := currentEquippedCreatureFromEquipment(equipped)
if !creature.valid() || int64(creature.itemID) != equipped.ItemID {
return fmt.Errorf(
"equipped creature item refresh rejected invalid slot 26: item=%d serial=%d",
equipped.ItemID,
creature.serialOrHandle,
)
}
entry := currentEquippedCreatureItemRefreshEntry(equipped)
if binary.LittleEndian.Uint16(entry.data[0:2]) != 26 {
return fmt.Errorf("equipped creature item refresh could not project slot 26")
}
body := buildCurrentItemUpdateBody(currentSocketListEquipment, []currentItemListEntry{entry})
s.logPacketEvent("game-upper-current-equipped-creature-item-refresh-send",
"conn_id", session.connID,
"channel_id", session.channel.ID,
"source", source,
"char_id", session.selectedCharacterID,
"msg_id", uint16(dnfenum.CmdPacketWalkoutPartyMember),
"classification", 0,
"list_type", currentSocketListEquipment,
"slot", 26,
"item_id", creature.itemID,
"creature_serial", creature.serialOrHandle,
"enchant_card_id", binary.LittleEndian.Uint32(entry.data[0x0E:0x12]),
"enchant_upgrade_count", entry.data[0x12],
"entry_count", 1,
"body_len", len(body),
"sequence", "existing_slot26_object_then_pet_only_op14_dynamic_state_refresh")
return s.sendGameUpperRawClass(
session,
uint16(dnfenum.CmdPacketWalkoutPartyMember),
body,
0,
)
}
// currentEquippedCreatureItemRefreshEntry must not use the legacy equipment
// raw row as a 0x77-byte base. The legacy card begins at +16 and its serial is
// repeated at +24; copying those bytes verbatim makes the card's third byte an
// enchant upgrade and serial<<16 a fake remaining duration (serial 41 appears
// as roughly 32 days). Build the list-3 row from zero and map only fields with
// proved equivalents in the current item-update grammar.
func currentEquippedCreatureItemRefreshEntry(equipped dnfrepo.EquipmentEntry) currentItemListEntry {
var entry currentItemListEntry
entry.patchCore(26, sceneInventoryUint32FromInt64(equipped.ItemID), currentItemListEquipmentInstance(equipped))
entry.data[0x0A] = currentItemListEquipmentExtData(equipped)
binary.LittleEndian.PutUint16(entry.data[0x0B:0x0D], currentItemListEquipmentDurability(equipped))
entry.data[0x0D] = currentItemListBindFlag(equipped.Bind, equipped.Extra)
binary.LittleEndian.PutUint32(entry.data[0x0E:0x12], currentEquippedCreatureEnchantCardID(equipped))
entry.data[0x12] = currentEquippedCreatureEnchantUpgrade(equipped)
entry.data[0x13] = currentEquippedCreatureEnchantAmplifyType(equipped)
binary.LittleEndian.PutUint16(entry.data[0x14:0x16], currentEquippedCreatureEnchantAmplifyValue(equipped))
expire := currentItemListEquipmentExpire(equipped)
binary.LittleEndian.PutUint32(
entry.data[currentPetRemainSecondsOffset:currentPetRemainSecondsOffset+4],
currentPetRemainingSecondsAt(expire, time.Now().UTC()),
)
binary.LittleEndian.PutUint32(
entry.data[currentItemListExpireTimeOffset:currentItemListExpireTimeOffset+4],
expire,
)
entry.data[0x57] = sceneInventoryExtraByte(equipped.Extra, "byte_57", "value_57")
entry.data[0x58] = sceneInventoryExtraByte(equipped.Extra, "byte_58", "value_58")
entry.data[0x59] = sceneInventoryExtraByte(equipped.Extra, "byte_59", "value_59")
entry.copyFixed(0x72, currentItemListFixedExtraBytes(equipped.Extra, 5, "tail_data_72", "tail72"))
return entry
}
func currentEquippedCreatureEnchantCardID(equipped dnfrepo.EquipmentEntry) uint32 {
if value, ok := currentPetEnchantExtraUint(equipped.Extra, "pet_enchant_card_item_id", "enchant_card_id", "value_a"); ok {
return sceneInventoryClampUint32(value)
}
if currentEquipmentEntryHasCurrentRaw77(equipped) {
return binary.LittleEndian.Uint32(equipped.RawEntry[0x0E:0x12])
}
if len(equipped.RawEntry) >= 20 {
return binary.LittleEndian.Uint32(equipped.RawEntry[16:20])
}
return 0
}
func mergeCurrentPetEnchantFromRecord(equipped *dnfrepo.EquipmentEntry, record dnfrepo.PetRecord) {
if equipped == nil || equipped.SlotIndex != 26 || equipped.ItemID <= 0 {
return
}
creature := currentEquippedCreatureFromEquipment(*equipped)
if !creature.valid() {
return
}
if currentEquippedCreatureEnchantCardID(*equipped) != 0 {
return
}
for _, pet := range record.Entries {
petSerial := pet.CreatureKey
if petSerial == 0 {
if parsed, err := strconv.ParseUint(strings.TrimSpace(pet.PetKey), 0, 32); err == nil {
petSerial = uint32(parsed)
}
}
if pet.ItemID != equipped.ItemID || petSerial != creature.serialOrHandle {
continue
}
card, ok := currentPetEnchantExtraUint(pet.Extra, "pet_enchant_card_item_id", "enchant_card_id", "value_a")
if !ok || card == 0 {
return
}
upgrade := uint64(0)
if value, found := currentPetEnchantExtraUint(pet.Extra, "pet_enchant_upgrade_count", "enchant_upgrade_count", "byte_12"); found {
upgrade = value
}
if equipped.Extra == nil {
equipped.Extra = make(map[string]string, 8)
}
cardValue := strconv.FormatUint(card, 10)
upgradeValue := strconv.FormatUint(upgrade, 10)
equipped.Extra["pet_enchant_card_item_id"] = cardValue
equipped.Extra["enchant_card_id"] = cardValue
equipped.Extra["value_a"] = cardValue
equipped.Extra["pet_enchant_upgrade_count"] = upgradeValue
equipped.Extra["enchant_upgrade_count"] = upgradeValue
equipped.Extra["byte_12"] = upgradeValue
if len(equipped.RawEntry) >= 0x15 {
equipped.RawEntry = append([]byte(nil), equipped.RawEntry...)
binary.LittleEndian.PutUint32(equipped.RawEntry[0x10:0x14], uint32(card))
equipped.RawEntry[0x14] = byte(min(upgrade, 0xFF))
}
return
}
}
func currentPetEnchantExtraUint(extra map[string]string, keys ...string) (uint64, bool) {
var first uint64
found := false
for _, key := range keys {
value, ok := sceneInventoryExtraUint(extra, key)
if !ok {
continue
}
if !found {
first, found = value, true
}
if value > 0 {
return value, true
}
}
return first, found
}
func currentEquippedCreatureEnchantUpgrade(equipped dnfrepo.EquipmentEntry) byte {
if value, ok := sceneInventoryExtraUint(
equipped.Extra,
"pet_enchant_upgrade_count",
"enchant_upgrade_count",
"byte_12",
"value_12",
); ok {
return byte(min(value, 0xFF))
}
if currentEquipmentEntryHasCurrentRaw77(equipped) {
return equipped.RawEntry[0x12]
}
if len(equipped.RawEntry) > 20 {
return equipped.RawEntry[20]
}
return 0
}
func currentEquippedCreatureEnchantAmplifyType(equipped dnfrepo.EquipmentEntry) byte {
if value, ok := sceneInventoryExtraUint(equipped.Extra, "byte_13", "value_13", "value_c"); ok {
return byte(min(value, 0xFF))
}
if currentEquipmentEntryHasCurrentRaw77(equipped) {
return equipped.RawEntry[0x13]
}
if len(equipped.RawEntry) > 21 {
return equipped.RawEntry[21]
}
return 0
}
func currentEquippedCreatureEnchantAmplifyValue(equipped dnfrepo.EquipmentEntry) uint16 {
if value, ok := sceneInventoryExtraUint(equipped.Extra, "marker_16", "marker16", "value_d"); ok {
return uint16(min(value, 0xFFFF))
}
if currentEquipmentEntryHasCurrentRaw77(equipped) {
return binary.LittleEndian.Uint16(equipped.RawEntry[0x14:0x16])
}
if len(equipped.RawEntry) >= 24 {
return binary.LittleEndian.Uint16(equipped.RawEntry[22:24])
}
return 0
}