Files
90CNServer/server/internal/services/dnfbridge/initial_town_scene_ready.go
T
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

175 lines
7.0 KiB
Go

package dnfbridge
func markReturnSelectTownReentry(session *gameSession, previousCharacterID uint16) {
if session == nil {
return
}
session.townMu.Lock()
session.returnSelectTownReentryPending = previousCharacterID != 0
session.accountSharedInventoryRebindPending = previousCharacterID != 0
session.returnSelectPreviousCharacterID = previousCharacterID
session.returnSelectRetainedActorRemoved = false
session.townMu.Unlock()
}
func clearReturnSelectTownReentry(session *gameSession) {
if session == nil {
return
}
session.townMu.Lock()
session.returnSelectTownReentryPending = false
session.accountSharedInventoryRebindPending = false
session.returnSelectPreviousCharacterID = 0
session.returnSelectRetainedActorRemoved = false
session.townMu.Unlock()
}
func returnSelectTownReentryPending(session *gameSession) bool {
if session == nil {
return false
}
session.townMu.Lock()
pending := session.returnSelectTownReentryPending
session.townMu.Unlock()
return pending
}
func sameCharacterReturnSelect(session *gameSession, characterID uint16) bool {
if session == nil || characterID == 0 {
return false
}
session.townMu.Lock()
same := session.returnSelectTownReentryPending &&
session.returnSelectPreviousCharacterID == characterID
session.townMu.Unlock()
return same
}
// currentSceneTransitionReadyForState identifies the first boundary at which
// the selected actor and its scene transition both exist. A town route still
// needs the current player/object finalization chain after this point.
//
// Two shapes count as ready, and the second one is the one that keeps every
// 公会 action alive: the initial (login) town route committed its transition, or
// an ordinary op24/op36 town-area move retired that route while the actor stayed
// bound to the same scene. The move path itself is what creates the second
// shape (town_move.go retires the route and then records
// townSceneReadyCharacterID), so without this branch the predicate can never be
// true again for the rest of the session, and every guarded caller fails with
// its module's generic error:
//
// - 公会金库 放入/取出/移动 → error 196, which the CN client renders as
// 「公会仓库尚未完成加载。」(dstr 21684) — live 2026-10-04 19:44, right after
// the 安徒恩 teleport + two op36 area moves;
// - 守护珠镶嵌、公会战商店/任务、公会管理 use this same predicate.
//
// This matches upstream (DNF_NEW internal/services/dnfbridge/
// initial_town_scene_ready.go); this fork had dropped the townReady branch.
func (s *Service) currentSceneTransitionReadyForState(session *gameSession) (bool, string) {
if session == nil {
return false, "session_missing"
}
if session.postStartMapPlayerStateSent {
return true, "dungeon_post_op29_player_state"
}
session.townMu.Lock()
ready := session.initialTownRouteCharacterID != 0 &&
session.initialTownRouteCharacterID == session.selectedCharacterID &&
session.initialTownRouteStage >= currentInitialTownRouteTransitionSent
townReady := session.selectedCharacterID != 0 &&
session.townSceneReadyCharacterID == session.selectedCharacterID &&
session.initialTownRouteCharacterID == 0
session.townMu.Unlock()
if ready {
return true, "initial_town_actor_bound_and_transition_sent"
}
// Ordinary op24 town moves retire the initial route but keep a bound actor.
if townReady {
session.dungeon.mu.Lock()
inDungeon := session.dungeon.runtime != nil
session.dungeon.mu.Unlock()
if !inDungeon {
return true, "committed_town_scene_after_initial_route"
}
}
return false, "actor_binding_or_scene_transition_incomplete"
}
// currentSceneTransitionReadyForStateNonBlocking is the peer-safe read of
// currentSceneTransitionReadyForState: it never waits for another session's town
// lock. A caller that already holds one session's town lock must not block on a
// peer's, because the peer can be running the mirror image of that same call at
// the same moment. Live incident 2026-09-22 20:06: two characters of one guild
// logged in one second apart, each session held its own townMu through
// sendCurrentInitialTownRoute -> publishCurrentTownPresence and then waited for
// the other's inside the guild presence refresh, and both sessions wedged
// forever.
//
// busy reports that the session's town state could not be read right now. It is
// never true for an idle session, so a call that does not race another lock
// holder keeps the blocking form's answer.
func (s *Service) currentSceneTransitionReadyForStateNonBlocking(session *gameSession) (ready bool, source string, busy bool) {
if session == nil {
return false, "session_missing", false
}
if session.postStartMapPlayerStateSent {
return true, "dungeon_post_op29_player_state", false
}
if !session.townMu.TryLock() {
return false, "session_town_state_busy", true
}
ready = session.initialTownRouteCharacterID != 0 &&
session.initialTownRouteCharacterID == session.selectedCharacterID &&
session.initialTownRouteStage >= currentInitialTownRouteTransitionSent
townReady := session.selectedCharacterID != 0 &&
session.townSceneReadyCharacterID == session.selectedCharacterID &&
session.initialTownRouteCharacterID == 0
session.townMu.Unlock()
if ready {
return true, "initial_town_actor_bound_and_transition_sent", false
}
// Same "ordinary town move retired the route but kept a bound actor" shape
// the blocking form accepts -- a peer session that just moved areas must not
// read as unready here.
if townReady {
session.dungeon.mu.Lock()
inDungeon := session.dungeon.runtime != nil
session.dungeon.mu.Unlock()
if !inDungeon {
return true, "committed_town_scene_after_initial_route", false
}
}
return false, "actor_binding_or_scene_transition_incomplete", false
}
// currentSceneActorReadyForState is the stronger finish-loading predicate.
// Dungeon entry reaches it through the post-op29 state chain. Completed town
// login prepares the full mode1/mode3/object-finalizer/display/placement chain
// before op24 and commits the transition only after that chain succeeds.
func (s *Service) currentSceneActorReadyForState(session *gameSession) (bool, string) {
if session == nil {
return false, "session_missing"
}
if session.postStartMapPlayerStateSent {
return true, "dungeon_post_op29_player_state"
}
session.townMu.Lock()
ready := session.initialTownRouteCharacterID != 0 &&
session.initialTownRouteCharacterID == session.selectedCharacterID &&
session.initialTownRouteStage >= currentInitialTownRoutePlayerStateSent
session.townMu.Unlock()
if ready {
session.townPostTransition.mu.Lock()
postTransitionStage := session.townPostTransition.stage
postTransitionCharacterID := session.townPostTransition.characterID
session.townPostTransition.mu.Unlock()
if postTransitionCharacterID == session.selectedCharacterID &&
postTransitionStage >= currentTownPostTransitionPending &&
postTransitionStage < currentTownPostTransitionComplete {
return false, "post_op24_actor_hud_generation_incomplete"
}
return true, "initial_town_player_state_finalized"
}
return false, "player_state_or_scene_finalizer_incomplete"
}