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,492 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
dnfhellparty "longheng.io/server/internal/modules/dnf/hellparty"
|
||||
dnfquest "longheng.io/server/internal/modules/dnf/quest"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
"longheng.io/server/internal/modules/dnf/worldmap"
|
||||
)
|
||||
|
||||
var errDungeonClearMapCompletionUnavailable = errors.New("dnf dungeon clear-map completion owner is unavailable")
|
||||
|
||||
type currentDungeonClearMapCompletionAuthority uint8
|
||||
|
||||
const (
|
||||
currentDungeonClearMapCompletionAuthorityBossDieCheck currentDungeonClearMapCompletionAuthority = iota + 1
|
||||
currentDungeonClearMapCompletionAuthorityScriptedAltar
|
||||
)
|
||||
|
||||
// persistCurrentDungeonClearMapCompletionPhaseA closes only the quest-row
|
||||
// Phase-A barrier. It may move real, active PVF clear-map quests to trigger
|
||||
// zero with a pending reward marker, but it never grants a reward, experience,
|
||||
// SP, currency, or an item.
|
||||
//
|
||||
// The caller holds session.dungeon.mu. A successful no-match is still retained
|
||||
// on the runtime so an op115 write retry cannot repeat repository work. The
|
||||
// stable key and timestamp are allocated before the first repository attempt,
|
||||
// which makes a failed-persistence replay deterministic as well.
|
||||
func (s *Service) persistCurrentDungeonClearMapCompletionPhaseA(
|
||||
ctx context.Context,
|
||||
session *gameSession,
|
||||
runtime *runtimeDungeonState,
|
||||
snapshot worldmap.DungeonSessionSnapshot,
|
||||
) error {
|
||||
return s.persistCurrentDungeonClearMapCompletionPhaseAWithAuthority(
|
||||
ctx,
|
||||
session,
|
||||
runtime,
|
||||
snapshot,
|
||||
currentDungeonClearMapCompletionAuthorityBossDieCheck,
|
||||
)
|
||||
}
|
||||
|
||||
// persistCurrentDungeonScriptedAltarClearMapCompletionPhaseA uses the same
|
||||
// durable quest owner as an ordinary clear, but its authority is the exhausted
|
||||
// server-owned altar round table rather than a fabricated boss object/op117.
|
||||
func (s *Service) persistCurrentDungeonScriptedAltarClearMapCompletionPhaseA(
|
||||
ctx context.Context,
|
||||
session *gameSession,
|
||||
runtime *runtimeDungeonState,
|
||||
snapshot worldmap.DungeonSessionSnapshot,
|
||||
) error {
|
||||
return s.persistCurrentDungeonClearMapCompletionPhaseAWithAuthority(
|
||||
ctx,
|
||||
session,
|
||||
runtime,
|
||||
snapshot,
|
||||
currentDungeonClearMapCompletionAuthorityScriptedAltar,
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Service) persistCurrentDungeonClearMapCompletionPhaseAWithAuthority(
|
||||
ctx context.Context,
|
||||
session *gameSession,
|
||||
runtime *runtimeDungeonState,
|
||||
snapshot worldmap.DungeonSessionSnapshot,
|
||||
authority currentDungeonClearMapCompletionAuthority,
|
||||
) error {
|
||||
if session == nil || runtime == nil || runtime.Session == nil || session.dungeon.runtime != runtime {
|
||||
return errDungeonClearMapCompletionUnavailable
|
||||
}
|
||||
if runtime.clearMapCompletionPhaseAPersisted {
|
||||
return nil
|
||||
}
|
||||
authorityAccepted := false
|
||||
receiptSource := "clear_map_phase_a"
|
||||
eventSource := "authoritative_completed_op117_before_op115_op31"
|
||||
switch authority {
|
||||
case currentDungeonClearMapCompletionAuthorityBossDieCheck:
|
||||
authorityAccepted = runtime.bossDieCheckAccepted
|
||||
case currentDungeonClearMapCompletionAuthorityScriptedAltar:
|
||||
authorityAccepted = currentDungeonScriptedAltarClearMapCompletionAccepted(runtime)
|
||||
receiptSource = "blood_altar_scripted_clear_map_phase_a"
|
||||
eventSource = "authoritative_scripted_blood_altar_round_completion"
|
||||
default:
|
||||
return errDungeonClearMapCompletionUnavailable
|
||||
}
|
||||
if !authorityAccepted || snapshot.Run.Status != worldmap.DungeonRunCompleted ||
|
||||
snapshot.Run.DungeonID != runtime.Dungeon.ID || snapshot.Run.MazeIndex != runtime.MazeIndex ||
|
||||
snapshot.Scene.Map.Map.ID <= 0 || !dungeonRuntimeOwnsCharacter(runtime, session.selectedCharacterID) {
|
||||
return fmt.Errorf(
|
||||
"%w: authority=%d accepted=%t status=%s runtime_dungeon=%d snapshot_dungeon=%d runtime_maze=%d snapshot_maze=%d map=%d",
|
||||
errDungeonClearMapCompletionUnavailable,
|
||||
authority,
|
||||
authorityAccepted,
|
||||
snapshot.Run.Status,
|
||||
runtime.Dungeon.ID,
|
||||
snapshot.Run.DungeonID,
|
||||
runtime.MazeIndex,
|
||||
snapshot.Run.MazeIndex,
|
||||
snapshot.Scene.Map.Map.ID,
|
||||
)
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if err := ensureCurrentDungeonCompletionReceiptKey(
|
||||
session,
|
||||
runtime,
|
||||
snapshot,
|
||||
s.gameplayNow(),
|
||||
receiptSource,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repositories, ok := s.repositoryGroup()
|
||||
if !ok || repositories.Character == nil || repositories.Quest == nil {
|
||||
return errDungeonClearMapCompletionUnavailable
|
||||
}
|
||||
characterID := strconv.FormatUint(uint64(session.selectedCharacterID), 10)
|
||||
if _, found, err := repositories.Character.Load(ctx, characterID); err != nil {
|
||||
return err
|
||||
} else if !found {
|
||||
return dnfquest.ErrCharacterNotFound
|
||||
}
|
||||
// An absent quest row means there is no persisted active quest to mutate.
|
||||
// Avoid loading PVF or creating a synthetic row in that case.
|
||||
if _, found, err := repositories.Quest.Load(ctx, characterID); err != nil {
|
||||
return err
|
||||
} else if !found {
|
||||
runtime.clearMapCompletionQuestIDs = nil
|
||||
runtime.clearMapCompletionCompletedQuestIDs = nil
|
||||
runtime.clearMapCompletionPendingQuestIDs = nil
|
||||
runtime.clearMapCompletionPhaseAPersisted = true
|
||||
s.logGameEvent(session, "game-dungeon-clear-map-phase-a-persisted",
|
||||
"char_id", session.selectedCharacterID,
|
||||
"dungeon_id", runtime.Dungeon.ID,
|
||||
"maze_index", runtime.MazeIndex,
|
||||
"map_id", snapshot.Scene.Map.Map.ID,
|
||||
"completion_key", runtime.clearMapCompletionKey,
|
||||
"completion_count", 0,
|
||||
"changed_fields", []dnfrepo.QuestField(nil),
|
||||
"quest_record_found", false,
|
||||
"reward_granted", false,
|
||||
"source", eventSource)
|
||||
return nil
|
||||
}
|
||||
|
||||
catalog, err := s.loadQuestCatalog(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load clear-map quest catalog: %w", err)
|
||||
}
|
||||
owner, err := dnfquest.NewOwner(repositories)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create clear-map quest owner: %w", err)
|
||||
}
|
||||
table, _, worldMapErr := s.dungeonWorldMap()
|
||||
worldMapID := int64(-1)
|
||||
if worldMapErr == nil {
|
||||
if area, found := table.FindAreaByDungeon(runtime.Dungeon.ID); found {
|
||||
worldMapID = area.ID
|
||||
}
|
||||
}
|
||||
hellPartyComplete := runtime.HellParty != nil &&
|
||||
runtime.HellParty.Runtime != nil &&
|
||||
runtime.HellParty.Runtime.Phase() == dnfhellparty.PhaseComplete
|
||||
allRoomsCleared := currentDungeonAllRoomsCleared(runtime.Session.Rooms(), snapshot.Run.Cleared)
|
||||
// The authoritative boss-death phase runs before the settlement path calls
|
||||
// CompleteCurrentRoom, so Run.Cleared normally does not yet contain the
|
||||
// room whose boss just died. Include the current snapshot room for the
|
||||
// "clear every room" condition; otherwise subtype-11 quests such as
|
||||
// 调查卢克的聚光镜 and 通向巨人的路 can never complete even when the boss
|
||||
// room is the only room left.
|
||||
clearedRooms := currentDungeonCompletedRooms(snapshot.Run.Cleared, snapshot.Scene.Coordinate)
|
||||
allRoomsCleared = currentDungeonAllRoomsCleared(runtime.Session.Rooms(), clearedRooms)
|
||||
clearedRoomCount := int64(len(clearedRooms))
|
||||
// 婚约任务(subtype 22)只认"正好这对夫妻、两人、普通副本"的通关。判定用副本
|
||||
// owner 的入场名册(不是城镇队伍列表),并在首次权威通关时冻结,
|
||||
// 避免先回城的一方把自己的状态改掉、让另一方的结果失效。
|
||||
weddingCouple, err := s.currentWeddingDungeonCouple(ctx, runtime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result, err := owner.ApplyClearMapCompletion(ctx, catalog, characterID, dnfquest.ClearMapCompletionInput{
|
||||
WeddingCouple: weddingCouple,
|
||||
DungeonID: runtime.Dungeon.ID,
|
||||
MapID: snapshot.Scene.Map.Map.ID,
|
||||
WorldMapID: worldMapID,
|
||||
Difficulty: int64(runtime.Request.Difficulty),
|
||||
HellPartyComplete: hellPartyComplete,
|
||||
AllRoomsCleared: allRoomsCleared,
|
||||
ClearedRoomCount: clearedRoomCount,
|
||||
CompletionKey: runtime.clearMapCompletionKey,
|
||||
CompletedAt: runtime.clearMapCompletionAt,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("persist clear-map quest phase A: %w", err)
|
||||
}
|
||||
// Phase A 只写任务状态(gain=0),但支援兵的持久效果仍要镜像过去:
|
||||
// 否则"清图任务完成了、支援兵的觉醒/装备槽却没跟上"这种历史数据
|
||||
// 永远修不好。事件键带 :phase-a 与后面的结算回执区分开。
|
||||
if record, found, questErr := repositories.Quest.Load(ctx, characterID); questErr == nil && found {
|
||||
s.syncCurrentDungeonCompanionProgression(
|
||||
ctx, session, runtime.clearMapCompletionKey+":phase-a", nil, 0, &record,
|
||||
)
|
||||
}
|
||||
questIDs := make([]int64, 0, len(result.Completions))
|
||||
completedQuestIDs := make([]int64, 0, len(result.Completions))
|
||||
for _, completion := range result.Completions {
|
||||
questIDs = append(questIDs, completion.QuestID)
|
||||
if completion.Completed {
|
||||
completedQuestIDs = append(completedQuestIDs, completion.QuestID)
|
||||
}
|
||||
}
|
||||
pendingQuestIDs := make([]int64, 0, len(result.Completions))
|
||||
if len(result.Completions) != 0 {
|
||||
if persisted, found, loadErr := repositories.Quest.Load(ctx, characterID); loadErr != nil {
|
||||
return loadErr
|
||||
} else if found {
|
||||
pendingQuestIDs = currentDungeonClearMapPendingRewardQuestIDs(persisted, result.Completions)
|
||||
}
|
||||
}
|
||||
runtime.clearMapCompletionQuestIDs = append(runtime.clearMapCompletionQuestIDs[:0], questIDs...)
|
||||
runtime.clearMapCompletionCompletedQuestIDs = append(runtime.clearMapCompletionCompletedQuestIDs[:0], completedQuestIDs...)
|
||||
runtime.clearMapCompletionPendingQuestIDs = append(runtime.clearMapCompletionPendingQuestIDs[:0], pendingQuestIDs...)
|
||||
runtime.clearMapCompletionPhaseAPersisted = true
|
||||
s.logGameEvent(session, "game-dungeon-clear-map-phase-a-persisted",
|
||||
"char_id", session.selectedCharacterID,
|
||||
"dungeon_id", runtime.Dungeon.ID,
|
||||
"maze_index", runtime.MazeIndex,
|
||||
"map_id", snapshot.Scene.Map.Map.ID,
|
||||
"completion_key", runtime.clearMapCompletionKey,
|
||||
"completion_count", len(result.Completions),
|
||||
"quest_ids", questIDs,
|
||||
"completed_quest_ids", completedQuestIDs,
|
||||
"worldmap_id", worldMapID,
|
||||
"difficulty", runtime.Request.Difficulty,
|
||||
"hell_party_complete", hellPartyComplete,
|
||||
"all_rooms_cleared", allRoomsCleared,
|
||||
"changed_fields", result.ChangedFields,
|
||||
"idempotent", result.Idempotent,
|
||||
"quest_record_found", true,
|
||||
"reward_granted", false,
|
||||
"source", eventSource)
|
||||
return nil
|
||||
}
|
||||
|
||||
func currentDungeonScriptedAltarClearMapCompletionAccepted(runtime *runtimeDungeonState) bool {
|
||||
if runtime == nil || runtime.altar == nil || runtime.altar.progress != currentDungeonAltarProgressDungeonComplete ||
|
||||
runtime.altar.completedRounds != runtime.altar.MaxRounds {
|
||||
return false
|
||||
}
|
||||
return (runtime.Dungeon.ID == 11006 && runtime.altar.Kind == worldmap.BloodAltarDungeonKindEndless && runtime.altar.MaxRounds == 15) ||
|
||||
(runtime.Dungeon.ID == 11007 && runtime.altar.Kind == worldmap.BloodAltarDungeonKindUltimate && runtime.altar.MaxRounds == 7)
|
||||
}
|
||||
|
||||
func currentDungeonCompletedRooms(cleared []worldmap.RoomCoordinate, current worldmap.RoomCoordinate) []worldmap.RoomCoordinate {
|
||||
out := append([]worldmap.RoomCoordinate(nil), cleared...)
|
||||
for _, coordinate := range out {
|
||||
if coordinate == current {
|
||||
return out
|
||||
}
|
||||
}
|
||||
return append(out, current)
|
||||
}
|
||||
|
||||
func currentDungeonAllRoomsCleared(rooms []worldmap.DungeonRoom, cleared []worldmap.RoomCoordinate) bool {
|
||||
if len(rooms) == 0 {
|
||||
return false
|
||||
}
|
||||
clearedSet := make(map[worldmap.RoomCoordinate]struct{}, len(cleared))
|
||||
for _, coordinate := range cleared {
|
||||
clearedSet[coordinate] = struct{}{}
|
||||
}
|
||||
for _, room := range rooms {
|
||||
if _, ok := clearedSet[room.Coordinate]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func ensureCurrentDungeonCompletionReceiptKey(
|
||||
session *gameSession,
|
||||
runtime *runtimeDungeonState,
|
||||
snapshot worldmap.DungeonSessionSnapshot,
|
||||
now time.Time,
|
||||
source string,
|
||||
) error {
|
||||
if session == nil || runtime == nil || runtime.Session == nil ||
|
||||
snapshot.Run.DungeonID != runtime.Dungeon.ID ||
|
||||
snapshot.Run.MazeIndex != runtime.MazeIndex ||
|
||||
snapshot.Scene.Map.Map.ID <= 0 ||
|
||||
!dungeonRuntimeOwnsCharacter(runtime, session.selectedCharacterID) {
|
||||
return fmt.Errorf(
|
||||
"%w: source=%s runtime_dungeon=%d snapshot_dungeon=%d runtime_maze=%d snapshot_maze=%d map=%d",
|
||||
errDungeonClearMapCompletionUnavailable,
|
||||
source,
|
||||
runtime.Dungeon.ID,
|
||||
snapshot.Run.DungeonID,
|
||||
runtime.MazeIndex,
|
||||
snapshot.Run.MazeIndex,
|
||||
snapshot.Scene.Map.Map.ID,
|
||||
)
|
||||
}
|
||||
if runtime.clearMapCompletionAt.IsZero() {
|
||||
runtime.clearMapCompletionAt = now.UTC()
|
||||
}
|
||||
if runtime.clearMapCompletionKey == "" {
|
||||
runtime.clearMapCompletionKey = fmt.Sprintf(
|
||||
"dungeon-clear:char:%d:dungeon:%d:maze:%d:map:%d:seed:%d:completed_at_unix_nano:%d:target:%d:source:%s",
|
||||
session.selectedCharacterID,
|
||||
runtime.Dungeon.ID,
|
||||
runtime.MazeIndex,
|
||||
snapshot.Scene.Map.Map.ID,
|
||||
runtime.Seed,
|
||||
runtime.clearMapCompletionAt.UnixNano(),
|
||||
runtime.bossDieCheckTargetObjectKey,
|
||||
source,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendCurrentDungeonClearMapCompletionNotificationLocked publishes the real
|
||||
// post-persistence active-quest snapshot before settlement starts. The current
|
||||
// EXE's class0/op0x023E reader rebuilds the active task group and has an
|
||||
// explicit trigger-zero completion branch, so this is a state notification,
|
||||
// not a fabricated FINISH_QUEST ACK or reward grant.
|
||||
//
|
||||
// The caller holds session.dungeon.mu. A failed repository verification or
|
||||
// socket write leaves the barrier open so the exact op117 replay resumes at
|
||||
// this stage without repeating Phase A.
|
||||
func (s *Service) sendCurrentDungeonClearMapCompletionNotificationLocked(
|
||||
ctx context.Context,
|
||||
session *gameSession,
|
||||
runtime *runtimeDungeonState,
|
||||
) error {
|
||||
if session == nil || runtime == nil || session.dungeon.runtime != runtime ||
|
||||
!runtime.clearMapCompletionPhaseAPersisted {
|
||||
return errDungeonClearMapCompletionUnavailable
|
||||
}
|
||||
if runtime.clearMapCompletionNotificationClosed {
|
||||
return nil
|
||||
}
|
||||
if len(runtime.clearMapCompletionQuestIDs) == 0 {
|
||||
runtime.clearMapCompletionNotificationClosed = true
|
||||
s.logGameEvent(session, "game-dungeon-clear-map-active-quest-snapshot-skipped",
|
||||
"char_id", session.selectedCharacterID,
|
||||
"dungeon_id", runtime.Dungeon.ID,
|
||||
"completion_key", runtime.clearMapCompletionKey,
|
||||
"reason", "no_matching_persisted_clear_map_quest",
|
||||
"reward_granted", false)
|
||||
return nil
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
repositories, ok := s.repositoryGroup()
|
||||
if !ok || repositories.Quest == nil {
|
||||
return errDungeonClearMapCompletionUnavailable
|
||||
}
|
||||
characterID := strconv.FormatUint(uint64(session.selectedCharacterID), 10)
|
||||
record, found, err := repositories.Quest.Load(ctx, characterID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found {
|
||||
return dnfquest.ErrQuestPersistVerify
|
||||
}
|
||||
|
||||
rows := currentSelectAckQuestRows(record, true)
|
||||
for _, questID := range runtime.clearMapCompletionPendingQuestIDs {
|
||||
visible := false
|
||||
for _, row := range rows {
|
||||
if int64(row.questID) == questID && row.triggerValue == 0 {
|
||||
visible = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !visible {
|
||||
return fmt.Errorf("%w: pending-reward quest %d is absent from trigger-zero active snapshot", dnfquest.ErrQuestPersistVerify, questID)
|
||||
}
|
||||
state, ok := currentDungeonClearMapQuestState(record, questID)
|
||||
if !ok || state.ProgressValue != 0 || state.Extra["completion_key"] != runtime.clearMapCompletionKey ||
|
||||
state.Extra["reward_state"] != "pending" {
|
||||
return fmt.Errorf("%w: quest %d pending-reward marker mismatch", dnfquest.ErrQuestPersistVerify, questID)
|
||||
}
|
||||
}
|
||||
|
||||
body := buildCurrentActiveQuestSnapshotBody(record, true)
|
||||
if !runtime.clearMapCompletionActiveSnapshotSent {
|
||||
s.logGameEvent(session, "game-dungeon-clear-map-active-quest-snapshot-send",
|
||||
"char_id", session.selectedCharacterID,
|
||||
"dungeon_id", runtime.Dungeon.ID,
|
||||
"maze_index", runtime.MazeIndex,
|
||||
"map_id", runtime.Session.Snapshot().Scene.Map.Map.ID,
|
||||
"completion_key", runtime.clearMapCompletionKey,
|
||||
"quest_ids", append([]int64(nil), runtime.clearMapCompletionQuestIDs...),
|
||||
"pending_reward_quest_ids", append([]int64(nil), runtime.clearMapCompletionPendingQuestIDs...),
|
||||
"active_snapshot_rows", len(rows),
|
||||
"msg_id", currentActiveQuestSnapshotMsgID,
|
||||
"classification", 0,
|
||||
"body_len", len(body),
|
||||
"ordering", "phase_a_persisted_then_op574_before_optional_completed_op21_and_op115_op31",
|
||||
"reward_granted", false,
|
||||
"body_source", "current_exe_sub_1D632D0_u32_count_u16_quest_u32_trigger")
|
||||
if err := s.sendGameUpperRawClass(session, currentActiveQuestSnapshotMsgID, body, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
runtime.clearMapCompletionActiveSnapshotSent = true
|
||||
}
|
||||
if currentDungeonClearMapRequiresAcceptableQuestRefresh(
|
||||
runtime.clearMapCompletionCompletedQuestIDs,
|
||||
runtime.clearMapCompletionPendingQuestIDs,
|
||||
) {
|
||||
s.logGameEvent(session, "game-dungeon-clear-map-acceptable-quest-refresh-send",
|
||||
"char_id", session.selectedCharacterID,
|
||||
"dungeon_id", runtime.Dungeon.ID,
|
||||
"maze_index", runtime.MazeIndex,
|
||||
"map_id", runtime.Session.Snapshot().Scene.Map.Map.ID,
|
||||
"completion_key", runtime.clearMapCompletionKey,
|
||||
"quest_ids", append([]int64(nil), runtime.clearMapCompletionQuestIDs...),
|
||||
"pending_reward_quest_ids", append([]int64(nil), runtime.clearMapCompletionPendingQuestIDs...),
|
||||
"msg_id", currentAcceptableQuestListMsgID,
|
||||
"classification", 0,
|
||||
"ordering", "completed_no_reward_quest_persisted_then_op574_then_op21_before_op115_op31",
|
||||
"body_source", "s4a12_completion_projection_timing_with_current_exe_op21_builder")
|
||||
if err := s.sendCurrentAcceptableQuestListOnlyForSession(
|
||||
session,
|
||||
"dungeon_clear_map_completed_no_reward_quest_after_active_snapshot_before_settlement",
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
runtime.clearMapCompletionNotificationClosed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func currentDungeonClearMapRequiresAcceptableQuestRefresh(completedQuestIDs, pendingRewardQuestIDs []int64) bool {
|
||||
if len(completedQuestIDs) == 0 {
|
||||
return false
|
||||
}
|
||||
pending := make(map[int64]struct{}, len(pendingRewardQuestIDs))
|
||||
for _, questID := range pendingRewardQuestIDs {
|
||||
pending[questID] = struct{}{}
|
||||
}
|
||||
for _, questID := range completedQuestIDs {
|
||||
if _, stillPending := pending[questID]; !stillPending {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func currentDungeonClearMapPendingRewardQuestIDs(record dnfrepo.QuestRecord, completions []dnfquest.ClearMapCompletion) []int64 {
|
||||
if len(completions) == 0 {
|
||||
return nil
|
||||
}
|
||||
pending := make([]int64, 0, len(completions))
|
||||
for _, completion := range completions {
|
||||
state, ok := currentDungeonClearMapQuestState(record, completion.QuestID)
|
||||
if !ok || state.ProgressValue != 0 || state.Extra == nil {
|
||||
continue
|
||||
}
|
||||
if state.Extra["reward_state"] == "pending" && stringsEqualFoldTrim(state.Status, "active") {
|
||||
pending = append(pending, completion.QuestID)
|
||||
}
|
||||
}
|
||||
return pending
|
||||
}
|
||||
|
||||
func currentDungeonClearMapQuestState(record dnfrepo.QuestRecord, questID int64) (dnfrepo.QuestState, bool) {
|
||||
if state, ok := record.States[questID]; ok {
|
||||
return state, true
|
||||
}
|
||||
if state, ok := record.Progress[questID]; ok {
|
||||
return state, true
|
||||
}
|
||||
return dnfrepo.QuestState{}, false
|
||||
}
|
||||
|
||||
func stringsEqualFoldTrim(left, right string) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(left), strings.TrimSpace(right))
|
||||
}
|
||||
Reference in new issue
Block a user