服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
229 lines
8.0 KiB
Go
229 lines
8.0 KiB
Go
package dnfbridge
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
|
|
"longheng.io/server/internal/modules/dnf/dnfenum"
|
|
)
|
|
|
|
// The caller holds only its own dungeon lock. Party members publish snapshots
|
|
// under owner.mu; continuations run in each peer's existing session mailbox.
|
|
func (s *Service) prepareRuntimePartyDungeonCardRewardsLocked(session *gameSession, runtime *runtimeDungeonState) (bool, error) {
|
|
owner := runtime.partyDungeonOwner
|
|
if owner == nil {
|
|
return true, nil
|
|
}
|
|
plan := runtime.settlementResultPlan
|
|
if plan == nil || plan.clearRewardSnapshot == nil {
|
|
return false, errors.New("party dungeon result has no owned reward snapshot")
|
|
}
|
|
owner.mu.Lock()
|
|
slot := byte(0xff)
|
|
expected := uint8(0)
|
|
for _, member := range owner.members {
|
|
expected |= uint8(1) << member.slot
|
|
if member.userID == session.selectedCharacterID && member.generation == session.characterGeneration {
|
|
slot = member.slot
|
|
}
|
|
}
|
|
if owner.retired || slot >= dungeonCardSlotsPerSide || !runtime.partyMemberIndexed || runtime.partyMemberIndex != slot ||
|
|
owner.cardRewardDepartedSlots&(uint8(1)<<slot) != 0 {
|
|
owner.mu.Unlock()
|
|
return false, errors.New("party dungeon reward session no longer owns its member slot")
|
|
}
|
|
bit := uint8(1) << slot
|
|
if !owner.cardRewardsFrozen && owner.cardRewardReadySlots&bit == 0 {
|
|
for kind, table := range [3][currentDungeonClearRewardGroupCount][]currentDungeonClearRewardPair{
|
|
plan.clearRewardSnapshot.CardSlots, plan.clearRewardSnapshot.AuxGroupsA, plan.clearRewardSnapshot.AuxGroupsB,
|
|
} {
|
|
owner.cardRewards[kind][slot] = append([]currentDungeonClearRewardPair(nil), table[slot]...)
|
|
}
|
|
owner.cardRewardReadySlots |= bit
|
|
}
|
|
expected &^= owner.cardRewardDepartedSlots
|
|
newlyReady := !owner.cardRewardsFrozen && owner.cardRewardReadySlots&expected == expected
|
|
if newlyReady {
|
|
owner.cardRewardsFrozen = true
|
|
}
|
|
ready := owner.cardRewardsFrozen
|
|
var groups [3][currentDungeonClearRewardGroupCount][]currentDungeonClearRewardPair
|
|
if ready {
|
|
for kind := range groups {
|
|
for i := range groups[kind] {
|
|
groups[kind][i] = append([]currentDungeonClearRewardPair(nil), owner.cardRewards[kind][i]...)
|
|
}
|
|
}
|
|
}
|
|
owner.mu.Unlock()
|
|
if !ready {
|
|
return false, nil
|
|
}
|
|
snapshot := *plan.clearRewardSnapshot
|
|
snapshot.CardSlots, snapshot.AuxGroupsA, snapshot.AuxGroupsB = groups[0], groups[1], groups[2]
|
|
body, err := buildCurrentDungeonClearRewardBody(snapshot)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
plan.ClearRewardBody = body
|
|
if newlyReady {
|
|
if err := s.resumeRuntimePartyDungeonCardResults(session, owner); err != nil {
|
|
s.logGameEvent(session, "game-party-card-result-resume-failed", "error", err)
|
|
}
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (s *Service) resumeRuntimePartyDungeonCardResults(source *gameSession, owner *runtimePartyDungeonOwner) error {
|
|
var result error
|
|
owner.mu.Lock()
|
|
departed := owner.cardRewardDepartedSlots
|
|
owner.mu.Unlock()
|
|
for _, member := range owner.roster() {
|
|
if departed&(uint8(1)<<member.slot) != 0 {
|
|
continue
|
|
}
|
|
identity, current := s.onlineGameSessionCharacterSnapshot(member.userID)
|
|
if !current || identity.session == nil || identity.session == source || identity.generation != member.generation {
|
|
continue
|
|
}
|
|
peer := identity.session
|
|
err := s.postGameSessionCharacterEvent(peer, "party-dungeon-card-rewards-ready", identity.character, identity.generation, func() error {
|
|
peer.dungeon.mu.Lock()
|
|
defer peer.dungeon.mu.Unlock()
|
|
runtime := peer.dungeon.runtime
|
|
if runtime == nil || runtime.partyDungeonOwner != owner || runtime.settlementClearRewardSent {
|
|
return nil
|
|
}
|
|
return s.sendCurrentDungeonSettlementResultsLocked(peer, runtime, "party_free_reward_table_ready")
|
|
})
|
|
result = errors.Join(result, err)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// A disconnected/changing character must not hold the remaining members at
|
|
// the reward barrier. Already frozen public rows remain stable for replays.
|
|
func (s *Service) detachRuntimePartyDungeonCardRewardsLocked(session *gameSession, runtime *runtimeDungeonState) {
|
|
if runtime == nil || runtime.partyDungeonOwner == nil {
|
|
return
|
|
}
|
|
owner := runtime.partyDungeonOwner
|
|
owner.mu.Lock()
|
|
if owner.cardRewardsFrozen {
|
|
owner.mu.Unlock()
|
|
return
|
|
}
|
|
for _, member := range owner.members {
|
|
if member.userID == session.selectedCharacterID && member.generation == session.characterGeneration {
|
|
owner.cardRewardDepartedSlots |= uint8(1) << member.slot
|
|
}
|
|
}
|
|
owner.mu.Unlock()
|
|
if err := s.resumeRuntimePartyDungeonCardResults(session, owner); err != nil {
|
|
s.logGameEvent(session, "game-party-card-result-resume-failed", "error", err)
|
|
}
|
|
}
|
|
|
|
func runtimePartyDungeonCardMemberIndex(runtime *runtimeDungeonState) byte {
|
|
if runtime != nil && runtime.partyDungeonOwner != nil && runtime.partyMemberIndexed {
|
|
return runtime.partyMemberIndex
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// publishRuntimePartyDungeonCardSelection mirrors the same-version party
|
|
// settlement projection: the selecting member owns the durable reward, while
|
|
// every other admitted member receives the public class1/op71 flip using the
|
|
// selector's stable 1P..4P slot. A peer that has not consumed op70 yet queues
|
|
// the flip so packet order remains op70 before op71 on that connection.
|
|
func (s *Service) publishRuntimePartyDungeonCardSelection(
|
|
source *gameSession,
|
|
runtime *runtimeDungeonState,
|
|
side dungeonCardSide,
|
|
memberIndex byte,
|
|
body []byte,
|
|
) error {
|
|
if s == nil || source == nil || runtime == nil || runtime.partyDungeonOwner == nil ||
|
|
side >= dungeonCardSideCount || memberIndex >= dungeonCardSlotsPerSide || len(body) == 0 {
|
|
return nil
|
|
}
|
|
owner := runtime.partyDungeonOwner
|
|
member, current := owner.member(source.selectedCharacterID, source.characterGeneration)
|
|
if !current || member.slot != memberIndex {
|
|
return fmt.Errorf("party dungeon card source %d is not stable slot %d", source.selectedCharacterID, memberIndex)
|
|
}
|
|
for _, peer := range owner.roster() {
|
|
if peer.userID == source.selectedCharacterID && peer.generation == source.characterGeneration {
|
|
continue
|
|
}
|
|
identity, online := s.onlineGameSessionCharacterSnapshot(peer.userID)
|
|
if !online || identity.session == nil || identity.generation != peer.generation {
|
|
return fmt.Errorf("party dungeon card member %d session generation changed", peer.userID)
|
|
}
|
|
peerSession := identity.session
|
|
peerBody := append([]byte(nil), body...)
|
|
if err := s.postGameSessionCharacterEvent(
|
|
peerSession,
|
|
"party-dungeon-card-selection",
|
|
identity.character,
|
|
identity.generation,
|
|
func() error {
|
|
peerSession.dungeon.mu.Lock()
|
|
defer peerSession.dungeon.mu.Unlock()
|
|
peerRuntime := peerSession.dungeon.runtime
|
|
if peerRuntime == nil || peerRuntime.partyDungeonOwner != owner {
|
|
return nil
|
|
}
|
|
if peerRuntime.partyCardSelectionSeen[side][memberIndex] {
|
|
return nil
|
|
}
|
|
if !peerRuntime.settlementCardLayoutSent {
|
|
peerRuntime.partyCardSelectionPending = append(
|
|
peerRuntime.partyCardSelectionPending,
|
|
peerBody,
|
|
)
|
|
peerRuntime.partyCardSelectionSeen[side][memberIndex] = true
|
|
return nil
|
|
}
|
|
if err := s.sendGameUpperRawClass(
|
|
peerSession,
|
|
uint16(dnfenum.CmdPacketSelectCard),
|
|
peerBody,
|
|
currentDungeonCardResponseClass,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
peerRuntime.partyCardSelectionSeen[side][memberIndex] = true
|
|
return nil
|
|
},
|
|
); err != nil {
|
|
return fmt.Errorf("publish party dungeon card to member %d: %w", peer.userID, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) flushRuntimePartyDungeonCardSelectionsLocked(
|
|
session *gameSession,
|
|
runtime *runtimeDungeonState,
|
|
) error {
|
|
if s == nil || session == nil || runtime == nil || session.dungeon.runtime != runtime ||
|
|
!runtime.settlementCardLayoutSent || len(runtime.partyCardSelectionPending) == 0 {
|
|
return nil
|
|
}
|
|
for len(runtime.partyCardSelectionPending) != 0 {
|
|
body := runtime.partyCardSelectionPending[0]
|
|
if err := s.sendGameUpperRawClass(
|
|
session,
|
|
uint16(dnfenum.CmdPacketSelectCard),
|
|
append([]byte(nil), body...),
|
|
currentDungeonCardResponseClass,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
runtime.partyCardSelectionPending = runtime.partyCardSelectionPending[1:]
|
|
}
|
|
return nil
|
|
}
|