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

187 lines
8.1 KiB
Go

package dnfbridge
import "longheng.io/server/internal/modules/dnf/dnfenum"
const (
currentDungeonPlayResultClientRankPointOffset = 10
currentDungeonCharacterStateMsgID = uint16(dnfenum.CmdPacketFinishLoading)
)
// sendCurrentDungeonSettlementResultsLocked sends only a previously frozen,
// committed plan. It never calculates or fills a reward while handling op46.
// The caller owns session.dungeon.mu.
func (s *Service) sendCurrentDungeonSettlementResultsLocked(
session *gameSession,
runtime *runtimeDungeonState,
source string,
) error {
if session == nil || runtime == nil || session.dungeon.runtime != runtime ||
!runtime.settlementPlayResultReceived ||
runtime.settlementPhase < currentDungeonSettlementPhaseClearEnabled ||
runtime.settlementPhase >= currentDungeonSettlementPhaseEnding {
return nil
}
plan := runtime.settlementResultPlan
if plan == nil {
s.logGameEvent(session, "game-dungeon-settlement-results-blocked",
"char_id", session.selectedCharacterID,
"dungeon_id", runtime.Dungeon.ID,
"source", source,
"reason", "committed_dungeon_reward_snapshot_missing")
return nil
}
if plan.CharacterID != session.selectedCharacterID ||
!dungeonRuntimeOwnsCharacter(runtime, plan.CharacterID) ||
plan.CompletionKey == "" || plan.Source == "" ||
(runtime.clearMapCompletionKey != "" && plan.CompletionKey != runtime.clearMapCompletionKey) {
s.logGameEvent(session, "game-dungeon-settlement-results-blocked",
"char_id", session.selectedCharacterID,
"plan_char_id", plan.CharacterID,
"dungeon_id", runtime.Dungeon.ID,
"plan_completion_key", plan.CompletionKey,
"runtime_completion_key", runtime.clearMapCompletionKey,
"source", source,
"reason", "committed_settlement_plan_owner_mismatch")
return nil
}
if len(runtime.settlementPlayResultBody) <= currentDungeonPlayResultClientRankPointOffset ||
runtime.settlementPlayResultBody[currentDungeonPlayResultClientRankPointOffset] != plan.ClientRankPoint {
s.logGameEvent(session, "game-dungeon-settlement-results-blocked",
"char_id", session.selectedCharacterID,
"dungeon_id", runtime.Dungeon.ID,
"request_body_len", len(runtime.settlementPlayResultBody),
"planned_client_rank_point", plan.ClientRankPoint,
"source", source,
"reason", "op46_client_rank_point_conflicts_with_committed_plan")
return nil
}
if len(plan.PlayResultBody) == 0 || len(plan.CharacterBody) != currentFinishLoadingCharacterStateBodySize ||
len(plan.ClearRewardBody) < currentDungeonClearRewardMinimumBodySize ||
!currentDungeonTowerPlanValid(runtime, plan) {
s.logGameEvent(session, "game-dungeon-settlement-results-blocked",
"char_id", session.selectedCharacterID,
"dungeon_id", runtime.Dungeon.ID,
"op34_body_len", len(plan.PlayResultBody),
"op37_body_len", len(plan.CharacterBody),
"op35_body_len", len(plan.ClearRewardBody),
"source", source,
"reason", "frozen_settlement_packet_plan_shape_invalid")
return nil
}
if !runtime.settlementClearRewardSent && plan.TowerRewardMsgID == 0 {
ready, err := s.prepareRuntimePartyDungeonCardRewardsLocked(session, runtime)
if err != nil || !ready {
return err
}
}
if !runtime.settlementResultNoticeSent {
if err := s.sendGameUpperRawClass(session, currentDungeonPlayResultNoticeMsgID, plan.PlayResultBody, 0); err != nil {
return err
}
runtime.settlementResultNoticeSent = true
s.logGameEvent(session, "game-dungeon-settlement-result-op34-sent",
"char_id", plan.CharacterID,
"dungeon_id", runtime.Dungeon.ID,
"completion_key", plan.CompletionKey,
"body_len", len(plan.PlayResultBody),
"body_source", "current_exe_sub_1D3BAE0_committed_runtime_result")
}
if !runtime.settlementCharacterStateSent {
if err := s.sendGameUpperRawClass(session, currentDungeonCharacterStateMsgID, plan.CharacterBody, 0); err != nil {
return err
}
runtime.settlementCharacterStateSent = true
s.logGameEvent(session, "game-dungeon-settlement-character-op37-sent",
"char_id", plan.CharacterID,
"dungeon_id", runtime.Dungeon.ID,
"completion_key", plan.CompletionKey,
"body_len", len(plan.CharacterBody),
"body_source", "current_exe_sub_1D78240_post_commit_character_snapshot")
}
if !runtime.settlementClearRewardSent {
if err := s.sendGameUpperRawClass(session, currentDungeonClearRewardMsgID, plan.ClearRewardBody, 0); err != nil {
return err
}
runtime.settlementClearRewardSent = true
s.logGameEvent(session, "game-dungeon-settlement-clear-reward-op35-sent",
"char_id", plan.CharacterID,
"dungeon_id", runtime.Dungeon.ID,
"completion_key", plan.CompletionKey,
"reward_source", plan.Source,
"body_len", len(plan.ClearRewardBody),
"body_source", "current_exe_sub_1D4D380_committed_reward_snapshot")
// 同公会成员组队通关地下城,可以获得1点公会贡献 (help text 76513).
// Paused on the user's request (2026-09-12): only 签到 pays 公会贡献 for now.
// Re-enable by uncommenting; the settlement is per character, so each guild
// member of the party would be paid by its own clear and never twice for one
// run.
// s.grantCurrentGuildPartyMileage(session, "current_dungeon_clear")
}
// 公会贡献度 coins delivered by this clear. Sent once, before the wedding
// capsule and tower rows, which is where upstream places it. The guard matters
// because the result send can run more than once for one settlement.
if !runtime.guildContributionSent {
if err := s.sendCurrentGuildContributionUpdates(session, runtime.guildContributionUpdates); err != nil {
return err
}
runtime.guildContributionSent = true
}
if len(plan.WeddingCapsuleBody) != 0 && !runtime.settlementWeddingCapsuleSent {
// 1016 号:已婚角色通关推荐副本后回婚姻成长胶囊进度。只发一次;
// 发送失败不置位,下一次结算会重试(进度取自已提交的角色记录,
// 重发不会重复计入)。
if err := s.sendGameUpperRawClass(session, 1016, plan.WeddingCapsuleBody, 0); err != nil {
return err
}
runtime.settlementWeddingCapsuleSent = true
}
if plan.TowerRewardMsgID != 0 && !runtime.settlementTowerRewardSent {
// The two daily pools are consumed by this clear; republish both native
// counters before the window so the tooltip agrees with the ledger.
if !runtime.settlementTowerCountsSent {
ctx, cancel := s.currentGuildContext()
err := s.sendCurrentAdventureTowerCounts(ctx, session, "tower_clear_committed")
cancel()
if err != nil {
return err
}
runtime.settlementTowerCountsSent = true
}
if !runtime.settlementTowerInventorySent {
if len(runtime.towerRewardUpdates) != 0 {
if err := s.sendGameUpperRawClass(session, 14, buildCurrentItemUpdateBody(0, runtime.towerRewardUpdates), 0); err != nil {
return err
}
}
runtime.settlementTowerInventorySent = true
}
if err := s.sendGameUpperRawClass(session, plan.TowerRewardMsgID, plan.TowerRewardBody, 0); err != nil {
return err
}
runtime.settlementTowerRewardSent = true
s.logGameEvent(session, "game-dungeon-tower-clear-reward-sent",
"char_id", plan.CharacterID, "dungeon_id", runtime.Dungeon.ID,
"completion_key", plan.CompletionKey, "msg_id", plan.TowerRewardMsgID,
"body_len", len(plan.TowerRewardBody),
"body_source", "native_ten_reward_slots_with_committed_tower_rewards_and_empty_sentinels")
}
if len(plan.DungeonPermissionBody) != 0 && !runtime.settlementDungeonPermissionSent {
if err := s.sendGameUpperRawClass(session, currentDungeonResourceStateMsgID, plan.DungeonPermissionBody, 0); err != nil {
return err
}
runtime.settlementDungeonPermissionSent = true
s.logGameEvent(session, "game-dungeon-settlement-permission-op5-sent",
"char_id", plan.CharacterID,
"dungeon_id", runtime.Dungeon.ID,
"completion_key", plan.CompletionKey,
"body_len", len(plan.DungeonPermissionBody),
"body_source", "current_exe_sub_1D37AC0_u16_count_u32_key_u8_state",
"state_source", "persisted_dungeon_clear_state_after_settlement")
}
runtime.advanceSettlementPhase(currentDungeonSettlementPhaseResultShown)
// Publish the lucky-star result committed with this settlement.
s.sendSuitableDungeonLuckyStarState(session, runtime)
return nil
}