DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

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

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

No files matched your search

@@ -0,0 +1,349 @@
package dnfbridge
// raid_log.go —— 团本(攻坚战)专用日志。
//
// 为什么单独开一类
//
// 攻坚相关事件以前全部混在 packet_log.txt 里(那台机器一天 6~10 GB),排查时要在
// 几十万行里翻几十条,且真正有用的字段(谁在等谁、名单几个人、就绪几个、
// 驱动者是谁、某个成员为什么没就绪)当时根本没记。本文件把团本事件单独落一个
// 文件(records/raid-YYYYMMDD-NNN.jsonl),字段尽量全,并且不阻塞玩法。
//
// 与 packet_log 的关系
//
// 同一事件仍然照常进 packet_log(保留原样,便于和客户端 trace 对齐),
// 但高频、低信息量的那些由 suppressPacketLogEvent 过滤掉,降低 packet_log 噪声。
//
// 三条纪律(沿用 activity_record.go 的既有约定)
//
// 1. 旁路:记录器未启用(records == nil)时,所有函数立刻返回,代价是一次指针判空。
// 2. 不阻塞:写入是 channel 入队 + 单协程落盘;这里只做 map 组装。
// 3. 锁安全:**本文件里的函数绝不主动获取 session.dungeon.mu**。
// 攻坚记录大量发生在已经持有 dungeon.mu 的路径上(例如 release 与
// 怪物死亡投影),在那里再取一次锁就是死锁风险。需要 runtime 字段时,
// 由调用方(已经持有 runtime)通过 raidStateFields 显式传进来。
//
// 字段命名约定
//
// raid_key / raid_phase / lifecycle_status / lifecycle_revision / roster_size /
// frozen_size / online_members / squad / slot / is_leader / leader_id /
// party_id / conn_id / character_id / character_name / channel_id /
// event / event_name / reason / result / wall_ms / error
import (
"fmt"
"time"
)
// recordRaidKind 是团本记录的文件类别(records/raid-*.jsonl)。
const recordRaidKind = recordKindRaid
// raidEventName 把事件名翻成中文,便于直接看文件排查。
func raidEventName(event string) string {
switch event {
// 生命周期
case "raid_created":
return "创建攻坚队"
case "raid_disbanded":
return "攻坚队解散"
case "raid_started":
return "开始攻坚"
case "raid_start_rejected":
return "开始攻坚被拒"
case "raid_phase_advanced":
return "阶段推进"
case "raid_phase_timeout":
return "阶段攻击超时"
case "raid_phase_force_return":
return "阶段收尾强制回城"
case "raid_cleared":
return "攻坚通关"
// 副本门(gate)生命周期
case "gate_selected":
return "选中副本门"
case "gate_loaded":
return "副本门加载完成"
case "gate_completed":
return "副本门通关"
case "gate_abandoned":
return "副本门放弃/被踢出"
case "gate_boundary_fanout":
return "门边界演员扇出"
// 读条 / 进图
case "loading_ready":
return "成员读条完成登记"
case "loading_released":
return "读条放行完成"
case "loading_release_driver":
return "读条放行驱动者"
case "loading_release_skipped":
return "读条放行跳过"
case "loading_barrier_timeout":
return "读条屏障超时"
case "entry_blocked":
return "进图被拒"
case "entry_prepared":
return "进图已准备"
case "forced_exit":
return "被强制移出副本"
case "forced_return":
return "被强制回城"
// 队伍 / 端点 / 中继
case "party_formed":
return "组队完成"
case "party_publish_deferred":
return "队伍发布被推迟"
case "party_peer_endpoint":
return "队友端点登记"
case "party_peer_endpoint_missing":
return "队友端点缺失"
case "party_member_removed":
return "成员被移出队伍"
case "party_kick_result":
return "踢人结果"
case "relay_leg_missing":
return "中继腿缺失"
case "relay_leg_unresolved":
return "中继腿重试仍失败"
case "relay_throttled":
return "中继被限流"
case "town_return_copresence":
return "回城共现重建"
case "scene_tail_deferred":
return "场景尾巴包被推迟"
// 奖励
case "session_cost_summary":
return "本场服务端成本汇总"
case "movie_confirm_fallback":
return "过场确认超时兜底开窗"
case "reward_cards_opened":
return "翻牌窗口打开"
case "reward_cards_open_skipped":
return "翻牌窗口被跳过"
case "reward_auto_flipped":
return "自动翻牌"
case "reward_manual_flip":
return "手动翻牌"
case "reward_delivered":
return "奖励已发放"
case "reward_incomplete":
return "奖励不完整"
default:
return event
}
}
// raidLifecycleStatusName 攻坚生命周期状态中文名。
func raidLifecycleStatusName(status runtimeRaidLifecycleStatus) string {
switch status {
case runtimeRaidLifecycleCreated:
return "已创建"
case runtimeRaidLifecyclePreparing:
return "准备中"
case runtimeRaidLifecyclePhaseActive:
return "阶段进行中"
case runtimeRaidLifecycleAwaitingMovie:
return "等待过场"
case runtimeRaidLifecycleAwaitingRewards:
return "等待翻牌"
case runtimeRaidLifecycleStandby:
return "待命"
case runtimeRaidLifecycleFinished:
return "已完成"
case runtimeRaidLifecycleAbandoned:
return "已放弃"
case runtimeRaidLifecycleFailed:
return "失败(攻击超时)"
default:
return fmt.Sprintf("状态%d", int(status))
}
}
// raidStateFields 从一份**调用方已经持有**的攻坚状态里取出排查所需字段。
//
// 调用方必须自己保证读 state 的并发安全(本函数不加锁,也不复制整份状态)。
// 缺字段就留空——记录是旁路,不为凑字段去多查一次库或抢一次锁。
func raidStateFields(state *runtimeRaidState, characterID uint16) map[string]any {
fields := map[string]any{}
if state == nil {
return fields
}
fields["raid_key"] = state.RaidKey
fields["raid_phase"] = int(state.Lifecycle.Phase)
fields["lifecycle_status"] = int(state.Lifecycle.Status)
fields["lifecycle_status_name"] = raidLifecycleStatusName(state.Lifecycle.Status)
fields["lifecycle_revision"] = state.Lifecycle.Revision
fields["raid_started"] = state.Started
fields["entry_cost_paid"] = state.EntryCostPaid
fields["start_attempt"] = state.StartAttempt
fields["leader_id"] = state.LeaderID
fields["roster_size"] = len(state.Members)
fields["frozen_size"] = len(state.FrozenMembers)
if len(state.NameBytes) != 0 {
fields["raid_name"] = string(state.NameBytes)
}
if characterID != 0 {
fields["is_leader"] = characterID == state.LeaderID
squad, slot, found := 0, 0, false
for _, member := range state.Members {
if member.CharID != characterID {
continue
}
squad = int(member.GroupIndex)
slot = int(member.SlotOrder)
found = true
break
}
if found {
fields["squad"] = squad
fields["slot"] = slot
if state.SquadPartyLeaders != nil {
if partyLeader := state.SquadPartyLeaders[byte(squad)]; partyLeader != 0 {
fields["squad_party_leader"] = partyLeader
}
}
}
}
return fields
}
// raidOnlineMembers 数一遍当前在线的攻坚成员。
//
// 会取 s.mu(onlineGameSession 内部),因此**只在本来就安全的调用点使用**:
// 创建/解散/开始/阶段推进/奖励这类低频事件。高频路径(门边界扇出、读条放行)
// 不要调用它,避免在持有 dungeon.mu 的调用栈里引入新的锁序。
func (s *Service) raidOnlineMembers(state *runtimeRaidState) (online int, total int) {
if s == nil || state == nil {
return 0, 0
}
for _, member := range state.FrozenMembers {
total++
if session, ok := s.onlineGameSession(member.CharID); ok && session != nil && session.conn != nil {
online++
}
}
if total == 0 {
for _, member := range state.Members {
total++
if session, ok := s.onlineGameSession(member.CharID); ok && session != nil && session.conn != nil {
online++
}
}
}
return online, total
}
// recordRaidEvent 记一条团本事件(轻量上下文:只读会话上已有的普通字段)。
func (s *Service) recordRaidEvent(session *gameSession, event string, fields map[string]any) {
if s == nil || s.records.Load() == nil {
return
}
merged := s.activitySessionFields(session)
if session != nil {
merged["conn_id"] = session.connID
merged["character_generation"] = session.characterGeneration
merged["session_online"] = session.conn != nil
}
merged["event"] = event
merged["event_name"] = raidEventName(event)
for key, value := range fields {
merged[key] = value
}
s.recordActivity(recordRaidKind, merged)
}
// recordRaidStateEvent 记一条团本事件,并带上调用方持有的攻坚状态与在线人数。
// 只用于低频事件(见 raidOnlineMembers 的说明)。
func (s *Service) recordRaidStateEvent(session *gameSession, event string, state *runtimeRaidState, fields map[string]any) {
if s == nil || s.records.Load() == nil {
return
}
merged := fields
if merged == nil {
merged = map[string]any{}
}
charID := uint16(0)
if session != nil {
charID = session.selectedCharacterID
}
for key, value := range raidStateFields(state, charID) {
merged[key] = value
}
if online, total := s.raidOnlineMembers(state); total > 0 {
merged["online_members"] = online
merged["roster_members"] = total
}
s.recordRaidEvent(session, event, merged)
}
// recordRaidStateEventLight 与 recordRaidStateEvent 相同,但**不**去数在线人数。
//
// 热路径/可能持有 session.dungeon.mu 的调用栈必须用这个版本:数在线人数要取 s.mu,
// 在已持有 dungeon.mu 的栈里再取 s.mu 会引入新的锁序。
func (s *Service) recordRaidStateEventLight(session *gameSession, event string, state *runtimeRaidState, fields map[string]any) {
if s == nil || s.records.Load() == nil {
return
}
merged := fields
if merged == nil {
merged = map[string]any{}
}
charID := uint16(0)
if session != nil {
charID = session.selectedCharacterID
}
for key, value := range raidStateFields(state, charID) {
merged[key] = value
}
s.recordRaidEvent(session, event, merged)
}
// raidMillis 把时长写成毫秒(float,保留一位小数),便于直接排序/筛选。
func raidMillis(d time.Duration) float64 {
return float64(d.Microseconds()) / 1000
}
// raidWallField 组装"这次操作花了多久"。
func raidWallField(d time.Duration) map[string]any {
return map[string]any{"wall_ms": raidMillis(d)}
}
// errorText 把 error 写成可读字符串(nil 记空串,便于 JSON 里直接看)。
func errorText(err error) string {
if err == nil {
return ""
}
return err.Error()
}
// raidMemberIDs 把成员列表压成 [1 12 22] 这样的数组,便于看名单。
func raidMemberIDsFromState(members []runtimeRaidMemberState) []uint16 {
out := make([]uint16, 0, len(members))
for _, member := range members {
out = append(out, member.CharID)
}
return out
}
// raidBitsCount 数一个 8 位槽位位图里有几个人(队伍最多 4 人,8 位足够)。
func raidBitsCount(mask uint8) int {
count := 0
for bit := uint8(1); bit != 0; bit <<= 1 {
if mask&bit != 0 {
count++
}
}
return count
}
// raidMissingSlots 返回"期望到齐但还没登记"的槽位,用于直接点名谁卡住了。
func raidMissingSlots(ready, expected uint8) []int {
missing := make([]int, 0, 8)
for slot := 0; slot < 8; slot++ {
bit := uint8(1) << slot
if expected&bit != 0 && ready&bit == 0 {
missing = append(missing, slot)
}
}
return missing
}