DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,649 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/alignedcmd"
|
||||
"longheng.io/server/internal/modules/dnf/dnfenum"
|
||||
dnfaltar "longheng.io/server/internal/modules/dnf/dungeonaltar"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
"longheng.io/server/internal/modules/dnf/worldmap"
|
||||
)
|
||||
|
||||
var errCurrentDungeonAltarSettlementInvalid = errors.New("current dungeon altar settlement is invalid")
|
||||
|
||||
type currentDungeonAltarSettlementPhase uint8
|
||||
|
||||
const (
|
||||
currentDungeonAltarSettlementPrepared currentDungeonAltarSettlementPhase = iota
|
||||
currentDungeonAltarSettlementRankingShown
|
||||
currentDungeonAltarSettlementRewardShown
|
||||
currentDungeonAltarSettlementRewardCommitted
|
||||
currentDungeonAltarSettlementExitReady
|
||||
)
|
||||
|
||||
type currentDungeonAltarSettlementPlan struct {
|
||||
CurrentRound uint32
|
||||
PlayTimeMilliseconds uint32
|
||||
BestRound uint32
|
||||
BestTimeMilliseconds uint32
|
||||
MaxRound uint32
|
||||
RewardExperience uint32
|
||||
RewardGroups [4][]currentDungeonAltarRewardEntry
|
||||
RewardBundle dungeonCardRewardBundle
|
||||
CharacterStateBody []byte
|
||||
}
|
||||
|
||||
type currentDungeonAltarRewardPlanningResult struct {
|
||||
plan currentDungeonAltarSettlementPlan
|
||||
state *dungeonCardState
|
||||
visit *runtimeDungeonRoomVisit
|
||||
nextDropSeed uint32
|
||||
definition currentDungeonAltarRewardDefinition
|
||||
}
|
||||
|
||||
// currentDungeonAltarSettlementRuntime freezes the complete op208/op209 view
|
||||
// and gates its one durable asset grant. It is owned by session.dungeon.mu;
|
||||
// dungeonCardState provides a second one-shot barrier around the repository
|
||||
// transaction so a settlement retry can never grant the same bundle twice.
|
||||
type currentDungeonAltarSettlementRuntime struct {
|
||||
phase currentDungeonAltarSettlementPhase
|
||||
plan currentDungeonAltarSettlementPlan
|
||||
rewardState *dungeonCardState
|
||||
rewardCommit dungeonCardAssetGrantResult
|
||||
rewardCommitted bool
|
||||
characterStateSent bool
|
||||
goldStateSent bool
|
||||
inventoryStateSent bool
|
||||
mailboxAlarmSent bool
|
||||
}
|
||||
|
||||
func (s *Service) startCurrentDungeonAltarSettlementLocked(
|
||||
session *gameSession,
|
||||
runtime *runtimeDungeonState,
|
||||
) error {
|
||||
if s == nil || session == nil || runtime == nil || runtime.altar == nil || runtime.Session == nil ||
|
||||
session.dungeon.runtime != runtime ||
|
||||
runtime.altar.completedRounds != runtime.altar.MaxRounds ||
|
||||
runtime.Session.Snapshot().Run.Status != worldmap.DungeonRunCompleted {
|
||||
return errCurrentDungeonAltarSettlementInvalid
|
||||
}
|
||||
settlement, err := s.prepareCurrentDungeonAltarSettlementLocked(session, runtime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch settlement.phase {
|
||||
case currentDungeonAltarSettlementPrepared:
|
||||
body := buildCurrentDungeonAltarRankingBody(
|
||||
settlement.plan.CurrentRound,
|
||||
settlement.plan.PlayTimeMilliseconds,
|
||||
settlement.plan.BestRound,
|
||||
settlement.plan.BestTimeMilliseconds,
|
||||
settlement.plan.MaxRound,
|
||||
settlement.plan.RewardExperience,
|
||||
)
|
||||
if err := s.sendGameUpperRawClass(session, currentDungeonAltarRankingMsgID, body, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
settlement.phase = currentDungeonAltarSettlementRankingShown
|
||||
s.logGameEvent(session, "game-dungeon-altar-ranking-sent",
|
||||
"dungeon_id", runtime.Dungeon.ID,
|
||||
"rounds", settlement.plan.CurrentRound,
|
||||
"max_rounds", settlement.plan.MaxRound,
|
||||
"play_time_ms", settlement.plan.PlayTimeMilliseconds,
|
||||
"reward_exp", settlement.plan.RewardExperience,
|
||||
"body_source", "current_exe_sub_1D38CF0_six_u32_time_current_best_time_best_max_exp")
|
||||
fallthrough
|
||||
case currentDungeonAltarSettlementRankingShown:
|
||||
return s.showCurrentDungeonAltarRewardLocked(session, runtime, settlement)
|
||||
case currentDungeonAltarSettlementRewardShown:
|
||||
return s.commitCurrentDungeonAltarRewardLocked(session, runtime, settlement)
|
||||
case currentDungeonAltarSettlementRewardCommitted:
|
||||
return s.projectCurrentDungeonAltarCommittedSettlementLocked(session, runtime, settlement)
|
||||
case currentDungeonAltarSettlementExitReady:
|
||||
return nil
|
||||
default:
|
||||
return errCurrentDungeonAltarSettlementInvalid
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) prepareCurrentDungeonAltarSettlementLocked(
|
||||
session *gameSession,
|
||||
runtime *runtimeDungeonState,
|
||||
) (*currentDungeonAltarSettlementRuntime, error) {
|
||||
if runtime.altar.settlement != nil {
|
||||
return runtime.altar.settlement, nil
|
||||
}
|
||||
now := s.gameplayNow().UTC()
|
||||
snapshot := runtime.Session.Snapshot()
|
||||
if err := ensureCurrentDungeonCompletionReceiptKey(
|
||||
session,
|
||||
runtime,
|
||||
snapshot,
|
||||
now,
|
||||
"blood_altar_dedicated_settlement",
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rewardPlanning, err := s.planCurrentDungeonAltarRewardsLocked(runtime, now)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
|
||||
defer cancel()
|
||||
resources, err := s.currentDungeonSettlementRewardResources(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rewardExperience, err := currentDungeonAltarExperience(runtime, resources, rewardPlanning.definition)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repositories, ok := s.repositoryGroup()
|
||||
if !ok {
|
||||
return nil, dnfaltar.ErrOwnerUnavailable
|
||||
}
|
||||
recordOwner, err := dnfaltar.NewOwner(repositories)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
record, err := recordOwner.UpdateBestRecord(
|
||||
ctx,
|
||||
runtime.Character.CharacterID,
|
||||
uint32(runtime.Dungeon.ID),
|
||||
rewardPlanning.plan.CurrentRound,
|
||||
rewardPlanning.plan.PlayTimeMilliseconds,
|
||||
"altar-record:"+runtime.clearMapCompletionKey,
|
||||
now,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rewardPlanning.plan.BestRound = record.BestRound
|
||||
rewardPlanning.plan.BestTimeMilliseconds = record.BestTimeMillis
|
||||
progressionCommit, err := s.commitCurrentDungeonSettlement(
|
||||
ctx,
|
||||
session,
|
||||
runtime,
|
||||
resources.Progression,
|
||||
rewardExperience,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
runtime.Character = dnfrepo.CloneCharacter(progressionCommit.Character)
|
||||
rewardPlanning.plan.RewardExperience = progressionCommit.ExperienceGain
|
||||
rewardPlanning.plan.CharacterStateBody = buildCurrentFinishLoadingCharacterStateBody(
|
||||
progressionCommit.Character,
|
||||
progressionCommit.Skill.Points,
|
||||
)
|
||||
if len(rewardPlanning.plan.CharacterStateBody) != currentFinishLoadingCharacterStateBodySize {
|
||||
return nil, fmt.Errorf("%w: op37 body=%d", errCurrentDungeonAltarSettlementInvalid, len(rewardPlanning.plan.CharacterStateBody))
|
||||
}
|
||||
settlement := ¤tDungeonAltarSettlementRuntime{
|
||||
phase: currentDungeonAltarSettlementPrepared,
|
||||
plan: rewardPlanning.plan,
|
||||
rewardState: rewardPlanning.state,
|
||||
}
|
||||
runtime.altar.settlement = settlement
|
||||
// Commit the room RNG cursor only after the complete settlement snapshot is
|
||||
// installed. Any transient progression/repository failure before this point
|
||||
// therefore retries the exact same reward plan instead of rerolling it.
|
||||
if rewardPlanning.visit != nil {
|
||||
rewardPlanning.visit.DropRNG = rewardPlanning.nextDropSeed
|
||||
}
|
||||
s.logGameEvent(session, "game-dungeon-altar-settlement-frozen",
|
||||
"dungeon_id", runtime.Dungeon.ID,
|
||||
"completion_key", runtime.clearMapCompletionKey,
|
||||
"rounds", rewardPlanning.plan.CurrentRound,
|
||||
"max_rounds", rewardPlanning.plan.MaxRound,
|
||||
"reward_exp", rewardPlanning.plan.RewardExperience,
|
||||
"reward_cards", len(rewardPlanning.plan.RewardGroups[0]),
|
||||
"reward_gold", rewardPlanning.plan.RewardBundle.Gold,
|
||||
"reward_items", len(rewardPlanning.plan.RewardBundle.Items),
|
||||
"progression_replayed", progressionCommit.Replayed,
|
||||
"experience_weight_vectors_applied", true,
|
||||
"entry_daily_count", runtime.altar.entryDailyCount,
|
||||
"best_round", record.BestRound,
|
||||
"best_time_ms", record.BestTimeMillis,
|
||||
"record_replayed", record.Replayed)
|
||||
return settlement, nil
|
||||
}
|
||||
|
||||
func (s *Service) planCurrentDungeonAltarRewardsLocked(
|
||||
runtime *runtimeDungeonState,
|
||||
now time.Time,
|
||||
) (currentDungeonAltarRewardPlanningResult, error) {
|
||||
if runtime == nil || runtime.altar == nil || runtime.altar.completedRounds <= 0 || runtime.altar.MaxRounds <= 0 ||
|
||||
!runtime.Dungeon.Metadata.BasisLevel.Set || runtime.Dungeon.Metadata.BasisLevel.Value <= 0 {
|
||||
return currentDungeonAltarRewardPlanningResult{}, errCurrentDungeonAltarSettlementInvalid
|
||||
}
|
||||
catalog, err := s.dungeonMonsterCatalog()
|
||||
if err != nil {
|
||||
return currentDungeonAltarRewardPlanningResult{}, err
|
||||
}
|
||||
definition, err := loadCurrentDungeonAltarRewardDefinition(catalog.source)
|
||||
if err != nil {
|
||||
return currentDungeonAltarRewardPlanningResult{}, err
|
||||
}
|
||||
goldReferences, err := currentDungeonGoldReferences(catalog)
|
||||
if err != nil {
|
||||
return currentDungeonAltarRewardPlanningResult{}, err
|
||||
}
|
||||
dungeonLevel := int(runtime.Dungeon.Metadata.BasisLevel.Value)
|
||||
goldReference, found := goldReferences[int64(dungeonLevel)]
|
||||
if !found {
|
||||
return currentDungeonAltarRewardPlanningResult{}, fmt.Errorf("%w: altar dungeon level=%d", errCurrentDungeonGoldReferenceInvalid, dungeonLevel)
|
||||
}
|
||||
itemCatalog, err := catalog.DropCatalog()
|
||||
if err != nil {
|
||||
return currentDungeonAltarRewardPlanningResult{}, err
|
||||
}
|
||||
dropDocument, err := parseDungeonCardPVFDocument(catalog.source, dungeonCardDropRulePath)
|
||||
if err != nil {
|
||||
return currentDungeonAltarRewardPlanningResult{}, err
|
||||
}
|
||||
itemReferences, err := parseDungeonCardPVFItemDropReferences(dropDocument)
|
||||
if err != nil {
|
||||
return currentDungeonAltarRewardPlanningResult{}, err
|
||||
}
|
||||
itemReference, found := itemReferences[int64(dungeonLevel)]
|
||||
if !found {
|
||||
return currentDungeonAltarRewardPlanningResult{}, fmt.Errorf("%w: altar dungeon level=%d", errDungeonCardPVFSectionMissing, dungeonLevel)
|
||||
}
|
||||
|
||||
rng := newCurrentDungeonDropLCG(runtime.Seed)
|
||||
var visit *runtimeDungeonRoomVisit
|
||||
if scene, ok := runtime.Session.Scene(); ok {
|
||||
if ownedVisit, visitErr := runtime.currentDungeonRoomVisit(scene); visitErr == nil {
|
||||
visit = ownedVisit
|
||||
rng = newCurrentDungeonDropLCG(ownedVisit.DropRNG)
|
||||
}
|
||||
}
|
||||
rewardGroups, bundle, err := currentDungeonAltarGenerateRewards(
|
||||
definition,
|
||||
runtime.altar,
|
||||
rng,
|
||||
goldReference,
|
||||
currentDungeonCardGoldTerms{
|
||||
Difficulty: int(runtime.Request.Difficulty),
|
||||
PartyMembers: 1,
|
||||
PartyRate: 1,
|
||||
WeightedKills: runtime.currentDungeonClearWeightedKills(),
|
||||
},
|
||||
itemCatalog,
|
||||
dungeonLevel,
|
||||
itemReference,
|
||||
now,
|
||||
)
|
||||
if err != nil {
|
||||
return currentDungeonAltarRewardPlanningResult{}, err
|
||||
}
|
||||
cardPlan, err := newDungeonCardRewardPlan(
|
||||
dungeonCardPlanIdentity{
|
||||
CharacterID: runtime.Character.CharacterID,
|
||||
DungeonID: runtime.Dungeon.ID,
|
||||
MazeIndex: runtime.MazeIndex,
|
||||
RunSeed: runtime.Seed,
|
||||
},
|
||||
"current_pvf_bloodclearreward_dedicated_settlement",
|
||||
bundle,
|
||||
dungeonCardRewardBundle{},
|
||||
)
|
||||
if err != nil {
|
||||
return currentDungeonAltarRewardPlanningResult{}, err
|
||||
}
|
||||
rewardState, err := newDungeonCardState(cardPlan)
|
||||
if err != nil {
|
||||
return currentDungeonAltarRewardPlanningResult{}, err
|
||||
}
|
||||
playTime := now.Sub(runtime.startedAt)
|
||||
if runtime.startedAt.IsZero() || playTime < 0 {
|
||||
playTime = 0
|
||||
}
|
||||
playTimeMilliseconds := uint32(min(int64(math.MaxUint32), playTime.Milliseconds()))
|
||||
completedRounds := uint32(runtime.altar.completedRounds)
|
||||
maximumRounds := uint32(runtime.altar.MaxRounds)
|
||||
return currentDungeonAltarRewardPlanningResult{
|
||||
plan: currentDungeonAltarSettlementPlan{
|
||||
CurrentRound: completedRounds,
|
||||
PlayTimeMilliseconds: playTimeMilliseconds,
|
||||
BestRound: completedRounds,
|
||||
BestTimeMilliseconds: playTimeMilliseconds,
|
||||
MaxRound: maximumRounds,
|
||||
RewardGroups: rewardGroups,
|
||||
RewardBundle: bundle,
|
||||
},
|
||||
state: rewardState,
|
||||
visit: visit,
|
||||
nextDropSeed: rng.Seed(),
|
||||
definition: definition,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func currentDungeonAltarExperience(
|
||||
runtime *runtimeDungeonState,
|
||||
resources currentDungeonSettlementRewardResources,
|
||||
definition currentDungeonAltarRewardDefinition,
|
||||
) (uint32, error) {
|
||||
if runtime == nil || runtime.altar == nil || resources.ExperienceSources == nil ||
|
||||
runtime.Character.Level <= 0 || runtime.altar.completedRounds <= 0 ||
|
||||
runtime.altar.completedRounds >= len(definition.RewardExperienceWeights) {
|
||||
return 0, errCurrentDungeonAltarSettlementInvalid
|
||||
}
|
||||
base, found := resources.ExperienceSources.MonsterTableValue(runtime.Character.Level)
|
||||
if !found || base == 0 {
|
||||
return 0, fmt.Errorf("%w: monster reward level=%d", errCurrentDungeonAltarSettlementInvalid, runtime.Character.Level)
|
||||
}
|
||||
weight := definition.RewardExperienceWeights[runtime.altar.completedRounds]
|
||||
value := float64(base) * float64(runtime.altar.completedRounds) * weight
|
||||
switch runtime.altar.Kind {
|
||||
case worldmap.BloodAltarDungeonKindUltimate:
|
||||
value *= 10
|
||||
case worldmap.BloodAltarDungeonKindEndless:
|
||||
if len(definition.EntryCountExperienceWeight) == 0 {
|
||||
return 0, errCurrentDungeonAltarSettlementInvalid
|
||||
}
|
||||
entryIndex := int(runtime.altar.entryDailyCount)
|
||||
if entryIndex >= len(definition.EntryCountExperienceWeight) {
|
||||
entryIndex = len(definition.EntryCountExperienceWeight) - 1
|
||||
}
|
||||
value *= definition.EntryCountExperienceWeight[entryIndex]
|
||||
default:
|
||||
return 0, errCurrentDungeonAltarSettlementInvalid
|
||||
}
|
||||
if math.IsNaN(value) || math.IsInf(value, 0) || value < 0 {
|
||||
return 0, errCurrentDungeonAltarSettlementInvalid
|
||||
}
|
||||
if value > 1_000_000 {
|
||||
value = 1_000_000
|
||||
}
|
||||
return uint32(value), nil
|
||||
}
|
||||
|
||||
func currentDungeonAltarGenerateRewards(
|
||||
definition currentDungeonAltarRewardDefinition,
|
||||
altar *currentDungeonAltarRuntime,
|
||||
rng *currentDungeonDropLCG,
|
||||
goldReference currentDungeonGoldReference,
|
||||
goldTerms currentDungeonCardGoldTerms,
|
||||
itemCatalog *pvfDungeonDropCatalog,
|
||||
dungeonLevel int,
|
||||
itemReference dungeonCardPVFItemDropReference,
|
||||
now time.Time,
|
||||
) ([4][]currentDungeonAltarRewardEntry, dungeonCardRewardBundle, error) {
|
||||
var groups [4][]currentDungeonAltarRewardEntry
|
||||
if altar == nil || rng == nil || itemCatalog == nil || altar.completedRounds <= 0 || altar.MaxRounds <= 0 {
|
||||
return groups, dungeonCardRewardBundle{}, errCurrentDungeonAltarSettlementInvalid
|
||||
}
|
||||
count := definition.rewardCardCount(altar.completedRounds, altar.MaxRounds)
|
||||
scale := definition.rewardRollScale()
|
||||
if count <= 0 || scale <= 0 {
|
||||
return groups, dungeonCardRewardBundle{}, errCurrentDungeonAltarRewardDefinitionInvalid
|
||||
}
|
||||
groups[0] = make([]currentDungeonAltarRewardEntry, 0, count+1)
|
||||
bundle := dungeonCardRewardBundle{}
|
||||
for index := 0; index < count; index++ {
|
||||
candidate, err := definition.classifyReward(altar.completedRounds, rng.Next(scale))
|
||||
if err != nil {
|
||||
return [4][]currentDungeonAltarRewardEntry{}, dungeonCardRewardBundle{}, err
|
||||
}
|
||||
switch candidate {
|
||||
case currentDungeonAltarRewardCandidateGold:
|
||||
amount := definition.scaleGold(currentDungeonCardGoldAmount(rng, goldReference, goldTerms))
|
||||
if amount <= 0 || amount > math.MaxUint32 || bundle.Gold > math.MaxInt64-amount {
|
||||
groups[0] = append(groups[0], currentDungeonAltarRewardEntry{ItemID: math.MaxUint32})
|
||||
continue
|
||||
}
|
||||
bundle.Gold += amount
|
||||
groups[0] = append(groups[0], currentDungeonAltarRewardEntry{Amount: uint32(amount)})
|
||||
case currentDungeonAltarRewardCandidateItem:
|
||||
item, found, itemErr := currentDungeonRandomCardItem(rng, itemCatalog, dungeonLevel, itemReference, now)
|
||||
if itemErr != nil {
|
||||
return [4][]currentDungeonAltarRewardEntry{}, dungeonCardRewardBundle{}, itemErr
|
||||
}
|
||||
if !found || item.ItemID <= 0 || item.ItemID > math.MaxUint32 || item.Count <= 0 || item.Count > math.MaxUint32 {
|
||||
groups[0] = append(groups[0], currentDungeonAltarRewardEntry{ItemID: math.MaxUint32})
|
||||
continue
|
||||
}
|
||||
bundle.Items = append(bundle.Items, item)
|
||||
groups[0] = append(groups[0], currentDungeonAltarRewardEntry{ItemID: uint32(item.ItemID), Amount: uint32(item.Count)})
|
||||
default:
|
||||
groups[0] = append(groups[0], currentDungeonAltarRewardEntry{ItemID: math.MaxUint32})
|
||||
}
|
||||
}
|
||||
if altar.Kind == worldmap.BloodAltarDungeonKindUltimate {
|
||||
point := definition.ultimatePoint(altar.completedDifficulties)
|
||||
if itemID, found := definition.ultimateRewardItem(point, rng.Next(100)); found {
|
||||
item, itemErr := currentDungeonCardItemReward(rng, itemCatalog, itemID, now)
|
||||
if itemErr != nil {
|
||||
return [4][]currentDungeonAltarRewardEntry{}, dungeonCardRewardBundle{}, itemErr
|
||||
}
|
||||
bundle.Items = append(bundle.Items, item)
|
||||
groups[0] = append(groups[0], currentDungeonAltarRewardEntry{ItemID: itemID, Amount: uint32(item.Count)})
|
||||
}
|
||||
}
|
||||
return groups, bundle, nil
|
||||
}
|
||||
|
||||
func (s *Service) showCurrentDungeonAltarRewardLocked(
|
||||
session *gameSession,
|
||||
runtime *runtimeDungeonState,
|
||||
settlement *currentDungeonAltarSettlementRuntime,
|
||||
) error {
|
||||
if settlement.phase != currentDungeonAltarSettlementRankingShown {
|
||||
return nil
|
||||
}
|
||||
body, err := buildCurrentDungeonAltarRewardBody(
|
||||
byte(min(uint32(math.MaxUint8), settlement.plan.CurrentRound)),
|
||||
byte(min(uint32(math.MaxUint8), settlement.plan.MaxRound)),
|
||||
settlement.plan.RewardGroups,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.sendGameUpperRawClass(session, currentDungeonAltarRewardMsgID, body, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
settlement.phase = currentDungeonAltarSettlementRewardShown
|
||||
s.logGameEvent(session, "game-dungeon-altar-reward-sent",
|
||||
"dungeon_id", runtime.Dungeon.ID,
|
||||
"cards", len(settlement.plan.RewardGroups[0]),
|
||||
"gold", settlement.plan.RewardBundle.Gold,
|
||||
"items", len(settlement.plan.RewardBundle.Items),
|
||||
"body_source", "current_exe_sub_1D3A770_four_independent_counted_groups")
|
||||
return s.commitCurrentDungeonAltarRewardLocked(session, runtime, settlement)
|
||||
}
|
||||
|
||||
func (s *Service) commitCurrentDungeonAltarRewardLocked(
|
||||
session *gameSession,
|
||||
runtime *runtimeDungeonState,
|
||||
settlement *currentDungeonAltarSettlementRuntime,
|
||||
) error {
|
||||
if settlement.phase != currentDungeonAltarSettlementRewardShown &&
|
||||
settlement.phase != currentDungeonAltarSettlementRewardCommitted {
|
||||
return nil
|
||||
}
|
||||
if !settlement.rewardCommitted {
|
||||
reservation, _, err := settlement.rewardState.reserveSelection(dungeonCardSideFree, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if reservation.grant {
|
||||
completionKey, keyErr := currentDungeonAltarCardRewardCompletionKey(runtime, settlement.rewardState)
|
||||
if keyErr != nil {
|
||||
return keyErr
|
||||
}
|
||||
grant, grantErr := s.deliverCurrentDungeonAltarRewardReservationWithCompletionKey(
|
||||
session,
|
||||
settlement.rewardState,
|
||||
reservation,
|
||||
completionKey,
|
||||
)
|
||||
if grantErr != nil {
|
||||
return grantErr
|
||||
}
|
||||
settlement.rewardCommit = grant
|
||||
}
|
||||
settlement.rewardCommitted = true
|
||||
settlement.phase = currentDungeonAltarSettlementRewardCommitted
|
||||
}
|
||||
return s.projectCurrentDungeonAltarCommittedSettlementLocked(session, runtime, settlement)
|
||||
}
|
||||
|
||||
// deliverCurrentDungeonAltarRewardReservation deliberately obtains the bag
|
||||
// capacity from the selected character's persisted op13 container state. A
|
||||
// zero literal here would make rewards without a PVF slot range invalid (or
|
||||
// route them away from the real main bag) and violates the card asset owner
|
||||
// contract.
|
||||
func (s *Service) deliverCurrentDungeonAltarRewardReservation(
|
||||
session *gameSession,
|
||||
state *dungeonCardState,
|
||||
reservation dungeonCardDeliveryReservation,
|
||||
) (dungeonCardAssetGrantResult, error) {
|
||||
return s.deliverCurrentDungeonAltarRewardReservationWithCompletionKey(session, state, reservation, "")
|
||||
}
|
||||
|
||||
func (s *Service) deliverCurrentDungeonAltarRewardReservationWithCompletionKey(
|
||||
session *gameSession,
|
||||
state *dungeonCardState,
|
||||
reservation dungeonCardDeliveryReservation,
|
||||
completionKey string,
|
||||
) (dungeonCardAssetGrantResult, error) {
|
||||
if s == nil || session == nil || state == nil || !reservation.grant {
|
||||
return dungeonCardAssetGrantResult{}, nil
|
||||
}
|
||||
characterID := strconv.Itoa(int(session.selectedCharacterID))
|
||||
if state.plan.CharacterID != characterID {
|
||||
state.finishDelivery(reservation, false)
|
||||
return dungeonCardAssetGrantResult{}, errCurrentDungeonAltarSettlementInvalid
|
||||
}
|
||||
repositories, ok := s.repositoryGroup()
|
||||
if !ok || repositories.CharacterAssets == nil || repositories.Settings == nil {
|
||||
state.finishDelivery(reservation, false)
|
||||
return dungeonCardAssetGrantResult{}, errDungeonCardAssetOwnerUnavailable
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
|
||||
containerState := s.loadCurrentItemListContainerState(
|
||||
ctx,
|
||||
repositories.Settings,
|
||||
characterID,
|
||||
dnfrepo.MainInventoryListType,
|
||||
)
|
||||
cancel()
|
||||
ctx, cancel = context.WithTimeout(context.Background(), createWriteTimeout)
|
||||
grant, err := deliverDungeonCardReservationWithCompletionKey(
|
||||
ctx,
|
||||
state,
|
||||
reservation,
|
||||
repositories,
|
||||
containerState.MainSlotCount,
|
||||
s.gameplayNow().UTC(),
|
||||
completionKey,
|
||||
)
|
||||
cancel()
|
||||
return grant, err
|
||||
}
|
||||
|
||||
func currentDungeonAltarCardRewardCompletionKey(
|
||||
runtime *runtimeDungeonState,
|
||||
state *dungeonCardState,
|
||||
) (string, error) {
|
||||
if runtime == nil || runtime.altar == nil || state == nil ||
|
||||
strings.TrimSpace(runtime.clearMapCompletionKey) == "" || strings.TrimSpace(state.plan.ID) == "" {
|
||||
return "", errCurrentDungeonAltarSettlementInvalid
|
||||
}
|
||||
return runtime.clearMapCompletionKey + ":altar-card-plan:" + state.plan.ID, nil
|
||||
}
|
||||
|
||||
func (s *Service) projectCurrentDungeonAltarCommittedSettlementLocked(
|
||||
session *gameSession,
|
||||
runtime *runtimeDungeonState,
|
||||
settlement *currentDungeonAltarSettlementRuntime,
|
||||
) error {
|
||||
if settlement == nil || !settlement.rewardCommitted || settlement.phase < currentDungeonAltarSettlementRewardCommitted {
|
||||
return errCurrentDungeonAltarSettlementInvalid
|
||||
}
|
||||
if !settlement.characterStateSent {
|
||||
if err := s.sendGameUpperRawClass(session, uint16(dnfenum.CmdPacketFinishLoading), settlement.plan.CharacterStateBody, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
settlement.characterStateSent = true
|
||||
}
|
||||
if !settlement.goldStateSent {
|
||||
if settlement.rewardCommit.GoldAfter != settlement.rewardCommit.GoldBefore {
|
||||
if err := s.sendGameUpperRawClass(session, uint16(dnfenum.CmdPacketWalkoutPartyMember), buildCurrentGoldStateBody(settlement.rewardCommit.GoldAfter), 0); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
settlement.goldStateSent = true
|
||||
}
|
||||
if !settlement.inventoryStateSent {
|
||||
refreshes := make([]alignedcmd.ItemSlotRefresh, 0, len(settlement.rewardCommit.ItemSlots))
|
||||
for _, slot := range settlement.rewardCommit.ItemSlots {
|
||||
if slot >= 0 {
|
||||
refreshes = append(refreshes, alignedcmd.ItemSlotRefresh{ListType: dnfrepo.MainInventoryListType, SlotIndex: slot})
|
||||
}
|
||||
}
|
||||
if err := s.sendSelectedIncrementalItemSlotRefreshes(session, "blood-altar-reward", refreshes); err != nil {
|
||||
return err
|
||||
}
|
||||
settlement.inventoryStateSent = true
|
||||
}
|
||||
if !settlement.mailboxAlarmSent {
|
||||
if settlement.rewardCommit.OverflowMailID != "" {
|
||||
if err := s.sendMailboxAlarmToOnlineRecipient(session.selectedCharacterID); err != nil {
|
||||
s.logWarn("blood altar reward mailbox alarm failed", "character_id", session.selectedCharacterID, "error", err)
|
||||
}
|
||||
}
|
||||
// Mailbox alarms are a best-effort projection. Mark the attempt even on a
|
||||
// transport failure so an op37/inventory retry cannot spam the recipient.
|
||||
settlement.mailboxAlarmSent = true
|
||||
}
|
||||
if settlement.phase == currentDungeonAltarSettlementRewardCommitted {
|
||||
// IDA proves the current dungeon handler table has no altar op260. The
|
||||
// client returns through its already verified op72 {state,option}
|
||||
// boundary after the dedicated op208/op209 reward projection is complete.
|
||||
settlement.phase = currentDungeonAltarSettlementExitReady
|
||||
}
|
||||
s.logGameEvent(session, "game-dungeon-altar-settlement-committed",
|
||||
"dungeon_id", runtime.Dungeon.ID,
|
||||
"reward_exp", settlement.plan.RewardExperience,
|
||||
"gold_before", settlement.rewardCommit.GoldBefore,
|
||||
"gold_after", settlement.rewardCommit.GoldAfter,
|
||||
"item_slots", settlement.rewardCommit.ItemSlots,
|
||||
"reward_receipt_replayed", settlement.rewardCommit.Replayed,
|
||||
"return_boundary", "current_exe_op72_cardless_after_altar_settlement",
|
||||
"unregistered_op260_skipped", true,
|
||||
"ordinary_op31_op46_op35_skipped", true)
|
||||
return nil
|
||||
}
|
||||
|
||||
func completedCurrentDungeonAltarSettlementExitReady(runtime *runtimeDungeonState, characterID uint16) bool {
|
||||
return runtime != nil && runtime.altar != nil && runtime.altar.settlement != nil &&
|
||||
runtime.altar.settlement.phase == currentDungeonAltarSettlementExitReady &&
|
||||
runtime.altar.settlement.rewardCommitted &&
|
||||
runtime.altar.settlement.characterStateSent &&
|
||||
runtime.altar.settlement.goldStateSent &&
|
||||
runtime.altar.settlement.inventoryStateSent &&
|
||||
runtime.altar.settlement.mailboxAlarmSent && runtime.Session != nil &&
|
||||
dungeonRuntimeOwnsCharacter(runtime, characterID) &&
|
||||
runtime.Session.Snapshot().Run.Status == worldmap.DungeonRunCompleted
|
||||
}
|
||||
|
||||
func pendingCurrentDungeonAltarSettlementCommitRetry(runtime *runtimeDungeonState, characterID uint16) bool {
|
||||
if runtime == nil || runtime.altar == nil || runtime.altar.settlement == nil || runtime.Session == nil ||
|
||||
!dungeonRuntimeOwnsCharacter(runtime, characterID) ||
|
||||
runtime.Session.Snapshot().Run.Status != worldmap.DungeonRunCompleted {
|
||||
return false
|
||||
}
|
||||
phase := runtime.altar.settlement.phase
|
||||
return phase == currentDungeonAltarSettlementRewardShown ||
|
||||
phase == currentDungeonAltarSettlementRewardCommitted
|
||||
}
|
||||
Reference in new issue
Block a user