Files
90CNServer/server/internal/services/dnfbridge/dungeon_fruz_narke.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

141 lines
4.7 KiB
Go

package dnfbridge
import (
"longheng.io/server/internal/modules/dnf/dnfenum"
"longheng.io/server/internal/modules/dnf/dungeoncmd"
"longheng.io/server/internal/modules/dnf/worldmap"
)
const (
currentFruzStationDungeonID = int64(102)
currentFruzStationNarkeQuestID = int64(3733)
currentFruzStationNarkeActorID = int64(11526)
currentFruzStationObstacleSmall = int64(16109)
currentFruzStationObstacleLarge = int64(16110)
)
func currentFruzStationNarkeObstacleMap(mapID int64) bool {
switch mapID {
case 76503, 76504, 76506:
return true
default:
return false
}
}
func currentFruzStationNarkeActorInRoom(room *runtimeDungeonRoom, objectKey uint32) bool {
if room == nil || objectKey == 0 {
return false
}
for _, actor := range room.Snapshot().ExtendedActors {
if actor.ObjectKey != objectKey || actor.Kind != runtimeDungeonActorAICharacter ||
actor.State != runtimeDungeonMonsterAnnounced || actor.HostileReference != nil ||
actor.Packet.Code != uint32(currentFruzStationNarkeActorID) || actor.AICharacter == nil {
continue
}
return actor.AICharacter.Code == currentFruzStationNarkeActorID &&
normalizeDungeonPVFSymbol(actor.AICharacter.Faction) == "character" &&
normalizeDungeonPVFSymbol(actor.AICharacter.AIType) == "normal"
}
return false
}
// Narke follows the player from the first room while keeping the actor key
// announced there. Check both the live room and cached visits, but accept only
// the exact friendly APC definition owned by quest 3733.
func currentFruzStationRuntimeContainsNarke(runtime *runtimeDungeonState, objectKey uint32) bool {
if runtime == nil || objectKey == 0 {
return false
}
if currentFruzStationNarkeActorInRoom(runtime.Room, objectKey) {
return true
}
for _, visit := range runtime.Rooms {
if visit == nil || visit.Room == nil || visit.Room == runtime.Room {
continue
}
if currentFruzStationNarkeActorInRoom(visit.Room, objectKey) {
return true
}
}
return false
}
// handleCurrentFruzStationNarkeObstacleDeathLocked owns only the native
// variable/count-zero op39 emitted after Narke's grenade hits quest rubble.
// The obstacles are client-side passive objects and are not ordinary runtime
// actors, so they need an op38 receipt without monster, reward, quest or room
// clear mutation. Missing this receipt leaves Narke waiting forever after the
// room's monsters are dead.
func (s *Service) handleCurrentFruzStationNarkeObstacleDeathLocked(
session *gameSession,
runtime *runtimeDungeonState,
scene worldmap.DungeonRoomScene,
request dungeoncmd.DieMonsterRequest,
) (bool, error) {
if s == nil || session == nil || runtime == nil || runtime.Session == nil || runtime.Room == nil ||
session.dungeon.runtime != runtime || !dungeonRuntimeOwnsCharacter(runtime, session.selectedCharacterID) ||
runtime.Dungeon.ID != currentFruzStationDungeonID ||
currentDungeonStoryQuestID(runtime) != currentFruzStationNarkeQuestID ||
!currentFruzStationNarkeObstacleMap(scene.Map.Map.ID) ||
request.Layout != dungeoncmd.DieMonsterRequestLayoutVariableCombat ||
request.CombatEntryCount != 0 {
return false, nil
}
objectID := int64(request.RuntimeObjectKey)
if objectID != currentFruzStationObstacleSmall && objectID != currentFruzStationObstacleLarge {
return false, nil
}
available := currentDungeonScenePassiveObjectCount(scene, objectID)
if available == 0 {
return false, nil
}
ownerIsNarke := request.OwnerObjectKey != ^uint16(0) &&
currentFruzStationRuntimeContainsNarke(runtime, uint32(request.OwnerObjectKey))
if request.OwnerObjectKey != ^uint16(0) && !ownerIsNarke {
return false, nil
}
visit, err := runtime.currentDungeonRoomVisit(scene)
if err != nil {
return false, err
}
responseBody, err := buildCurrentDungeonDeathNotificationBody(
request.RuntimeObjectKey,
currentDungeonDeathResponseAICharacter,
)
if err != nil {
return false, err
}
if err := s.sendGameUpperRawClass(
session,
uint16(dnfenum.CmdPacketNotifyDieMonster),
responseBody,
0,
); err != nil {
return false, err
}
retired := visit.QuestPassiveObjectDeaths[objectID]
if retired < available {
if visit.QuestPassiveObjectDeaths == nil {
visit.QuestPassiveObjectDeaths = make(map[int64]uint32)
}
retired++
visit.QuestPassiveObjectDeaths[objectID] = retired
}
s.logGameEvent(session, "game-dungeon-fruz-narke-obstacle-death-accepted",
"dungeon_id", runtime.Dungeon.ID,
"quest_id", currentFruzStationNarkeQuestID,
"maze_index", runtime.MazeIndex,
"map_id", scene.Map.Map.ID,
"object_key", request.RuntimeObjectKey,
"related_actor_object_key", request.OwnerObjectKey,
"owner_is_narke", ownerIsNarke,
"retired", retired,
"available", available,
"response_opcode", uint16(dnfenum.CmdPacketNotifyDieMonster))
return true, nil
}