服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
113 lines
4.4 KiB
Go
113 lines
4.4 KiB
Go
package dnfbridge
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"time"
|
|
)
|
|
|
|
// preloadDungeonDropItemDefinitions 在监听端口打开前,把 itemRefs 索引里的全部道具
|
|
// 定义解析好并填入 itemCache。
|
|
//
|
|
// 背景(2026-09-26 实测:本机 + 203.0.113.13 对照,卡顿栈采样器抓到完整栈)
|
|
//
|
|
// 进城引导
|
|
// → sendCurrentSelectInventoryBootstrap select_character_inventory_bootstrap.go:117
|
|
// → buildCurrentItemListBodyForSession current_item_list_session.go:137
|
|
// → reconcileCurrentPVFItemExpirationsBestEffort :358
|
|
// → 账号/角色资产事务包装器 (MySQL 事务内)
|
|
// → projectCurrentPVFItemExpirations item_expiration_owner.go:91
|
|
// → currentPVFDefinitionForItem :19
|
|
// → pvfDungeonDropCatalog.ResolveItem dungeon_drop_catalog.go:405
|
|
// → readDungeonDropText(读 PVF 文本)+ dnfpvf.Parse(解析文档)
|
|
//
|
|
// 该调用点对本角色的**每一个**背包 / 仓库 / 账号金库 / 装备栏条目各调一次
|
|
// ResolveItem。ResolveItem 本身有 itemCache,但**进程内首次为空**,于是全部落到冷路径。
|
|
// 单角色约 490 个不同道具,实测该段在本机占 419ms,在低配 VPS 上占到 2.4 秒
|
|
// (那边是 4 核 4 线程 / 4GB 内存且已超配,冷访问大量落在页面文件上)。
|
|
//
|
|
// 所以这里在启动期把 itemCache 一次性填满:之后那约 490 次调用全部变成 map 命中,
|
|
// 玩家侧的这段开销归零。
|
|
//
|
|
// 取向与官方端一致:df_game_r 由 main -> Init(int,char**) ->
|
|
// ARAD::ScriptManager::LoadScript() 在启动期把每个子系统脚本过一遍,出错就拒绝启动。
|
|
//
|
|
// 代价(刻意接受):itemRefs 有 195,651 条(见启动事件
|
|
// dnf-pvf-item-catalog-loaded item_refs=...),全量解析约 0.85ms/条,即
|
|
// **启动多花约 2~3 分钟、常驻内存增加约 100MB**。启动慢可以接受,玩家侧卡顿不可以。
|
|
//
|
|
// 失败语义:**单个道具解析失败只记 WARN 并计数,不让服务起不来。**
|
|
// 理由与 warmOptionalPVFCatalogs 一致——新增一条启动失败路径,等于把
|
|
// 「某个道具文件写坏了」从「那个道具不可用」升级成「整台服务器起不来」,
|
|
// 那是比卡顿严重得多的回归。本函数只在「索引根本拿不到」时返回 error
|
|
// (那本来就是致命故障)。
|
|
func (s *Service) preloadDungeonDropItemDefinitions(ctx context.Context) error {
|
|
if s == nil {
|
|
return nil
|
|
}
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
catalog, err := s.currentPVFItemCatalog()
|
|
if err != nil {
|
|
return fmt.Errorf("preload dungeon drop item definitions: %w", err)
|
|
}
|
|
if catalog == nil {
|
|
return nil
|
|
}
|
|
ids := catalog.dungeonDropItemIDs()
|
|
started := time.Now()
|
|
resolved, unresolved, failed := 0, 0, 0
|
|
for _, itemID := range ids {
|
|
// 启动期 ctx 被取消说明服务正在关闭,早点退出,别拖住关闭流程。
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
if _, resolveErr := catalog.ResolveItem(itemID); resolveErr != nil {
|
|
if errors.Is(resolveErr, errDungeonDropItemUnresolved) {
|
|
unresolved++
|
|
continue
|
|
}
|
|
failed++
|
|
// 只报前几条,避免一个坏 PVF 刷出二十万行日志。
|
|
if failed <= 5 {
|
|
s.logWarn("dnfbridge dungeon drop item definition unusable",
|
|
"item_id", itemID, "error", resolveErr)
|
|
}
|
|
continue
|
|
}
|
|
resolved++
|
|
}
|
|
s.logPacketEvent("dnf-dungeon-drop-item-definitions-preloaded",
|
|
"items", len(ids),
|
|
"resolved", resolved,
|
|
"unresolved", unresolved,
|
|
"failed", failed,
|
|
"elapsed_ms", time.Since(started).Milliseconds())
|
|
return nil
|
|
}
|
|
|
|
// dungeonDropItemIDs 返回 itemRefs 索引里全部道具 ID 的稳定快照(升序)。
|
|
//
|
|
// 只在锁内取一次键集合,真正的解析在锁外逐个进行——ResolveItem 自己会加锁,
|
|
// 若在这里持锁遍历两小时,会把启动期其他读该 catalog 的路径全堵死。
|
|
// 升序是为了让启动日志与内存布局可复现(map 遍历顺序随机会让排查困难)。
|
|
func (c *pvfDungeonDropCatalog) dungeonDropItemIDs() []uint32 {
|
|
if c == nil {
|
|
return nil
|
|
}
|
|
c.mu.Lock()
|
|
ids := make([]uint32, 0, len(c.itemRefs))
|
|
for itemID := range c.itemRefs {
|
|
ids = append(ids, itemID)
|
|
}
|
|
c.mu.Unlock()
|
|
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
|
|
return ids
|
|
}
|