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

204 lines
7.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package dnfbridge
import (
"context"
"longheng.io/server/internal/modules/dnf/dnfenum"
"longheng.io/server/internal/modules/dnf/worldmap"
)
// 超时空:**队长按下撤退(无包体 op42)时,当前这支临时队伍一起回城。**
//
// 操作员 2026-10-02 的定案:
//
// 「然后是战况表撤退 在组成临时队伍后 队长点击战况表的撤退 当前临时队伍所有人
// 都应该直接回城」
//
// 真机证据(raid 12058625,分片 20261002-006,两两进罗什那次):
//
// 10:29:10 game-000141 (char 1 = 礼白666,party_id=22 的队长) 发 op42
// ENUM_CMDPACKET_GIVEUP_GAME
// → game-dungeon-full-town-route-send char_id=1 dungeon_id=4978
// source=current_exe_bodyless_op42_active_run_giveup_abandoned_run_town_return
// → 只有队长一个人回了城;同队的 char 21 留在那一格里
// 10:29:33 game-000148 (char 21,队员) 连点 op72{1,2}(战况表那个按钮)
// → game-dungeon-card-exit-blocked request_value_a=1 request_value_b=2
// reason=settlement_result_not_ready —— 这一窗口里被拒了 119 次
// (队员点它不是队长的活,见 leadRuntimePartyHomeFromActiveRun 的守卫)
// → 23 秒后他自己又按了一次 op42 才结束
//
// 全日志(2026-10-01 + 2026-10-02)里客户端从来没有发过 661 RAID_DO_BEHAVIOR /
// 667 SET_RAID_WAITING / 670 MODIFY_RAID_INFO 中的任何一个,所以客户端那个按钮
// 就是 op42;而 op72{1,2} 是**队员**手里的同一个战况表按钮。
//
// 现成的整队通道早就有:`sendCurrentActiveDungeonReturnToTownKeepingParty`
// (战况表队长带队回城 / 攻坚阶段收尾都用它,回城后编制保留)。op72 那条路
// (`leadRuntimePartyHomeFromActiveRun`) 已经会遍历全队 —— 缺的只是
// **队长这一脚 op42 也走整队**。
//
// 作用域:只加超时空。安东/卢克一个字不改(它们的小队编制与进图模型不同,
// 而且它们从没报过这个症状)。
// runtimeFiendWarPartyRetreatPlan 是"这一次 op42 要不要把同队的人一起带回家"的
// 快照。必须在**队长自己那一趟回城之前**抓下来:那一趟会把 runtime 上的攻坚绑定
// 清掉,之后就判不出他当时属于哪支临时队伍了。
type runtimeFiendWarPartyRetreatPlan struct {
armed bool
raidKey uint32
partyID uint16
leader uint16
members []uint16
}
// runtimeFiendWarPartyRetreatPlanFor 判断这一脚是不是"队长在临时队伍里按下撤退"。
// 调用者持 session.dungeon.mu(op42 处理体里就是那个位置)。
func (s *Service) runtimeFiendWarPartyRetreatPlanFor(
session *gameSession,
runtime *runtimeDungeonState,
) runtimeFiendWarPartyRetreatPlan {
if s == nil || session == nil || runtime == nil {
return runtimeFiendWarPartyRetreatPlan{}
}
if !isCurrentFiendWarRaidChannel(session) {
return runtimeFiendWarPartyRetreatPlan{}
}
owner := runtime.partyDungeonOwner
if owner == nil || runtime.raidDungeon.RaidKey == 0 {
return runtimeFiendWarPartyRetreatPlan{}
}
owner.mu.Lock()
leader, partyID, retired := owner.leaderID, owner.partyID, owner.retired
members := make([]uint16, 0, len(owner.members))
for _, member := range owner.members {
members = append(members, member.userID)
}
owner.mu.Unlock()
// 只有队长的这一按拉全队;队员自己的退出仍旧只退他自己。
if retired || partyID == 0 || leader == 0 || leader != session.selectedCharacterID {
return runtimeFiendWarPartyRetreatPlan{}
}
if len(members) == 0 {
return runtimeFiendWarPartyRetreatPlan{}
}
return runtimeFiendWarPartyRetreatPlan{
armed: true,
raidKey: runtime.raidDungeon.RaidKey,
partyID: partyID,
leader: leader,
members: members,
}
}
// runRuntimeFiendWarPartyRetreat 把同队其余成员各自送回城。
//
// 队长的自己那一趟由调用方(op42 处理体)走原有通道发,这里**只处理别人**:
// 不重复、也不改动队长那一脚已经发出去的报文序列。
//
// 只带**还在这一场攻坚里、而且还在自己的副本里**的人:已经站在城里的成员不动,
// 别人的副本不碰。占着别人 dungeon 锁的人跳过而不是等待 —— 两个会话互相等会死锁
// (同 leadRuntimePartyHomeFromActiveRun 里那条纪律)。
func (s *Service) runRuntimeFiendWarPartyRetreat(plan runtimeFiendWarPartyRetreatPlan, source string) {
if s == nil || !plan.armed {
return
}
returned := 0
for _, charID := range plan.members {
if charID == 0 || charID == plan.leader {
continue
}
target, online := s.onlineGameSession(charID)
if !online || target == nil {
continue
}
if !target.dungeon.mu.TryLock() {
s.logGameEvent(nil, "game-fiendwar-party-retreat-skipped-busy-session",
"raid_key", plan.raidKey,
"party_id", plan.partyID,
"leader_char_id", plan.leader,
"char_id", charID,
"source", source)
continue
}
targetRuntime := target.dungeon.runtime
inside, active := false, false
if targetRuntime != nil &&
targetRuntime.raidDungeon.RaidKey == plan.raidKey &&
dungeonRuntimeOwnsCharacter(targetRuntime, charID) {
inside = true
active = targetRuntime.Session.Snapshot().Run.Status == worldmap.DungeonRunActive
}
if inside && active {
// 这一场对他也结束了:标记放弃 + 保留编制(和战况表带队回城同一条路)。
targetRuntime.raidDungeon.abandonedNotified = true
targetRuntime.townReturnKeepsSquadLabel = true
}
target.dungeon.mu.Unlock()
if !inside {
continue
}
if !active {
// 已经通关的成员走他自己的结算/回城链,别抢在他结算前面把他拖回城。
s.logGameEvent(target, "game-fiendwar-party-retreat-member-skipped",
"raid_key", plan.raidKey,
"party_id", plan.partyID,
"leader_char_id", plan.leader,
"char_id", charID,
"dungeon_id", targetRuntime.Dungeon.ID,
"run_status", targetRuntime.Session.Snapshot().Run.Status,
"reason", "member_run_is_not_active",
"source", source)
continue
}
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
transition, err := s.prepareCurrentDungeonTownTransitionForSession(ctx, target, charID)
cancel()
if err != nil {
s.logGameEvent(target, "game-fiendwar-party-retreat-member-blocked",
"raid_key", plan.raidKey,
"party_id", plan.partyID,
"leader_char_id", plan.leader,
"char_id", charID,
"dungeon_id", targetRuntime.Dungeon.ID,
"reason", "town_transition_unavailable_for_party_retreat",
"error", err,
"source", source)
continue
}
if err := s.sendCurrentActiveDungeonReturnToTownKeepingParty(
target,
targetRuntime,
transition,
uint16(dnfenum.CmdPacketGiveupGame),
source,
); err != nil {
s.logGameEvent(target, "game-fiendwar-party-retreat-member-blocked",
"raid_key", plan.raidKey,
"party_id", plan.partyID,
"leader_char_id", plan.leader,
"char_id", charID,
"dungeon_id", targetRuntime.Dungeon.ID,
"reason", "party_retreat_town_return_failed",
"error", err,
"source", source)
continue
}
returned++
s.logGameEvent(target, "game-fiendwar-party-retreat-member-returned",
"raid_key", plan.raidKey,
"party_id", plan.partyID,
"leader_char_id", plan.leader,
"char_id", charID,
"dungeon_id", targetRuntime.Dungeon.ID,
"source", source,
"reason", "the_leader_retreated_so_the_temporary_party_goes_home_together")
}
if returned > 0 {
s.logGameEvent(nil, "game-fiendwar-party-retreat-committed",
"raid_key", plan.raidKey,
"party_id", plan.partyID,
"leader_char_id", plan.leader,
"returned_members", returned,
"source", source)
}
}