Files
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

234 lines
8.8 KiB
Go

package dnfbridge
import (
"context"
"encoding/binary"
"errors"
"fmt"
"math"
"strings"
"longheng.io/server/internal/modules/dnf/dnfenum"
dnfproto "longheng.io/server/internal/modules/dnf/protocol"
dnfquest "longheng.io/server/internal/modules/dnf/quest"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
)
const currentGiveUpQuestRequestBodySize = 4
// currentGiveUpQuestReplayKey scopes the destructive operation receipt to one
// selected character and one TCP session. The first ACK removes the row from
// the current client's task UI, so a duplicate success receipt is unsafe.
type currentGiveUpQuestReplayKey struct {
characterID uint16
questID uint16
}
func newCurrentGiveUpQuestReplayKey(characterID uint16, request dnfquest.QuestIDRequest) currentGiveUpQuestReplayKey {
return currentGiveUpQuestReplayKey{characterID: characterID, questID: request.QuestID}
}
func (session *gameSession) currentGiveUpQuestReplaySuppressed(key currentGiveUpQuestReplayKey) bool {
if session == nil {
return false
}
session.questReplay.giveUpMu.Lock()
defer session.questReplay.giveUpMu.Unlock()
_, ok := session.questReplay.giveUpAnswered[key]
return ok
}
func (session *gameSession) suppressCurrentGiveUpQuestReplay(key currentGiveUpQuestReplayKey) {
if session == nil {
return
}
session.questReplay.giveUpMu.Lock()
defer session.questReplay.giveUpMu.Unlock()
if session.questReplay.giveUpAnswered == nil {
session.questReplay.giveUpAnswered = make(map[currentGiveUpQuestReplayKey]struct{}, 1)
}
session.questReplay.giveUpAnswered[key] = struct{}{}
}
func (session *gameSession) clearCurrentGiveUpQuestReplay(key currentGiveUpQuestReplayKey) {
if session == nil {
return
}
session.questReplay.giveUpMu.Lock()
defer session.questReplay.giveUpMu.Unlock()
delete(session.questReplay.giveUpAnswered, key)
}
func (s *Service) handleCurrentGiveUpQuest(session *gameSession, body []byte) error {
if session == nil || session.selectedCharacterID == 0 {
return nil
}
opcode := uint16(dnfenum.CmdPacketGiveupQuest)
if len(body) != currentGiveUpQuestRequestBodySize || binary.LittleEndian.Uint16(body[:2]) != opcode {
s.logGameEvent(session, "game-upper-give-up-quest-blocked",
"body_len", len(body),
"reason", "current_exe_op32_requires_exact_echo_and_quest_id")
return nil
}
request, err := dnfquest.DecodeQuestIDRequest(body)
if err != nil || request.QuestID == 0 {
s.logGameEvent(session, "game-upper-give-up-quest-blocked",
"body_len", len(body),
"reason", "request_decode_failed_or_zero_quest_id",
"error", err)
return nil
}
replayKey := newCurrentGiveUpQuestReplayKey(session.selectedCharacterID, request)
if session.currentGiveUpQuestReplaySuppressed(replayKey) {
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
defer cancel()
catalog, err := s.loadQuestCatalog(ctx)
if err != nil {
s.logGameEvent(session, "game-upper-give-up-quest-blocked",
"quest_id", request.QuestID,
"reason", "quest_catalog_unavailable",
"error", err)
return nil
}
repositories, ok := s.repositoryGroup()
if !ok || repositories.Character == nil || repositories.Quest == nil {
s.logGameEvent(session, "game-upper-give-up-quest-blocked",
"quest_id", request.QuestID,
"reason", "character_or_quest_repository_unavailable")
return nil
}
owner, err := dnfquest.NewOwner(repositories)
if err != nil {
return fmt.Errorf("create quest give-up owner: %w", err)
}
result, err := owner.ApplyGiveUpSettlement(ctx, catalog, dnfquest.GiveUpInput{
AccountID: s.accountIDForSession(session),
SelectedCharacterID: session.selectedCharacterID,
QuestID: request.QuestID,
IsQuestItem: s.currentGiveUpQuestItemClassifier(session),
})
if err != nil {
s.logGameEvent(session, "game-upper-give-up-quest-blocked",
"quest_id", request.QuestID,
"reason", "pvf_db_give_up_preflight_or_persist_failed",
"error", err)
switch {
case errors.Is(err, dnfquest.ErrQuestNotActive), errors.Is(err, dnfquest.ErrQuestDefinitionMissing):
// 86JP's matching current protocol uses error 19 when the active
// quest cannot be found. No mutation has occurred on this path.
return s.sendGameUpperFailure(session, opcode, 19)
case errors.Is(err, dnfquest.ErrQuestCannotGiveUp):
return s.sendGameUpperFailure(session, opcode, 20)
default:
// Persistence uncertainty must never produce a success receipt.
return nil
}
}
if result.ReclaimSkipped {
s.logGameEvent(session, "game-upper-give-up-quest-reclaim-skipped",
"quest_id", result.QuestID,
"reason", result.ReclaimSkipReason)
}
var payload packetWriter
payload.writeUint16(result.QuestID)
s.logGameEvent(session, "game-upper-give-up-quest-success-send",
"quest_id", result.QuestID,
"msg_id", opcode,
"classification", dnfproto.DefaultChannelClassification,
"plain_body_len", 3,
"reclaimed_slots", len(result.Reclaimed),
"body_source", "current_exe_sub_1D1A4B0_u16_success_reader_after_persisted_removal")
if err := s.sendGameUpperSuccess(session, opcode, payload.bytes()); err != nil {
return err
}
if err := s.sendCurrentGiveUpQuestReclaimedItemUpdate(session, result); err != nil {
return err
}
session.clearCurrentAcceptQuestReplay(newCurrentAcceptQuestReplayKey(session.selectedCharacterID, request))
session.suppressCurrentGiveUpQuestReplay(replayKey)
return nil
}
// currentGiveUpQuestItemClassifier 返回"该道具是否属于 PVF `[quest]` 家族"的判定函数。
// `[quest]` 家族就是落在任务栏(list 0 slot 177..232)、不可交易的**任务专属**道具,
// 与 dungeonDropStackableSlotRange 的落格规则同源。
//
// 物品目录不可用时返回 nil:放弃照常成功,只是不回收任何东西 —— 与参考端在物品
// 元数据不可用时的降级一致,绝不因为"回收判定不可用"而挡住玩家放弃任务。
func (s *Service) currentGiveUpQuestItemClassifier(session *gameSession) func(int64) bool {
catalog, err := s.currentPVFItemCatalog()
if err != nil || catalog == nil {
s.logGameEvent(session, "game-upper-give-up-quest-item-catalog-unavailable",
"reason", "reclaim_classifier_unavailable_abandon_still_allowed",
"error", err)
return nil
}
return func(itemID int64) bool {
if itemID <= 0 || itemID > math.MaxUint32 {
return false
}
definition, err := catalog.ResolveItem(uint32(itemID))
if err != nil {
return false
}
return strings.HasPrefix(normalizeDungeonDropStackableType(definition.StackableType), "[quest]")
}
}
var errCurrentGiveUpQuestReclaimShape = errors.New("current EXE give-up reclaim row shape is invalid")
// sendCurrentGiveUpQuestReclaimedItemUpdate 把回收后的背包行推给客户端
// (与任务交付扣除材料走同一种 op14 class-0 增量行)。不回收时不发任何包,
// 保持改动前的行为。
func (s *Service) sendCurrentGiveUpQuestReclaimedItemUpdate(session *gameSession, result dnfquest.GiveUpResult) error {
if len(result.Reclaimed) == 0 {
return nil
}
entries := make([]currentItemListEntry, 0, len(result.Reclaimed))
for _, reclaimed := range result.Reclaimed {
if reclaimed.SlotIndex > math.MaxInt16 || reclaimed.ItemID <= 0 ||
reclaimed.ItemID > math.MaxUint32 || reclaimed.Delta <= 0 || reclaimed.PostCount < 0 {
return errCurrentGiveUpQuestReclaimShape
}
var entry currentItemListEntry
if reclaimed.PostCount <= 0 {
// 整行删掉:客户端按 slot + 0xFFFFFFFF 移除该格。
entry.patchCore(int16(reclaimed.SlotIndex), math.MaxUint32, 0)
entries = append(entries, entry)
continue
}
if len(reclaimed.RawEntry) == currentItemListEntryWireSize &&
binary.LittleEndian.Uint16(reclaimed.RawEntry[0:2]) == reclaimed.SlotIndex &&
binary.LittleEndian.Uint32(reclaimed.RawEntry[2:6]) == uint32(reclaimed.ItemID) {
copy(entry.data[:], reclaimed.RawEntry)
entries = append(entries, entry)
continue
}
// 原始行不可用时用提交后的快照重建该行,绝不拿旧行糊弄客户端。
if !result.HasPostCommitInventory {
return errCurrentGiveUpQuestReclaimShape
}
stack, found := result.PostCommitInventory.Slots[reclaimed.SlotKey]
if !found || stack.ItemID != reclaimed.ItemID || stack.Count != reclaimed.PostCount {
return errCurrentGiveUpQuestReclaimShape
}
entries = append(entries, currentItemListEntryFromStack(
dnfrepo.MainInventoryListType, int16(reclaimed.SlotIndex), stack))
}
body := buildCurrentItemUpdateBody(0, entries)
s.logGameEvent(session, "game-upper-give-up-quest-reclaim-item-update-send",
"quest_id", result.QuestID,
"msg_id", uint16(dnfenum.CmdPacketWalkoutPartyMember),
"classification", 0,
"list_type", 0,
"entry_count", len(entries),
"entry_size", currentItemListEntryWireSize,
"body_len", len(body),
"body_source", "committed_give_up_quest_reclaim_op14_raw77")
return s.sendGameUpperRawClass(session, uint16(dnfenum.CmdPacketWalkoutPartyMember), body, 0)
}