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,225 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 本文件为 syncRuntimeRaidMateActorsAllAtSceneBoundary 的 M×(M−1) 双循环提供
|
||||
// 「按演员复用投影 body」的缓存。
|
||||
//
|
||||
// 背景(2026-09-25 测试服实测,8 人攻坚):
|
||||
//
|
||||
// 一次 loaded 边界产生 56 个有序对,每个对都要建两份 body
|
||||
// (mode0 场景对象表 + mode1 角色绑定),每份都带一次 PVF 属性重算。
|
||||
// 一次边界 6.0 秒,全部串行。
|
||||
//
|
||||
// 但两份 body **只由演员决定,除一个字节外**:
|
||||
//
|
||||
// character_scene_object.go:85-90 writeByte(mode0) writeUint16(1)
|
||||
// writeByte(currentSceneObjectRoute) writeByte(ownerChannel)
|
||||
// userinfo_current_modes.go:32-35 writeByte(mode1) writeUint16(1)
|
||||
// writeByte(currentSceneObjectRoute) writeByte(ownerChannel)
|
||||
//
|
||||
// 即两份 body 的**字节 4** 就是 ownerChannel,其余全部来自演员。
|
||||
// 而 ownerChannel = currentTownRemoteActorOwnerContext(target, _) 显式忽略
|
||||
// charID(online_player_manager.go:978-983,第二个参数就是 `_ uint16`),
|
||||
// 只取决于**接收者**。
|
||||
//
|
||||
// ⇒ 缓存键里**不能**含 ownerChannel:那会让每个接收者各建一遍、命中率为零。
|
||||
// 第一版就是这么写的,实测一次边界只从 6.0s 降到 4.5s(那点收益全部来自
|
||||
// mode1 那处 includeEquipment 修复)。正确做法是按演员缓存一份「字节 4 归零」
|
||||
// 的 body,发给每个接收者前把字节 4 补成该接收者的 owner。
|
||||
//
|
||||
// 这样 56 次构建退化成约 M 次,且**最终字节与改动前逐字相同**。
|
||||
//
|
||||
// 缓存生命周期 = 一次扇出(挂在 ctx 上),与 projectionReadCache 同一约定:
|
||||
// 函数返回即失效,绝不跨请求留存演员状态。
|
||||
|
||||
// actorProjectionOwnerChannelOffset 是两份 body 里 ownerChannel 的字节偏移,
|
||||
// 由上面两个 writer 的逐行核实得出。
|
||||
const actorProjectionOwnerChannelOffset = 4
|
||||
|
||||
// actorProjectionBodies 是一个键对应的两份 body(字节 4 已归零)。
|
||||
type actorProjectionBodies struct {
|
||||
mode0 []byte
|
||||
mode1 []byte
|
||||
}
|
||||
|
||||
// actorProjectionBodiesKey 是缓存键:只含**演员维度**的输入。
|
||||
//
|
||||
// charID —— 演员角色 ID(body 的决定性输入)
|
||||
// binding —— withBinding(决定 mode1 是否带 ~31 行装备行,见
|
||||
//
|
||||
// sendTownRemoteActorProjectionWithContext)
|
||||
type actorProjectionBodiesKey struct {
|
||||
charID uint16
|
||||
withBinding bool
|
||||
}
|
||||
|
||||
type actorProjectionBodiesCache struct {
|
||||
entries map[actorProjectionBodiesKey]actorProjectionBodies
|
||||
|
||||
// 诊断计数:回答「键为什么没复用」。
|
||||
//
|
||||
// 按 withBinding 分开统计,因为 withBinding 由**接收者**场景决定
|
||||
// (runtimeRaidPeerActorInstallModeFor(state, target))。如果它随接收者变化,
|
||||
// 同一个演员就会被拆成多个键、命中率暴跌 —— 这正是要证伪的假设。
|
||||
mu sync.Mutex
|
||||
hitNoBinding int
|
||||
hitBinding int
|
||||
missNoBinding int
|
||||
missBinding int
|
||||
buildNanos int64
|
||||
sendNanos int64
|
||||
}
|
||||
|
||||
func (cache *actorProjectionBodiesCache) noteHit(withBinding bool) {
|
||||
cache.mu.Lock()
|
||||
if withBinding {
|
||||
cache.hitBinding++
|
||||
} else {
|
||||
cache.hitNoBinding++
|
||||
}
|
||||
cache.mu.Unlock()
|
||||
}
|
||||
|
||||
func (cache *actorProjectionBodiesCache) noteMiss(withBinding bool) {
|
||||
cache.mu.Lock()
|
||||
if withBinding {
|
||||
cache.missBinding++
|
||||
} else {
|
||||
cache.missNoBinding++
|
||||
}
|
||||
cache.mu.Unlock()
|
||||
}
|
||||
|
||||
// logSummary 在一次扇出结束时打一行计数,用于判定缓存是否真的复用上了。
|
||||
func (cache *actorProjectionBodiesCache) logSummary(s *Service, source string) {
|
||||
if cache == nil || s == nil {
|
||||
return
|
||||
}
|
||||
cache.mu.Lock()
|
||||
hitNo, hitYes := cache.hitNoBinding, cache.hitBinding
|
||||
missNo, missYes := cache.missNoBinding, cache.missBinding
|
||||
entries := len(cache.entries)
|
||||
buildNanos, sendNanos := cache.buildNanos, cache.sendNanos
|
||||
cache.mu.Unlock()
|
||||
s.logPacketEvent("game-actor-body-cache-summary",
|
||||
"source", source,
|
||||
"hit_nobind", hitNo,
|
||||
"hit_bind", hitYes,
|
||||
"miss_nobind", missNo,
|
||||
"miss_bind", missYes,
|
||||
"distinct_keys", entries,
|
||||
"build_ms", buildNanos/1e6,
|
||||
"send_ms", sendNanos/1e6)
|
||||
}
|
||||
|
||||
func newActorProjectionBodiesCache() *actorProjectionBodiesCache {
|
||||
return &actorProjectionBodiesCache{
|
||||
entries: make(map[actorProjectionBodiesKey]actorProjectionBodies),
|
||||
}
|
||||
}
|
||||
|
||||
type actorProjectionBodiesCtxKey struct{}
|
||||
|
||||
// withActorProjectionBodiesCache 为一次扇出挂上 body 缓存。
|
||||
func withActorProjectionBodiesCache(ctx context.Context) context.Context {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
return context.WithValue(ctx, actorProjectionBodiesCtxKey{}, newActorProjectionBodiesCache())
|
||||
}
|
||||
|
||||
// actorProjectionBodiesCacheFromContext 取当前扇出的缓存;没有则返回 nil,
|
||||
// 此时 lookup 一律未命中、store 是空操作 —— 也就是**改动前的原始行为**。
|
||||
func actorProjectionBodiesCacheFromContext(ctx context.Context) *actorProjectionBodiesCache {
|
||||
if ctx == nil {
|
||||
return nil
|
||||
}
|
||||
cache, _ := ctx.Value(actorProjectionBodiesCtxKey{}).(*actorProjectionBodiesCache)
|
||||
return cache
|
||||
}
|
||||
|
||||
// lookup 命中时返回**缓存内容的副本**。
|
||||
//
|
||||
// 副本是刻意的:调用方会把切片交给发包路径,缓存自己必须留一份不受影响的
|
||||
// 权威拷贝,否则任何下游对切片的写入都会污染后续所有接收者。一次约 500 字节
|
||||
// 的 memcpy 相比重新构建(含 PVF 重算与仓储读)可以忽略。
|
||||
func (cache *actorProjectionBodiesCache) lookup(
|
||||
key actorProjectionBodiesKey,
|
||||
) ([]byte, []byte, bool) {
|
||||
if cache == nil {
|
||||
return nil, nil, false
|
||||
}
|
||||
cache.mu.Lock()
|
||||
entry, ok := cache.entries[key]
|
||||
cache.mu.Unlock()
|
||||
if !ok {
|
||||
cache.noteMiss(key.withBinding)
|
||||
return nil, nil, false
|
||||
}
|
||||
cache.noteHit(key.withBinding)
|
||||
return append([]byte(nil), entry.mode0...), append([]byte(nil), entry.mode1...), true
|
||||
}
|
||||
|
||||
// store 保存两份 body 的副本,并把 ownerChannel 字节归零。
|
||||
//
|
||||
// 归零是刻意的:缓存代表**演员维度**的内容,接收者维度由
|
||||
// applyActorProjectionOwnerChannel 在取出后补上。
|
||||
func (cache *actorProjectionBodiesCache) store(
|
||||
key actorProjectionBodiesKey, mode0, mode1 []byte,
|
||||
) {
|
||||
if cache == nil {
|
||||
return
|
||||
}
|
||||
cache.mu.Lock()
|
||||
cache.entries[key] = actorProjectionBodies{
|
||||
mode0: zeroActorProjectionOwnerChannel(mode0),
|
||||
mode1: zeroActorProjectionOwnerChannel(mode1),
|
||||
}
|
||||
cache.mu.Unlock()
|
||||
}
|
||||
|
||||
// noteBuild / noteSend 累加「建 body」与「发包」各自花掉的时间。
|
||||
//
|
||||
// 这是为了回答「4.3 秒到底在构建还是在投递」。缓存已经把构建从 56 次降到 8 次
|
||||
// (hit 48 / miss 8),如果 noteSend 仍占绝大部分,那剩下的就是同步写进每个
|
||||
// 接收者 writer 的等待(gameWriterSyncBudget=250ms),与构建无关。
|
||||
func (cache *actorProjectionBodiesCache) noteBuild(d time.Duration) {
|
||||
if cache == nil || d <= 0 {
|
||||
return
|
||||
}
|
||||
cache.mu.Lock()
|
||||
cache.buildNanos += d.Nanoseconds()
|
||||
cache.mu.Unlock()
|
||||
}
|
||||
|
||||
func (cache *actorProjectionBodiesCache) noteSend(d time.Duration) {
|
||||
if cache == nil || d <= 0 {
|
||||
return
|
||||
}
|
||||
cache.mu.Lock()
|
||||
cache.sendNanos += d.Nanoseconds()
|
||||
cache.mu.Unlock()
|
||||
}
|
||||
|
||||
func zeroActorProjectionOwnerChannel(body []byte) []byte {
|
||||
stored := append([]byte(nil), body...)
|
||||
if len(stored) > actorProjectionOwnerChannelOffset {
|
||||
stored[actorProjectionOwnerChannelOffset] = 0
|
||||
}
|
||||
return stored
|
||||
}
|
||||
|
||||
// applyActorProjectionOwnerChannel 把 body 的 ownerChannel 字节补成接收者的值。
|
||||
//
|
||||
// 该字节是两份 body 里**唯一**与接收者相关的值(已逐行核实两个 writer)。
|
||||
// body 短于该偏移时不改,与改动前保持一致。
|
||||
func applyActorProjectionOwnerChannel(body []byte, ownerChannel byte) {
|
||||
if len(body) > actorProjectionOwnerChannelOffset {
|
||||
body[actorProjectionOwnerChannelOffset] = ownerChannel
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user