服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
519 lines
19 KiB
Go
519 lines
19 KiB
Go
package dnfbridge
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math"
|
|
"strconv"
|
|
"time"
|
|
|
|
"longheng.io/server/internal/modules/dnf/alignedcmd"
|
|
"longheng.io/server/internal/modules/dnf/dnfenum"
|
|
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
|
"longheng.io/server/internal/modules/dnf/worldmap"
|
|
)
|
|
|
|
const currentTournamentRewardAutoSelectDelay = 3 * time.Second
|
|
|
|
// sendCurrentTournamentClearRewardLocked freezes the PVF-selected reward
|
|
// faces and projects op388 once. Visible cards are committed through op458;
|
|
// an all-empty PVF plan has no client selection and crosses the gate here.
|
|
func (s *Service) sendCurrentTournamentClearRewardLocked(session *gameSession, runtime *runtimeDungeonState) error {
|
|
if s == nil || session == nil || runtime == nil || runtime.tournament == nil {
|
|
return nil
|
|
}
|
|
if runtime.tournamentClearRewardSent {
|
|
plan, ok := runtime.tournament.RewardPlanSnapshot()
|
|
if ok && plan.Cardless() && runtime.tournament.RewardSelectionComplete() {
|
|
return s.completeCurrentTournamentRewardSelectionLocked(session, runtime)
|
|
}
|
|
return nil
|
|
}
|
|
_, completed, terminal := runtime.tournament.Snapshot()
|
|
if terminal == worldmap.TournamentActive {
|
|
return nil
|
|
}
|
|
experience, ok := runtime.tournament.Definition.Experience[completed]
|
|
if !ok {
|
|
return fmt.Errorf("tournament reward experience missing for completed_rounds=%d", completed)
|
|
}
|
|
experienceSource := "current_pvf_tournament_clear_reward_experience"
|
|
if experience == 0 {
|
|
catalog, catalogErr := s.dungeonMonsterCatalog()
|
|
if catalogErr == nil {
|
|
resources, resourceErr := currentDungeonMonsterExperienceResourcesForCatalog(catalog)
|
|
if resourceErr == nil {
|
|
fallback, fallbackErr := currentDungeonClearExperience(runtime, currentDungeonSettlementRewardResources{
|
|
Progression: resources.Progression,
|
|
ExperienceSources: resources.Sources,
|
|
})
|
|
if fallbackErr == nil {
|
|
experience = fallback
|
|
experienceSource = "ordinary_clear_base_experience_for_zero_tournament_pvf"
|
|
} else {
|
|
s.logGameEvent(session, "game-tournament-display-experience-fallback-blocked",
|
|
"dungeon_id", runtime.Dungeon.ID,
|
|
"completed_rounds", completed,
|
|
"reason", "clear_experience_invalid",
|
|
"error", fallbackErr)
|
|
}
|
|
} else {
|
|
s.logGameEvent(session, "game-tournament-display-experience-fallback-blocked",
|
|
"dungeon_id", runtime.Dungeon.ID,
|
|
"completed_rounds", completed,
|
|
"reason", "progression_resources_unavailable",
|
|
"error", resourceErr)
|
|
}
|
|
} else {
|
|
s.logGameEvent(session, "game-tournament-display-experience-fallback-blocked",
|
|
"dungeon_id", runtime.Dungeon.ID,
|
|
"completed_rounds", completed,
|
|
"reason", "monster_catalog_unavailable",
|
|
"error", catalogErr)
|
|
}
|
|
}
|
|
gold, err := s.currentTournamentRewardGold(runtime)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
plan, _, err := runtime.tournament.PrepareRewardPlan(experience, gold, s.chooseDungeonIndex)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
packet := currentTournamentClearReward{
|
|
CompletedRounds: plan.CompletedRounds,
|
|
Experience: plan.Experience,
|
|
}
|
|
cardless := plan.Cardless()
|
|
if !cardless {
|
|
for side := byte(0); side < 2; side++ {
|
|
entries := make([]currentTournamentClearRewardEntry, 0, plan.CardCount(side))
|
|
for index := byte(0); index < plan.CardCount(side); index++ {
|
|
reward, _ := plan.Card(side, index)
|
|
entries = append(entries, currentTournamentRewardEntry(reward))
|
|
}
|
|
if side == 0 {
|
|
packet.GroupA = entries
|
|
} else {
|
|
packet.GroupB = entries
|
|
}
|
|
}
|
|
}
|
|
body, err := packet.Build()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := s.sendGameUpperRawClass(session, currentTournamentClearRewardNotification, body, 0); err != nil {
|
|
return err
|
|
}
|
|
runtime.tournamentClearRewardSent = true
|
|
runtime.tournamentRewardExperience = plan.Experience
|
|
runtime.tournamentRewardGold = gold
|
|
s.logGameEvent(session, "game-tournament-clear-reward-sent",
|
|
"dungeon_id", runtime.Dungeon.ID,
|
|
"completed_rounds", plan.CompletedRounds,
|
|
"experience", plan.Experience,
|
|
"experience_source", experienceSource,
|
|
"gold", gold,
|
|
"champion", plan.Champion,
|
|
"cardless", cardless,
|
|
"group_a_count", len(packet.GroupA),
|
|
"group_b_count", len(packet.GroupB),
|
|
"body_len", len(body),
|
|
"source", "current_exe_op388_verified_reader")
|
|
if cardless {
|
|
if !runtime.tournament.CommitCardlessRewardPlan() {
|
|
return fmt.Errorf("commit cardless tournament reward plan")
|
|
}
|
|
return s.completeCurrentTournamentRewardSelectionLocked(session, runtime)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func currentTournamentRewardEntry(reward worldmap.TournamentReward) currentTournamentClearRewardEntry {
|
|
if reward.Gold {
|
|
return currentTournamentClearRewardEntry{CountOrGold: reward.Count}
|
|
}
|
|
if reward.ItemID != 0 {
|
|
return currentTournamentClearRewardEntry{ItemOrSentinel: reward.ItemID, CountOrGold: reward.Count}
|
|
}
|
|
return currentTournamentClearRewardEntry{ItemOrSentinel: currentTournamentEmptyRewardSentinel}
|
|
}
|
|
|
|
func (s *Service) currentTournamentRewardGold(runtime *runtimeDungeonState) (uint32, error) {
|
|
if s == nil || runtime == nil || runtime.tournament == nil {
|
|
return 0, fmt.Errorf("tournament runtime is required for gold")
|
|
}
|
|
if runtime.Dungeon.Metadata.BasisLevel.Value <= 0 || runtime.Dungeon.Metadata.TournamentGoldRate <= 0 {
|
|
return 0, fmt.Errorf("tournament gold metadata is invalid")
|
|
}
|
|
catalog, err := s.dungeonMonsterCatalog()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
references, err := currentDungeonGoldReferences(catalog)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
reference, ok := references[runtime.Dungeon.Metadata.BasisLevel.Value]
|
|
if !ok {
|
|
return 0, fmt.Errorf("tournament gold reference missing for level=%d", runtime.Dungeon.Metadata.BasisLevel.Value)
|
|
}
|
|
rng := newCurrentDungeonDropLCG(runtime.Seed)
|
|
base := currentDungeonCardGoldAmount(rng, reference, currentDungeonCardGoldTerms{
|
|
Difficulty: int(runtime.Request.Difficulty),
|
|
PartyMembers: 1,
|
|
PartyRate: 1,
|
|
WeightedKills: runtime.currentDungeonClearWeightedKills(),
|
|
})
|
|
if base <= 0 || base > math.MaxInt64/runtime.Dungeon.Metadata.TournamentGoldRate {
|
|
return 0, fmt.Errorf("tournament gold overflows: base=%d rate=%d", base, runtime.Dungeon.Metadata.TournamentGoldRate)
|
|
}
|
|
amount := base * runtime.Dungeon.Metadata.TournamentGoldRate
|
|
if amount > math.MaxUint32 {
|
|
return 0, fmt.Errorf("tournament gold exceeds wire range: %d", amount)
|
|
}
|
|
return uint32(amount), nil
|
|
}
|
|
|
|
func decodeCurrentTournamentRewardStateRequest(body []byte) error {
|
|
if len(body) != 0 {
|
|
return fmt.Errorf("%w: op457 body_len=%d want=0", errTournamentProtocolRewardRequest, len(body))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) handleTournamentRewardSelectState(session *gameSession, body []byte) error {
|
|
if err := decodeCurrentTournamentRewardStateRequest(body); err != nil {
|
|
s.logGameEvent(session, "game-tournament-reward-state-blocked", "body_len", len(body), "reason", "request_shape", "error", err)
|
|
return nil
|
|
}
|
|
if session == nil {
|
|
return nil
|
|
}
|
|
session.dungeon.mu.Lock()
|
|
defer session.dungeon.mu.Unlock()
|
|
runtime := session.dungeon.runtime
|
|
if runtime == nil || runtime.tournament == nil || !runtime.tournamentClearRewardSent {
|
|
s.logGameEvent(session, "game-tournament-reward-state-blocked", "reason", "clear_reward_not_sent")
|
|
return nil
|
|
}
|
|
if err := s.sendGameUpperRawClass(
|
|
session,
|
|
currentTournamentRewardSelectState,
|
|
append([]byte(nil), currentTournamentRewardSelectOpenBody...),
|
|
1,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
s.armCurrentTournamentRewardAutoSelectLocked(session, runtime)
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) handleTournamentRewardSelect(session *gameSession, body []byte) error {
|
|
request, err := decodeCurrentTournamentRewardSelectRequest(body)
|
|
if err != nil {
|
|
s.logGameEvent(session, "game-tournament-reward-select-blocked", "body_len", len(body), "reason", "request_shape", "error", err)
|
|
return nil
|
|
}
|
|
if session == nil {
|
|
return nil
|
|
}
|
|
session.dungeon.mu.Lock()
|
|
defer session.dungeon.mu.Unlock()
|
|
runtime := session.dungeon.runtime
|
|
if runtime == nil || runtime.tournament == nil || !runtime.tournamentClearRewardSent {
|
|
s.logGameEvent(session, "game-tournament-reward-select-blocked", "side", request.Side, "card_index", request.CardIndex, "reason", "clear_reward_not_sent")
|
|
return nil
|
|
}
|
|
return s.applyCurrentTournamentRewardSelectionLocked(session, runtime, request, "client")
|
|
}
|
|
|
|
func (s *Service) applyCurrentTournamentRewardSelectionLocked(
|
|
session *gameSession,
|
|
runtime *runtimeDungeonState,
|
|
request currentTournamentRewardSelectRequest,
|
|
source string,
|
|
) error {
|
|
if session == nil || runtime == nil || runtime.tournament == nil ||
|
|
session.dungeon.runtime != runtime || !runtime.tournamentClearRewardSent {
|
|
return nil
|
|
}
|
|
reward, reserved, replay := runtime.tournament.ReserveRewardSelection(request.Side, request.CardIndex)
|
|
if !reserved && !replay {
|
|
s.logGameEvent(session, "game-tournament-reward-select-blocked", "side", request.Side, "card_index", request.CardIndex, "source", source, "reason", "selection_rejected")
|
|
return nil
|
|
}
|
|
bundle, err := s.currentTournamentRewardBundle(reward)
|
|
if err != nil {
|
|
if reserved {
|
|
runtime.tournament.RollbackRewardSelection(request.Side, request.CardIndex)
|
|
}
|
|
return err
|
|
}
|
|
if reserved {
|
|
grant, err := s.grantCurrentTournamentRewardLocked(session, runtime, bundle)
|
|
if err != nil {
|
|
runtime.tournament.RollbackRewardSelection(request.Side, request.CardIndex)
|
|
return err
|
|
}
|
|
if !runtime.tournament.CommitRewardSelection(request.Side, request.CardIndex) {
|
|
// The database grant has already committed. Do not roll the
|
|
// selection back here: doing so could grant the same reward again
|
|
// on a retry if this invariant is ever violated.
|
|
s.logGameEvent(session, "game-tournament-reward-select-commit-gate-failed",
|
|
"side", request.Side, "card_index", request.CardIndex,
|
|
"gold_after", grant.GoldAfter, "item_slots", len(grant.ItemSlots))
|
|
return fmt.Errorf("tournament reward selection commit gate rejected side=%d index=%d", request.Side, request.CardIndex)
|
|
}
|
|
if err := s.projectCurrentTournamentRewardLocked(session, grant); err != nil {
|
|
// Durable state is already committed; the caller may retry the
|
|
// same request to replay the client projection.
|
|
return err
|
|
}
|
|
}
|
|
plan, ok := runtime.tournament.RewardPlanSnapshot()
|
|
if !ok {
|
|
return fmt.Errorf("tournament reward plan missing after selection commit")
|
|
}
|
|
selectionState, err := (currentTournamentRewardSelectionState{Plan: plan}).build()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := s.sendGameUpperRawClass(session, currentTournamentRewardSelect, selectionState, 1); err != nil {
|
|
return err
|
|
}
|
|
s.logGameEvent(session, "game-tournament-reward-select-sent",
|
|
"side", request.Side, "card_index", request.CardIndex,
|
|
"item_id", reward.ItemID, "count", reward.Count,
|
|
"gold", reward.Gold, "empty", reward.Empty,
|
|
"reserved", reserved, "replay", replay,
|
|
"item_detail_response_sent", false,
|
|
"selection_state_len", len(selectionState),
|
|
"source", source)
|
|
if runtime.tournament.RewardSelectionComplete() {
|
|
return s.completeCurrentTournamentRewardSelectionLocked(session, runtime)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) armCurrentTournamentRewardAutoSelectLocked(
|
|
session *gameSession,
|
|
runtime *runtimeDungeonState,
|
|
) {
|
|
if s == nil || session == nil || runtime == nil || runtime.tournament == nil ||
|
|
session.dungeon.runtime != runtime || runtime.lifecycleToken == 0 ||
|
|
runtime.tournament.RewardSelectionComplete() || runtime.tournamentRewardAutoSelectTimerName != "" {
|
|
return
|
|
}
|
|
queue := s.ensureGameplayTimeQueue()
|
|
if queue == nil {
|
|
return
|
|
}
|
|
runtime.tournamentRewardAutoSelectGeneration++
|
|
generation := runtime.tournamentRewardAutoSelectGeneration
|
|
runToken := runtime.lifecycleToken
|
|
timerName := fmt.Sprintf("dnf-tournament-reward-auto-select:%s:run:%d", session.connID, runToken)
|
|
characterID, characterGeneration, err := gameSessionCharacterEventIdentity(session, session.selectedCharacterID)
|
|
if err != nil {
|
|
return
|
|
}
|
|
runtime.tournamentRewardAutoSelectTimerName = timerName
|
|
if err := queue.ScheduleAfter(timerName, currentTournamentRewardAutoSelectDelay, func(time.Time) {
|
|
postErr := s.postGameSessionCharacterEvent(
|
|
session,
|
|
"tournament-reward-auto-select",
|
|
characterID,
|
|
characterGeneration,
|
|
func() error {
|
|
session.dungeon.mu.Lock()
|
|
defer session.dungeon.mu.Unlock()
|
|
if session.dungeon.runtime != runtime || runtime.lifecycleToken != runToken ||
|
|
runtime.tournamentRewardAutoSelectGeneration != generation ||
|
|
runtime.tournamentRewardAutoSelectTimerName != timerName {
|
|
return nil
|
|
}
|
|
runtime.tournamentRewardAutoSelectTimerName = ""
|
|
return s.autoSelectCurrentTournamentRewardsLocked(session, runtime)
|
|
},
|
|
)
|
|
if postErr != nil && !isClosedGameSessionEventError(postErr) {
|
|
s.logPacketEvent("game-tournament-reward-auto-select-submit-failed",
|
|
"conn_id", session.connID, "char_id", characterID, "error", postErr)
|
|
}
|
|
}); err != nil {
|
|
runtime.tournamentRewardAutoSelectTimerName = ""
|
|
s.logGameEvent(session, "game-tournament-reward-auto-select-schedule-failed",
|
|
"dungeon_id", runtime.Dungeon.ID, "error", err)
|
|
return
|
|
}
|
|
s.logGameEvent(session, "game-tournament-reward-auto-select-scheduled",
|
|
"dungeon_id", runtime.Dungeon.ID, "run_token", runToken,
|
|
"timer_generation", generation, "delay_ms", currentTournamentRewardAutoSelectDelay.Milliseconds())
|
|
}
|
|
|
|
func (s *Service) autoSelectCurrentTournamentRewardsLocked(
|
|
session *gameSession,
|
|
runtime *runtimeDungeonState,
|
|
) error {
|
|
if session == nil || runtime == nil || runtime.tournament == nil ||
|
|
session.dungeon.runtime != runtime || runtime.tournament.RewardSelectionComplete() {
|
|
return nil
|
|
}
|
|
plan, ok := runtime.tournament.RewardPlanSnapshot()
|
|
if !ok {
|
|
return nil
|
|
}
|
|
for side := byte(0); side < 2; side++ {
|
|
if plan.CardCount(side) == 0 || runtime.tournament.RewardSideSelectionComplete(side) {
|
|
continue
|
|
}
|
|
if err := s.applyCurrentTournamentRewardSelectionLocked(
|
|
session,
|
|
runtime,
|
|
currentTournamentRewardSelectRequest{Side: side, CardIndex: 0},
|
|
"timequeue_auto_select",
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) cancelCurrentTournamentRewardAutoSelectLocked(
|
|
session *gameSession,
|
|
runtime *runtimeDungeonState,
|
|
source string,
|
|
) {
|
|
if runtime == nil {
|
|
return
|
|
}
|
|
timerName := runtime.tournamentRewardAutoSelectTimerName
|
|
runtime.tournamentRewardAutoSelectTimerName = ""
|
|
runtime.tournamentRewardAutoSelectGeneration++
|
|
if timerName == "" || s == nil {
|
|
return
|
|
}
|
|
cancelled := false
|
|
if queue := s.ensureGameplayTimeQueue(); queue != nil {
|
|
cancelled = queue.Cancel(timerName)
|
|
}
|
|
s.logGameEvent(session, "game-tournament-reward-auto-select-cancelled",
|
|
"dungeon_id", runtime.Dungeon.ID, "source", source, "cancelled", cancelled)
|
|
}
|
|
|
|
// completeCurrentTournamentRewardSelectionLocked is the tournament equivalent
|
|
// of the ordinary final-room clear coordinator. The current EXE only proceeds
|
|
// to the result request after both op458 selections have been committed; an
|
|
// an all-empty PVF plan crosses the same gate without exposing a card.
|
|
func (s *Service) completeCurrentTournamentRewardSelectionLocked(
|
|
session *gameSession,
|
|
runtime *runtimeDungeonState,
|
|
) error {
|
|
if session == nil || runtime == nil || runtime.tournament == nil ||
|
|
runtime.Session == nil || session.dungeon.runtime != runtime ||
|
|
!runtime.tournament.RewardSelectionComplete() {
|
|
return nil
|
|
}
|
|
s.cancelCurrentTournamentRewardAutoSelectLocked(session, runtime, "reward_selection_complete")
|
|
snapshot := runtime.Session.Snapshot()
|
|
if snapshot.Run.Status == worldmap.DungeonRunCompleted {
|
|
return s.sendCurrentDungeonSettlementEntryLocked(
|
|
session, runtime, "tournament_reward_selection_replay")
|
|
}
|
|
if snapshot.Run.Status != worldmap.DungeonRunActive {
|
|
return nil
|
|
}
|
|
if err := runtime.Session.CompleteCurrentRoom(); err != nil {
|
|
return fmt.Errorf("complete tournament reward room: %w", err)
|
|
}
|
|
completed := runtime.Session.Snapshot()
|
|
s.logGameEvent(session, "game-tournament-reward-selection-complete",
|
|
"dungeon_id", runtime.Dungeon.ID,
|
|
"run_status", completed.Run.Status,
|
|
"source", "current_exe_op458_two_phase_selection")
|
|
return s.sendCurrentDungeonSettlementEntryLocked(
|
|
session, runtime, "tournament_reward_selection_complete")
|
|
}
|
|
|
|
func (s *Service) currentTournamentRewardBundle(reward worldmap.TournamentReward) (dungeonCardRewardBundle, error) {
|
|
if reward.Gold {
|
|
return dungeonCardRewardBundle{Gold: int64(reward.Count)}, nil
|
|
}
|
|
if reward.Empty || reward.ItemID == 0 {
|
|
return dungeonCardRewardBundle{}, nil
|
|
}
|
|
catalog, err := s.dungeonMonsterCatalog()
|
|
if err != nil {
|
|
return dungeonCardRewardBundle{}, err
|
|
}
|
|
definition, err := catalog.DropCatalog()
|
|
if err != nil {
|
|
return dungeonCardRewardBundle{}, err
|
|
}
|
|
item, err := definition.ResolveItem(reward.ItemID)
|
|
if err != nil {
|
|
return dungeonCardRewardBundle{}, err
|
|
}
|
|
extra := map[string]string{
|
|
"source": "tournament_reward",
|
|
"item_kind": string(item.Kind),
|
|
"pvf_path": item.PVFPath,
|
|
}
|
|
if item.StackableType != "" {
|
|
extra["stackable_type"] = item.StackableType
|
|
}
|
|
if item.StackLimit > 0 {
|
|
extra["stack_limit"] = strconv.FormatInt(item.StackLimit, 10)
|
|
}
|
|
if item.EquipmentType != "" {
|
|
extra["equipment_type"] = item.EquipmentType
|
|
}
|
|
if item.Durability > 0 {
|
|
extra["durability"] = strconv.FormatUint(uint64(item.Durability), 10)
|
|
extra["max_durability"] = strconv.FormatUint(uint64(item.Durability), 10)
|
|
}
|
|
stack := dnfrepo.ItemStack{ItemID: int64(reward.ItemID), Count: int64(reward.Count), Extra: extra}
|
|
entry := currentItemListEntryFromStack(dnfrepo.MainInventoryListType, 0, stack)
|
|
raw := append([]byte(nil), entry.data[:]...)
|
|
return dungeonCardRewardBundle{Items: []dungeonCardItemReward{{
|
|
ItemID: int64(reward.ItemID), Count: int64(reward.Count),
|
|
Stackable: item.Kind == dungeonDropItemStackable, StackLimit: item.StackLimit,
|
|
SlotStart: item.SlotStart, SlotEnd: item.SlotEnd,
|
|
ExpireAt: item.ExpirationDate, RawEntry: append([]byte(nil), raw...), Extra: extra,
|
|
}}}, nil
|
|
}
|
|
|
|
func (s *Service) grantCurrentTournamentRewardLocked(session *gameSession, runtime *runtimeDungeonState, bundle dungeonCardRewardBundle) (dungeonCardAssetGrantResult, error) {
|
|
repositories, ok := s.repositoryGroup()
|
|
if !ok || repositories.CharacterAssets == nil || repositories.Settings == nil {
|
|
return dungeonCardAssetGrantResult{}, errDungeonCardAssetOwnerUnavailable
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
|
|
characterID := strconv.Itoa(int(session.selectedCharacterID))
|
|
state := s.loadCurrentItemListContainerState(ctx, repositories.Settings, characterID, dnfrepo.MainInventoryListType)
|
|
cancel()
|
|
ctx, cancel = context.WithTimeout(context.Background(), createWriteTimeout)
|
|
grant, err := grantDungeonCardRewardBundle(ctx, repositories, characterID, state.MainSlotCount, bundle, s.gameplayNow().UTC())
|
|
cancel()
|
|
if err != nil {
|
|
return dungeonCardAssetGrantResult{}, err
|
|
}
|
|
return grant, nil
|
|
}
|
|
|
|
func (s *Service) projectCurrentTournamentRewardLocked(session *gameSession, grant dungeonCardAssetGrantResult) error {
|
|
if grant.GoldAfter != grant.GoldBefore {
|
|
if err := s.sendGameUpperRawClass(session, uint16(dnfenum.CmdPacketWalkoutPartyMember), buildCurrentGoldStateBody(grant.GoldAfter), 0); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
refreshes := make([]alignedcmd.ItemSlotRefresh, 0, len(grant.ItemSlots))
|
|
for _, slot := range grant.ItemSlots {
|
|
if slot >= 0 {
|
|
refreshes = append(refreshes, alignedcmd.ItemSlotRefresh{ListType: dnfrepo.MainInventoryListType, SlotIndex: slot})
|
|
}
|
|
}
|
|
return s.sendSelectedIncrementalItemSlotRefreshes(session, "tournament-reward", refreshes)
|
|
}
|