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,153 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// warmOptionalPVFCatalogs 在监听端口打开前,把「否则会拖到玩家第一个请求里才建」的
|
||||
// PVF 目录先建好。
|
||||
//
|
||||
// 背景(2026-09-26 本机测试服实测)
|
||||
//
|
||||
// 服务重启后第一次进城镇:op143 ENUM_CMDPACKET_CHANGE_TUTORIAL_FLAG = 1589ms
|
||||
// 同一客户端 30 秒后第二次进城: 同一 op143 = 491ms
|
||||
//
|
||||
// 同一份 packet_log.txt 显示差额来自三个目录被懒加载在那一次进城请求里:
|
||||
// cera 商店目录(前后空档 308ms)、消费时间目录、称号簿映射。
|
||||
// 也就是说 ~1100ms 是「解析 + 建索引」的账,而且它发生在玩家自己的会话事件循环上
|
||||
// ——那 1.59 秒里该会话的事件循环完全堵死,客户端收不到任何东西。
|
||||
//
|
||||
// 参照官方端 df_game_r:它对每个子系统都有一个**显式命名的加载入口**
|
||||
// (importCashShopItemList / importAttendanceScript / importTitleBookEtcScript …),
|
||||
// 由 main -> Init(int,char**) -> ScriptManager::LoadScript() 在启动期统一跑完,
|
||||
// 而不是留一个懒单例等玩家去触发。
|
||||
//
|
||||
// 为什么不加进 pvfPreloads 表
|
||||
//
|
||||
// 那张表(见 Start)把 loader 返回的 error 当作**致命错误**:会取消 ctx、关闭
|
||||
// packet log 并拒绝启动。把这三个**可选功能**加进去,等于让「PVF 里没有商店数据」
|
||||
// 从「商城不可用」升级成「整台服务器起不来」——那是比卡顿严重得多的回归。
|
||||
//
|
||||
// 所以这里只做**尽力而为**的预热,并且比懒路径更保守:
|
||||
// - 返回 error → 只记一条 WARN,不中断启动;
|
||||
// - panic → 由 warmOptionalCatalog 兜住,只记一条 WARN,不中断启动。
|
||||
//
|
||||
// 三个懒访问器保持原语义(首次使用时重试,或返回同一个已经记下的错误),
|
||||
// 改动前后它们的对外行为逐字节等价。
|
||||
//
|
||||
// 调用位置的两个硬约束
|
||||
//
|
||||
// 1. 必须在 pvfPreloads 之后。currentCeraShopCatalog 与消费时间目录都要读
|
||||
// s.initialEquipmentArchive,而它由该表第 1 项 preloadInitialEquipmentIndex 打开。
|
||||
// 提前调用会让 LoadArchive 的失败被永久缓存进 initialEquipmentLoadErr
|
||||
// (见 initialEquipmentArchiveLocked),那才会真的修 A 坏 B。
|
||||
// 2. 必须在 FreeOSMemory 边界之前。那条既有注释写明该边界就是用来回收启动期这些
|
||||
// 「短命 PVF 文档索引」的,预热属于同一类工作,不该落在边界之外。
|
||||
func (s *Service) warmOptionalPVFCatalogs(ctx context.Context) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// cera 商店目录。成功时它自己会打 dnf-cera-shop-pvf-catalog-loaded。
|
||||
// 失败时不做补偿:错误已被 currentCeraShopCatalog 记进 ceraShopCatalogLoadErr,
|
||||
// 与玩家首次打开商城时失败的结果完全相同——只是提前到了启动期。
|
||||
s.warmOptionalCatalog("cera shop catalog", func() {
|
||||
if _, err := s.currentCeraShopCatalog(); err != nil {
|
||||
s.logWarn("dnfbridge cera shop catalog warmup failed", "error", err)
|
||||
}
|
||||
})
|
||||
|
||||
// 商城限购扫描:把 cera 商店目录**自带的那一份**道具解析器的缓存填满。
|
||||
//
|
||||
// 背景(2026-09-26 卡顿栈采样器抓到,conn=game-000364,本地 13:36:15.678):
|
||||
//
|
||||
// readDungeonDropText
|
||||
// → (*pvfDungeonDropCatalog).ResolveItem
|
||||
// → (*pvfCeraShopCatalog).BuyRestrictPolicies
|
||||
// → (*Service).currentCeraShopSoldOutEntries
|
||||
// → (*Service).sendCurrentCeraShopBuyRestrictProjection
|
||||
//
|
||||
// 这一段跑在会话事件循环上,实占 445ms,是「重启后第一次进城」窗口里最大的一笔。
|
||||
//
|
||||
// 为什么既有的全量道具预热盖不住它:newPVFCeraShopCatalog 在 cera_shop_catalog.go:78
|
||||
// **自己**调了一次 newPVFDungeonDropCatalog,于是进程里存在**两份互相独立的
|
||||
// itemRefs / itemCache**——服务级那份见 currentPVFItemCatalog,商城那份挂在
|
||||
// pvfCeraShopCatalog.items。preloadDungeonDropItemDefinitions 只填服务级那份,
|
||||
// 所以商城这条路径照旧逐条冷读 PVF 文本 + dnfpvf.Parse,一字不减。
|
||||
//
|
||||
// 这里把同一段扫描在启动期**空跑一遍**(空角色/账号数据,结果直接丢弃),
|
||||
// 目的只是让那三万余个商品各自的道具定义落进私有 catalog 的 itemCache。
|
||||
// 它只读不写:currentCeraShopSoldOutEntries 不碰任何持久化状态,
|
||||
// ResolveItem 的副作用也只有它自己的缓存。
|
||||
//
|
||||
// 代价 ≈ 该扫描本身的耗时(本机实测 445ms 启动时间),换来的是重启后第一个
|
||||
// 进城的玩家不再承担它(同一段在低配 VPS 上实测 2.4 秒)。
|
||||
s.warmOptionalCatalog("cera shop buy restrict sweep", func() {
|
||||
catalog, err := s.currentCeraShopCatalog()
|
||||
if err != nil || catalog == nil {
|
||||
// 目录本身没建起来:上一步已经记过 WARN,这里不重复报。
|
||||
return
|
||||
}
|
||||
started := time.Now()
|
||||
entries := s.currentCeraShopSoldOutEntries(catalog, nil, nil, s.currentPurchaseLimitTime())
|
||||
// 这一行是**验收依据**:没有它,本次改动在日志上完全不可见,
|
||||
// 只能靠"第一次进城少了 445ms"间接推断。elapsed_ms 应当与
|
||||
// 玩家侧原本承担的那一段同量级(本机 ≈445ms)。
|
||||
// entries 用空角色/账号数据跑,正常应为 0——非 0 说明有人拿
|
||||
// "已买满"判定依赖了空 map 的零值,那是个需要单独看的信号。
|
||||
s.logPacketEvent("dnf-cera-shop-buy-restrict-sweep-preloaded",
|
||||
"products", len(catalog.products),
|
||||
"entries_with_empty_stats", len(entries),
|
||||
"elapsed_ms", time.Since(started).Milliseconds())
|
||||
})
|
||||
|
||||
// 消费时间目录,三态返回 (state, available, err):
|
||||
// available == false 表示「运行时 PVF 归档尚未打开」,是**延后**而不是失败,
|
||||
// 调用方会 defer 到下一次、且不会缓存任何东西。预热阶段本不该出现它
|
||||
// (pvfPreloads 已打开归档),真出现也只记一条。
|
||||
//
|
||||
// 刻意传 runCtx 而**不**像懒路径那样给超时 ctx:LoadAttendancePVFCatalog 一旦
|
||||
// 因 ctx 取消而返回错误,该错误会被永久写进 spendTimeCatalogLoadErr,
|
||||
// 等于把一个本可重试的失败变成永久失败。runCtx 只在服务关闭时取消,
|
||||
// 不会引入这种风险。
|
||||
s.warmOptionalCatalog("spend time catalog", func() {
|
||||
if _, available, err := s.currentSpendTimeRuntimeCatalogIfAvailable(ctx); err != nil {
|
||||
s.logWarn("dnfbridge spend time catalog warmup failed", "error", err)
|
||||
} else if !available {
|
||||
s.logWarn("dnfbridge spend time catalog warmup deferred",
|
||||
"reason", "runtime_pvf_archive_not_loaded")
|
||||
}
|
||||
})
|
||||
|
||||
// 称号簿映射:无 error 返回。失败时返回 nil 且**不缓存**(titleBookMappingCache
|
||||
// 只在成功解析后才赋值),下一次调用会重试,因此这里不需要也不应该做任何补偿。
|
||||
s.warmOptionalCatalog("title book mapping", func() {
|
||||
s.currentTitleBookMapping()
|
||||
})
|
||||
}
|
||||
|
||||
// warmOptionalCatalog 跑一次预热,把 panic 降级成一条 WARN。
|
||||
//
|
||||
// 为什么必须自己 recover:这三个访问器在正常服务里只在会话事件循环内被调用,
|
||||
// 而事件循环自带 recover(见 eventloop 的 recordProcessed 收 panicked 参数)——
|
||||
// 也就是说解析器即使 panic,今天最坏结果也只是毁掉那一个请求。Start 里没有这层
|
||||
// 保护,直接调用会把同一个 panic 升级成「服务器起不来」:那是比卡顿严重得多的回归。
|
||||
//
|
||||
// 兜住之后最坏情况退化成「这一次预热被跳过」,懒访问器会在玩家首次使用时重试,
|
||||
// 与改动前的行为一致。
|
||||
func (s *Service) warmOptionalCatalog(name string, run func()) {
|
||||
defer func() {
|
||||
if recovered := recover(); recovered != nil {
|
||||
s.logWarn("dnfbridge optional catalog warmup panicked",
|
||||
"catalog", name, "panic", recovered)
|
||||
}
|
||||
}()
|
||||
run()
|
||||
}
|
||||
Reference in new issue
Block a user