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,281 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/dnfenum"
|
||||
"longheng.io/server/internal/modules/dnf/dungeoncmd"
|
||||
"longheng.io/server/internal/modules/dnf/worldmap"
|
||||
)
|
||||
|
||||
const (
|
||||
currentDungeonBossDieCheckResponseSize = 4
|
||||
currentDungeonTutorialExitMsgID = uint16(dnfenum.CmdPacketSetQuestTrigger)
|
||||
currentDungeonTutorialExitReason = byte(100)
|
||||
)
|
||||
|
||||
func (s *Service) handleDungeonBossDieCheck(session *gameSession, body []byte) error {
|
||||
requestBody := append([]byte(nil), body...)
|
||||
return s.executeRuntimePartyDungeonCommand(
|
||||
session,
|
||||
"party-dungeon-boss-die-check",
|
||||
func(member *gameSession) error {
|
||||
return s.handleDungeonBossDieCheckLocal(member, requestBody)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Service) handleDungeonBossDieCheckLocal(session *gameSession, body []byte) error {
|
||||
if session == nil {
|
||||
return nil
|
||||
}
|
||||
if len(body) != dungeoncmd.BossDieCheckRequestSize {
|
||||
s.logGameEvent(session, "game-dungeon-boss-die-check-blocked",
|
||||
"body_len", len(body),
|
||||
"expected_body_len", dungeoncmd.BossDieCheckRequestSize,
|
||||
"reason", "current_exe_op117_plaintext_boundary_mismatch")
|
||||
return nil
|
||||
}
|
||||
request, err := dungeoncmd.DecodeBossDieCheckRequest(body)
|
||||
if err != nil {
|
||||
s.logGameEvent(session, "game-dungeon-boss-die-check-blocked",
|
||||
"body_len", len(body),
|
||||
"reason", "current_exe_op117_request_malformed",
|
||||
"error", err)
|
||||
return nil
|
||||
}
|
||||
if request.ReservedZero != 0 || request.Field12 != 0 {
|
||||
s.logGameEvent(session, "game-dungeon-boss-die-check-blocked",
|
||||
"body_len", len(body),
|
||||
"related_actor_object_key", request.RelatedActorObjectKey,
|
||||
"target_object_key", request.TargetObjectKey,
|
||||
"reserved_zero", request.ReservedZero,
|
||||
"field_12", request.Field12,
|
||||
"reason", "current_exe_op117_writer_invariant_mismatch")
|
||||
return nil
|
||||
}
|
||||
s.logGameEvent(session, "game-dungeon-boss-die-check-request-decoded",
|
||||
"body_len", len(body),
|
||||
"related_actor_object_key", request.RelatedActorObjectKey,
|
||||
"target_object_key", request.TargetObjectKey,
|
||||
"reserved_zero", request.ReservedZero,
|
||||
"field_08", request.Field08,
|
||||
"field_12", request.Field12,
|
||||
"field_13", request.Field13,
|
||||
"field_17", request.Field17,
|
||||
"field_25", request.Field25,
|
||||
"body_source", "current_exe_sub_24351D0_exact_39_byte_plaintext")
|
||||
if session.selectedCharacterID == 0 {
|
||||
s.logGameEvent(session, "game-dungeon-boss-die-check-blocked",
|
||||
"body_len", len(body),
|
||||
"reason", "selected_character_missing")
|
||||
return nil
|
||||
}
|
||||
|
||||
session.dungeon.mu.Lock()
|
||||
defer session.dungeon.mu.Unlock()
|
||||
runtime := session.dungeon.runtime
|
||||
return s.handleDungeonBossDieCheckLocked(session, runtime, request, false)
|
||||
}
|
||||
|
||||
// handleDungeonBossDieCheckLocked owns the final-room request state machine.
|
||||
// A validated request may arrive immediately before the same target's op39;
|
||||
// retain only that exact request and finish it after the authoritative death
|
||||
// report instead of fabricating a death or sending an unsolicited completion.
|
||||
func (s *Service) handleDungeonBossDieCheckLocked(
|
||||
session *gameSession,
|
||||
runtime *runtimeDungeonState,
|
||||
request dungeoncmd.BossDieCheckRequest,
|
||||
afterTargetDeath bool,
|
||||
) error {
|
||||
if runtime == nil || runtime.Session == nil || runtime.Room == nil {
|
||||
s.logGameEvent(session, "game-dungeon-boss-die-check-blocked",
|
||||
"related_actor_object_key", request.RelatedActorObjectKey,
|
||||
"target_object_key", request.TargetObjectKey,
|
||||
"reason", "active_dungeon_runtime_missing")
|
||||
return nil
|
||||
}
|
||||
if !dungeonRuntimeOwnsCharacter(runtime, session.selectedCharacterID) {
|
||||
s.logGameEvent(session, "game-dungeon-boss-die-check-blocked",
|
||||
"char_id", session.selectedCharacterID,
|
||||
"runtime_char_id", runtime.Character.CharacterID,
|
||||
"related_actor_object_key", request.RelatedActorObjectKey,
|
||||
"target_object_key", request.TargetObjectKey,
|
||||
"reason", "active_dungeon_runtime_character_owner_mismatch")
|
||||
return nil
|
||||
}
|
||||
if runtime.bossDieCheckAccepted {
|
||||
acceptedRequest := runtime.bossDieCheckRelatedActorObjectKey == request.RelatedActorObjectKey &&
|
||||
runtime.bossDieCheckTargetObjectKey == request.TargetObjectKey
|
||||
acceptedSnapshot := runtime.Session.Snapshot()
|
||||
if acceptedRequest && acceptedSnapshot.Run.Status == worldmap.DungeonRunCompleted {
|
||||
s.logGameEvent(session, "game-dungeon-boss-die-check-completion-replay",
|
||||
"dungeon_id", runtime.Dungeon.ID,
|
||||
"related_actor_object_key", request.RelatedActorObjectKey,
|
||||
"target_object_key", request.TargetObjectKey,
|
||||
"completion_persisted", runtime.tutorialCompletionPersisted,
|
||||
"op115_sent", runtime.bossDieCheckResponseSent,
|
||||
"settlement_entry_sent", runtime.settlementEntrySent,
|
||||
"reason", "retry_persist_op115_or_bodyless_op31_at_exact_failed_stage")
|
||||
return s.completeCurrentDungeonBossDieCheckLocked(session, runtime, "op117_exact_replay")
|
||||
}
|
||||
s.logGameEvent(session, "game-dungeon-boss-die-check-blocked",
|
||||
"dungeon_id", runtime.Dungeon.ID,
|
||||
"related_actor_object_key", request.RelatedActorObjectKey,
|
||||
"target_object_key", request.TargetObjectKey,
|
||||
"accepted_related_actor_object_key", runtime.bossDieCheckRelatedActorObjectKey,
|
||||
"accepted_target_object_key", runtime.bossDieCheckTargetObjectKey,
|
||||
"run_status", acceptedSnapshot.Run.Status,
|
||||
"reason", "conflicting_or_invalid_boss_die_check_replay")
|
||||
return nil
|
||||
}
|
||||
if runtime.bossDieCheckPending {
|
||||
if runtime.bossDieCheckPendingRequest != request {
|
||||
s.logGameEvent(session, "game-dungeon-boss-die-check-blocked",
|
||||
"dungeon_id", runtime.Dungeon.ID,
|
||||
"related_actor_object_key", request.RelatedActorObjectKey,
|
||||
"target_object_key", request.TargetObjectKey,
|
||||
"pending_related_actor_object_key", runtime.bossDieCheckPendingRequest.RelatedActorObjectKey,
|
||||
"pending_target_object_key", runtime.bossDieCheckPendingRequest.TargetObjectKey,
|
||||
"reason", "conflicting_pending_boss_die_check")
|
||||
return nil
|
||||
}
|
||||
if !afterTargetDeath {
|
||||
pendingSnapshot := runtime.Session.Snapshot()
|
||||
_, _, targetDefeated := currentDungeonDefeatedActor(
|
||||
runtime,
|
||||
pendingSnapshot.Scene,
|
||||
uint32(request.TargetObjectKey),
|
||||
)
|
||||
if !targetDefeated {
|
||||
s.logGameEvent(session, "game-dungeon-boss-die-check-pending-replay",
|
||||
"dungeon_id", runtime.Dungeon.ID,
|
||||
"related_actor_object_key", request.RelatedActorObjectKey,
|
||||
"target_object_key", request.TargetObjectKey,
|
||||
"reason", "waiting_for_same_target_authoritative_op39")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
snapshot := runtime.Session.Snapshot()
|
||||
scene := snapshot.Scene
|
||||
if snapshot.Run.Status != worldmap.DungeonRunActive {
|
||||
s.logGameEvent(session, "game-dungeon-boss-die-check-blocked",
|
||||
"dungeon_id", runtime.Dungeon.ID,
|
||||
"run_status", snapshot.Run.Status,
|
||||
"reason", "dungeon_run_not_active")
|
||||
return nil
|
||||
}
|
||||
// Record the sighting before any accept/retain/block decision. The client
|
||||
// emits op117 once per boss death; whether this server consumes it or
|
||||
// drops it, the tutorial final room must not keep waiting for another.
|
||||
if currentDungeonRoomOwnsScene(runtime, scene) {
|
||||
runtime.bossDieCheckObserved = true
|
||||
runtime.bossDieCheckObservedRoom = scene.Coordinate
|
||||
runtime.bossDieCheckObservedMapID = scene.Map.Map.ID
|
||||
}
|
||||
if !currentDungeonRoomOwnsScene(runtime, scene) {
|
||||
s.logGameEvent(session, "game-dungeon-boss-die-check-blocked",
|
||||
"dungeon_id", runtime.Dungeon.ID,
|
||||
"room", scene.Coordinate.String(),
|
||||
"map_id", scene.Map.Map.ID,
|
||||
"reason", "current_room_not_owned_by_active_runtime")
|
||||
return nil
|
||||
}
|
||||
tutorialScene := isPVFTutorialDungeonScene(runtime, scene)
|
||||
if !scene.Boss || !runtime.BossSet || scene.Coordinate != runtime.BossCoordinate ||
|
||||
!currentDungeonStoryFinalStageMatches(runtime, scene) {
|
||||
s.logGameEvent(session, "game-dungeon-boss-die-check-blocked",
|
||||
"dungeon_id", runtime.Dungeon.ID,
|
||||
"room", scene.Coordinate.String(),
|
||||
"map_id", scene.Map.Map.ID,
|
||||
"scene_boss", scene.Boss,
|
||||
"runtime_boss_set", runtime.BossSet,
|
||||
"runtime_boss_room", runtime.BossCoordinate.String(),
|
||||
"story_stage_index", runtime.StoryStageIndex,
|
||||
"story_stage_count", len(runtime.StoryStages),
|
||||
"reason", "current_room_or_story_map_not_authoritative_runtime_boss")
|
||||
return nil
|
||||
}
|
||||
|
||||
playerObjectKey := currentSceneActorObjectKey(session.selectedCharacterID)
|
||||
if request.RelatedActorObjectKey != playerObjectKey {
|
||||
if isHostileAnnouncedKiller(runtime.Room, uint32(request.RelatedActorObjectKey)) {
|
||||
s.logGameEvent(session, "game-dungeon-boss-die-check-blocked",
|
||||
"dungeon_id", runtime.Dungeon.ID,
|
||||
"related_actor_object_key", request.RelatedActorObjectKey,
|
||||
"player_object_key", playerObjectKey,
|
||||
"target_object_key", request.TargetObjectKey,
|
||||
"reason", "related_actor_not_owned_by_dungeon_runtime")
|
||||
return nil
|
||||
}
|
||||
if !dungeonRuntimeContainsDeathOwnerObjectKey(runtime, uint32(request.RelatedActorObjectKey)) &&
|
||||
!runtimePartyDungeonContainsActor(runtime, request.RelatedActorObjectKey) {
|
||||
// Friendly APCs and party members pass silently. Same 90US alignment
|
||||
// as handleDungeonMonsterDeathLocal: the killer is display-only, so
|
||||
// non-hostile summon/pet/APC keys are allowed.
|
||||
s.logGameEvent(session, "game-dungeon-boss-die-check-killer-allowed-non-hostile",
|
||||
"dungeon_id", runtime.Dungeon.ID,
|
||||
"related_actor_object_key", request.RelatedActorObjectKey,
|
||||
"player_object_key", playerObjectKey,
|
||||
"target_object_key", request.TargetObjectKey,
|
||||
"reason", "killer_not_player_nor_friendly_apc_allowed")
|
||||
}
|
||||
}
|
||||
|
||||
targetObjectKey := uint32(request.TargetObjectKey)
|
||||
targetReference, targetIsOrdinaryMonster, ok := currentDungeonDefeatedActor(runtime, scene, targetObjectKey)
|
||||
if !ok {
|
||||
announcedReference, announcedOrdinaryMonster, announced := currentDungeonAnnouncedActor(runtime, scene, targetObjectKey)
|
||||
// The current EXE's ordinary-dungeon op117 must close only a target
|
||||
// already committed by the authoritative op39 death path. The narrow
|
||||
// op117-before-op39 retention exists solely for PVF tutorial CMT order.
|
||||
if !tutorialScene || !announced || afterTargetDeath ||
|
||||
!currentDungeonBossDieCheckCanWaitForTarget(scene, targetObjectKey, announcedReference) {
|
||||
s.logGameEvent(session, "game-dungeon-boss-die-check-blocked",
|
||||
"dungeon_id", runtime.Dungeon.ID,
|
||||
"room", scene.Coordinate.String(),
|
||||
"map_id", scene.Map.Map.ID,
|
||||
"target_object_key", request.TargetObjectKey,
|
||||
"target_announced", announced,
|
||||
"after_target_death", afterTargetDeath,
|
||||
"room_cleared", scene.Cleared,
|
||||
"tutorial_scene", tutorialScene,
|
||||
"reason", "target_actor_not_authoritatively_defeated")
|
||||
return nil
|
||||
}
|
||||
runtime.bossDieCheckPending = true
|
||||
runtime.bossDieCheckPendingRequest = request
|
||||
s.logGameEvent(session, "game-dungeon-boss-die-check-retained",
|
||||
"dungeon_id", runtime.Dungeon.ID,
|
||||
"maze_index", runtime.MazeIndex,
|
||||
"room", scene.Coordinate.String(),
|
||||
"map_id", scene.Map.Map.ID,
|
||||
"related_actor_object_key", request.RelatedActorObjectKey,
|
||||
"target_object_key", request.TargetObjectKey,
|
||||
"target_kind", announcedReference.Kind,
|
||||
"target_is_ordinary_monster", announcedOrdinaryMonster,
|
||||
"room_cleared", scene.Cleared,
|
||||
"reason", "valid_final_request_arrived_before_same_target_op39")
|
||||
return nil
|
||||
}
|
||||
return s.completeValidatedDungeonBossDieCheckLocked(
|
||||
session,
|
||||
runtime,
|
||||
request,
|
||||
scene,
|
||||
tutorialScene,
|
||||
targetReference,
|
||||
targetIsOrdinaryMonster,
|
||||
targetObjectKey,
|
||||
)
|
||||
}
|
||||
|
||||
func buildCurrentDungeonBossDieCheckResponse(targetObjectKey uint16) []byte {
|
||||
body := make([]byte, currentDungeonBossDieCheckResponseSize)
|
||||
body[0] = 1
|
||||
body[1] = 1
|
||||
binary.LittleEndian.PutUint16(body[2:4], targetObjectKey)
|
||||
return body
|
||||
}
|
||||
Reference in new issue
Block a user