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,375 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/dnfenum"
|
||||
)
|
||||
|
||||
func (s *Service) armCurrentInitialTownRoute(session *gameSession, characterID uint16) {
|
||||
if session == nil || characterID == 0 {
|
||||
return
|
||||
}
|
||||
session.townMu.Lock()
|
||||
resetCurrentTownPostTransitionPlayerState(session)
|
||||
session.initialTownRouteCharacterID = characterID
|
||||
session.initialTownRouteStage = currentInitialTownRouteArmed
|
||||
session.initialTownGuardianGemRefreshSent = false
|
||||
session.initialTownGuardianShieldDeckSent = false
|
||||
session.initialTownGuardianShieldAppearanceSent = false
|
||||
session.chatEmoticonCatalogSent = false
|
||||
session.initialTownActorSceneSnapshotSent = false
|
||||
session.initialTownActorDisplaySent = false
|
||||
session.initialTownActorPlacementSent = false
|
||||
session.initialTownDungeonPermissionSent = false
|
||||
session.initialTownLocationNotificationsSent = false
|
||||
session.initialTownQuestSnapshotsSent = false
|
||||
session.initialTownQuestHUDRefreshSent = false
|
||||
session.initialTownGuildStateProjectionSent = false
|
||||
session.initialTownSkillInfoPrepared = false
|
||||
session.initialTownSkillInfoSent = false
|
||||
session.initialTownSkillInfo = currentSceneSkillInfoProjection{}
|
||||
session.initialTownMode3RefreshPending = false
|
||||
session.completedTownReturnBuffRegistrationPending = false
|
||||
session.initialTownLegacySceneReadyAccepted = false
|
||||
session.initialTownAdventureOverheadRefreshSent = false
|
||||
session.initialTownCombatPowerAffixesSent = false
|
||||
session.initialTownMoodProjectionSent = false
|
||||
session.crystalContractMu.Lock()
|
||||
session.crystalContractTownUIReadyStateSent = false
|
||||
session.crystalContractMu.Unlock()
|
||||
session.auraSkinMu.Lock()
|
||||
session.auraSkinTownUIReadyStateSent = false
|
||||
session.auraSkinMu.Unlock()
|
||||
session.townMu.Unlock()
|
||||
s.logGameEvent(session, "game-initial-town-route-armed",
|
||||
"char_id", characterID,
|
||||
"tutorial_route_index", currentSelectAckPage1RouteIndex,
|
||||
"expected_client_progress", currentInitialTownProgress,
|
||||
"reason", "wait_for_current_exe_page_controller_before_selected_actor_and_town_transition")
|
||||
}
|
||||
|
||||
func (s *Service) handleCurrentInitialTownProgress(
|
||||
session *gameSession,
|
||||
progress uint32,
|
||||
) error {
|
||||
return s.sendCurrentInitialTownRoute(session, progress, true, false)
|
||||
}
|
||||
|
||||
// resumeCurrentInitialTownRouteAfterReturnSelect owns the selector re-entry
|
||||
// boundary. The current client emits progress36 only on the first selector-page
|
||||
// controller lifecycle. After a successful op7 module return it sends op8 and a
|
||||
// new op4, but does not repeat op143. Resume the already-proved town transition
|
||||
// after the new op4 ACK without inventing an op143 request or success response.
|
||||
func (s *Service) resumeCurrentInitialTownRouteAfterReturnSelect(session *gameSession) error {
|
||||
return s.sendCurrentInitialTownRoute(session, currentInitialTownProgress, false, true)
|
||||
}
|
||||
|
||||
// resumeCurrentInitialTownRouteAfterSelectHeartbeat covers the current client
|
||||
// path where a completed character reaches the town loading page but never
|
||||
// emits the old op143/progress36 page-controller request. The heartbeat is not
|
||||
// acknowledged as op143; it only lets the server resume the already armed town
|
||||
// actor/transition stream that select-character prepared.
|
||||
func (s *Service) resumeCurrentInitialTownRouteAfterSelectHeartbeat(session *gameSession, source string) error {
|
||||
if session == nil || session.selectedCharacterID == 0 {
|
||||
return nil
|
||||
}
|
||||
session.townMu.Lock()
|
||||
routeCharacterID := session.initialTownRouteCharacterID
|
||||
routeStage := session.initialTownRouteStage
|
||||
session.townMu.Unlock()
|
||||
if routeCharacterID == 0 ||
|
||||
routeCharacterID != session.selectedCharacterID ||
|
||||
routeStage < currentInitialTownRouteArmed ||
|
||||
routeStage >= currentInitialTownRoutePlayerStateSent {
|
||||
return nil
|
||||
}
|
||||
s.logGameEvent(session, "game-initial-town-route-heartbeat-resume",
|
||||
"source", source,
|
||||
"char_id", session.selectedCharacterID,
|
||||
"route_char_id", routeCharacterID,
|
||||
"route_stage", routeStage,
|
||||
"progress", currentInitialTownProgress,
|
||||
"op143_ack_sent", false,
|
||||
"reason", "client_reached_town_loading_page_without_progress36")
|
||||
return s.sendCurrentInitialTownRoute(session, currentInitialTownProgress, false, false)
|
||||
}
|
||||
|
||||
func (s *Service) sendCurrentInitialTownRoute(
|
||||
session *gameSession,
|
||||
progress uint32,
|
||||
acknowledgeProgress bool,
|
||||
requireReturnSelectReentry bool,
|
||||
) error {
|
||||
if session == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
session.dungeon.mu.Lock()
|
||||
hasDungeonRuntime := session.dungeon.runtime != nil
|
||||
session.dungeon.mu.Unlock()
|
||||
if hasDungeonRuntime {
|
||||
s.logGameEvent(session, "game-initial-town-route-blocked",
|
||||
"char_id", session.selectedCharacterID,
|
||||
"progress", progress,
|
||||
"reason", "active_dungeon_runtime_owns_op143")
|
||||
return nil
|
||||
}
|
||||
|
||||
session.townMu.Lock()
|
||||
defer session.townMu.Unlock()
|
||||
if requireReturnSelectReentry && !session.returnSelectTownReentryPending {
|
||||
s.logGameEvent(session, "game-initial-town-route-deferred",
|
||||
"char_id", session.selectedCharacterID,
|
||||
"route_char_id", session.initialTownRouteCharacterID,
|
||||
"progress", progress,
|
||||
"route_stage", session.initialTownRouteStage,
|
||||
"reason", "return_select_reentry_not_owned_by_server_op7")
|
||||
return nil
|
||||
}
|
||||
if progress != currentInitialTownProgress ||
|
||||
session.initialTownRouteStage < currentInitialTownRouteArmed ||
|
||||
session.initialTownRouteCharacterID == 0 ||
|
||||
session.initialTownRouteCharacterID != session.selectedCharacterID {
|
||||
s.logGameEvent(session, "game-initial-town-route-deferred",
|
||||
"char_id", session.selectedCharacterID,
|
||||
"route_char_id", session.initialTownRouteCharacterID,
|
||||
"progress", progress,
|
||||
"route_stage", session.initialTownRouteStage,
|
||||
"reason", "progress36_not_owned_by_completed_select_route")
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
|
||||
defer cancel()
|
||||
characterID, characterName, character, hasCharacter := s.selectedCharacterForEnter(ctx, session)
|
||||
if characterID != session.initialTownRouteCharacterID || !hasCharacter ||
|
||||
selectedCharacterStartsInTutorial(character, hasCharacter) {
|
||||
s.logGameEvent(session, "game-initial-town-route-blocked",
|
||||
"char_id", session.selectedCharacterID,
|
||||
"route_char_id", session.initialTownRouteCharacterID,
|
||||
"loaded_char_id", characterID,
|
||||
"has_character", hasCharacter,
|
||||
"tutorial_completed", hasPersistedDungeonTutorialCompletion(character),
|
||||
"progress", progress,
|
||||
"reason", "fresh_character_state_does_not_own_completed_select_route")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Character-list login uses the Seria-room owner for completed characters.
|
||||
// A tutorial-pending character returned above never reaches this branch, so
|
||||
// its first in-dungeon position remains client/PVF owned by that dungeon map.
|
||||
character, townID, areaID, row, mapPath, err := s.currentCharacterListLoginTransition(ctx, session, characterID, character)
|
||||
if err != nil {
|
||||
s.logGameEvent(session, "game-initial-town-route-blocked",
|
||||
"char_id", session.selectedCharacterID,
|
||||
"progress", progress,
|
||||
"reason", "persisted_character_list_login_location_unavailable",
|
||||
"error", err)
|
||||
return nil
|
||||
}
|
||||
transitionBody, err := buildCurrentSceneTransitionBody(townID, areaID, []currentSceneTransitionRow{row})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// The cold-login selected actor must use the channel identity established by
|
||||
// CHANNELINFO. The current client keeps the mode0/mode1 actor in that native
|
||||
// channel context; context zero is reserved for compatibility-hook paths and
|
||||
// must not be used for this initial town route.
|
||||
ownerChannel := currentConnectionTownActorOwnerContext(session)
|
||||
session.townActorOwnerChannel = ownerChannel
|
||||
objectBody := s.buildCurrentSceneObjectListBodyForSessionInContext(
|
||||
ctx,
|
||||
session,
|
||||
characterID,
|
||||
characterName,
|
||||
character,
|
||||
hasCharacter,
|
||||
ownerChannel,
|
||||
)
|
||||
adventureSummary := s.currentAccountAdventureGroupSummaryForPacket(ctx, session, character, hasCharacter)
|
||||
adventureLevel := uint32(adventureSummary.ManageLevel)
|
||||
// mode0's completion can immediately schedule the combat-power reader.
|
||||
// Bind the selected actor with the authoritative equipment rows now,
|
||||
// before the scene snapshot and any lengthy account/container projection.
|
||||
mode1Body := s.buildCurrentActorBindingMode1BodyForSelectedWithEquipmentInContext(
|
||||
ctx,
|
||||
session,
|
||||
character,
|
||||
hasCharacter,
|
||||
characterID,
|
||||
adventureLevel,
|
||||
ownerChannel,
|
||||
true,
|
||||
)
|
||||
commitTransitionEarly := requireReturnSelectReentry || session.returnSelectTownReentryPending
|
||||
var earlyActorDisplayBody []byte
|
||||
if commitTransitionEarly {
|
||||
earlyActorDisplayBody = buildCurrentInitialTownOp9Body(
|
||||
session,
|
||||
currentSceneActorObjectKey(characterID),
|
||||
character,
|
||||
hasCharacter,
|
||||
characterName,
|
||||
)
|
||||
}
|
||||
|
||||
acceptedEvent := "game-initial-town-route-progress36-accepted"
|
||||
sequence := "op143_ack_then_op376_then_msg2_mode0_then_complete_equipment_mode1_then_scene_snapshot_and_full_player_state_then_typed_op24"
|
||||
trigger := "client_op143_progress36"
|
||||
if requireReturnSelectReentry {
|
||||
acceptedEvent = "game-initial-town-route-return-select-reentry-accepted"
|
||||
sequence = "op376_then_msg2_mode0_then_complete_equipment_mode1_then_scene_snapshot_and_full_player_state_then_typed_op24"
|
||||
trigger = "server_owned_op7_then_client_op8_then_client_op4"
|
||||
}
|
||||
s.logGameEvent(session, acceptedEvent,
|
||||
"char_id", characterID,
|
||||
"progress", progress,
|
||||
"route_stage", session.initialTownRouteStage,
|
||||
"trigger", trigger,
|
||||
"op143_ack_sent", acknowledgeProgress,
|
||||
"tutorial_completed", hasPersistedDungeonTutorialCompletion(character),
|
||||
"town_id", townID,
|
||||
"area_id", areaID,
|
||||
"map_path", mapPath,
|
||||
"position_x", row.Value1,
|
||||
"position_y", row.Value2,
|
||||
"direction", row.Value3,
|
||||
"area_state", row.Value4,
|
||||
"adventure_total_point", adventureSummary.TotalPoint,
|
||||
"adventure_manage_level", adventureSummary.ManageLevel,
|
||||
"owner_server", 0,
|
||||
"owner_channel", ownerChannel,
|
||||
"sequence", sequence,
|
||||
"evidence_boundary", "current_exe_live_first_select_and_return_select_reentry_lifecycle")
|
||||
|
||||
// The current op143 success reader consumes success plus a reward count.
|
||||
// A duplicate progress-36 request receives another response, but the
|
||||
// one-shot selected-object packets below resume from their committed stage.
|
||||
if acknowledgeProgress {
|
||||
if err := s.sendGameUpperSuccess(
|
||||
session,
|
||||
uint16(dnfenum.CmdPacketChangeTutorialFlag),
|
||||
[]byte{0},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
policy := currentInitialTownRoutePolicy{
|
||||
includeReportClientSpec: true,
|
||||
includeSecondFullMode1: !commitTransitionEarly,
|
||||
skipPrerequisiteMode1: true,
|
||||
commitTransitionEarly: commitTransitionEarly,
|
||||
earlyActorDisplayBody: earlyActorDisplayBody,
|
||||
deferClearQuestListUntilLocationNotifications: true,
|
||||
}
|
||||
if session.initialTownRouteStage >= currentInitialTownRouteTransitionSent {
|
||||
if session.initialTownRouteStage == currentInitialTownRouteTransitionSent {
|
||||
if policy.commitTransitionEarly {
|
||||
if err := s.sendCurrentInitialTownDungeonPermissionSnapshotLocked(
|
||||
session,
|
||||
"return_select_retry_after_op24_scene_commit",
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.sendCurrentTownPlayerStateLocked(session, "return_select_resume_after_early_typed_op24", policy); err != nil {
|
||||
return fmt.Errorf("resume return-select player state after early transition: %w", err)
|
||||
}
|
||||
} else {
|
||||
if err := s.sendCurrentTownTransitionLocked(
|
||||
session,
|
||||
characterID,
|
||||
townID,
|
||||
areaID,
|
||||
row,
|
||||
transitionBody,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if policy.commitTransitionEarly {
|
||||
if err := s.sendCurrentPostTownTransitionQuestSnapshotsLocked(
|
||||
session,
|
||||
"current_exe_return_select_retry_after_op24_scene_commit",
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.sendCurrentExperienceConsumableBuffProjection(session, "return_select_retry_after_early_op24"); err != nil {
|
||||
return fmt.Errorf("restore experience consumable HUD after resumed return-select transition: %w", err)
|
||||
}
|
||||
// 城镇增益药剂(史诗Buff药剂一族)走同一条"恢复"语义:重登/重选之后
|
||||
// buff 图标与倒计时都要按角色身上还没过期的窗口补发,直到持续时间耗尽。
|
||||
if err := s.sendCurrentStatChangePotionBuffProjection(session, "return_select_retry_after_early_op24"); err != nil {
|
||||
return fmt.Errorf("restore stat-change potion HUD after resumed return-select transition: %w", err)
|
||||
}
|
||||
}
|
||||
if err := s.sendCurrentInitialTownGuildStateProjectionLocked(
|
||||
session,
|
||||
"current_exe_initial_town_retry_after_op24_scene_commit",
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
s.publishCurrentTownPresence(
|
||||
session,
|
||||
character,
|
||||
townID,
|
||||
areaID,
|
||||
row,
|
||||
0,
|
||||
mapPath,
|
||||
"initial_town_completed_route_retry",
|
||||
)
|
||||
duplicateEvent := "game-initial-town-route-duplicate-ack-only"
|
||||
if commitTransitionEarly {
|
||||
session.returnSelectTownReentryPending = false
|
||||
duplicateEvent = "game-initial-town-route-return-select-reentry-duplicate-skipped"
|
||||
}
|
||||
s.logGameEvent(session, duplicateEvent,
|
||||
"char_id", characterID,
|
||||
"progress", progress,
|
||||
"route_stage", session.initialTownRouteStage)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := s.sendCurrentInitialTownActorRoutePacketsWithOptionsLocked(
|
||||
session,
|
||||
characterID,
|
||||
townID,
|
||||
areaID,
|
||||
row,
|
||||
objectBody,
|
||||
mode1Body,
|
||||
transitionBody,
|
||||
policy,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
s.publishCurrentTownPresence(
|
||||
session,
|
||||
character,
|
||||
townID,
|
||||
areaID,
|
||||
row,
|
||||
0,
|
||||
mapPath,
|
||||
"initial_town_actor_route_complete",
|
||||
)
|
||||
if commitTransitionEarly {
|
||||
session.returnSelectTownReentryPending = false
|
||||
}
|
||||
|
||||
s.logGameEvent(session, "game-initial-town-route-transition-sent",
|
||||
"char_id", characterID,
|
||||
"progress", progress,
|
||||
"town_id", townID,
|
||||
"area_id", areaID,
|
||||
"map_path", mapPath,
|
||||
"row_count", 1,
|
||||
"trigger", trigger,
|
||||
"op143_ack_sent", acknowledgeProgress,
|
||||
"route_stage", session.initialTownRouteStage,
|
||||
"next_owner", "current_client_scene_request_or_finish_loading")
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user