服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
206 lines
5.5 KiB
Go
206 lines
5.5 KiB
Go
package runtimeguard
|
||
|
||
import (
|
||
"errors"
|
||
"sort"
|
||
"strings"
|
||
"sync"
|
||
)
|
||
|
||
// 历史背景:本包之前只暴露 Check / CheckDescriber 这一对无状态函数;wiring 层
|
||
// 要为每一个 runtime 模块(matchmaker、party、crossserver、未来还会更多)手动
|
||
// 调用一次,谁漏调用谁就静默逃过门禁。
|
||
//
|
||
// 全局清单把"声明 — 校验"分成两步:
|
||
// 1. 各 runtime 模块构造完成后立即调用 Register / RegisterDescriber,
|
||
// 把模块名 + 后端类型挂进进程内全局清单。
|
||
// 2. wiring 收尾前调用 Verify(profile) 一次性核对所有声明,违规项被合并
|
||
// 成一个 multi-error 抛出,main 看到 error != nil 直接停机。
|
||
//
|
||
// 这样的好处:
|
||
// - 加新的 runtime 模块不再需要 wiring 代码同步加一行 Check;
|
||
// - 多个违规会一次性报出来,而不是修一条复活一条;
|
||
// - 单元测试可以用 Reset 清空全局状态,避免污染。
|
||
|
||
// Registration 记录一次注册。Source 是可选的调用方标识(比如 main 的 file:line),
|
||
// 主要用来在 Verify 失败时给 ops 一个"到底是谁声明的"的线索。
|
||
type Registration struct {
|
||
Module string
|
||
Backend Backend
|
||
Source string
|
||
}
|
||
|
||
type Catalog struct {
|
||
mu sync.RWMutex
|
||
entries []Registration
|
||
}
|
||
|
||
func NewCatalog() *Catalog {
|
||
return &Catalog{}
|
||
}
|
||
|
||
var defaultCatalog = NewCatalog()
|
||
|
||
// Register 把 module + backend 加进全局清单。生产代码调用,参数静态可见。
|
||
// 重复注册(同 module)允许,最后一个生效;这样在 hot-reload 或重新装配场景
|
||
// 下不会泄漏过时声明。
|
||
func Register(module string, backend Backend) {
|
||
defaultCatalog.Register(module, backend)
|
||
}
|
||
|
||
// RegisterDescriber 是 Register 的便捷形式。describer 为 nil 视为 BackendUnknown。
|
||
func RegisterDescriber(module string, describer BackendDescriber) {
|
||
defaultCatalog.RegisterDescriber(module, describer)
|
||
}
|
||
|
||
func (inv *Catalog) Register(module string, backend Backend) {
|
||
if inv == nil {
|
||
return
|
||
}
|
||
inv.registerInternal(Registration{Module: normalizeModule(module), Backend: backend})
|
||
}
|
||
|
||
func (inv *Catalog) RegisterDescriber(module string, describer BackendDescriber) {
|
||
var b Backend
|
||
if describer != nil {
|
||
b = describer.BackendKind()
|
||
}
|
||
inv.Register(module, b)
|
||
}
|
||
|
||
// RegisterFromSource 与 Register 相同,但额外记录 caller 信息。
|
||
// 推荐 wiring 层使用 "package.Function" 或文件路径作为 source。
|
||
func RegisterFromSource(module string, backend Backend, source string) {
|
||
defaultCatalog.RegisterFromSource(module, backend, source)
|
||
}
|
||
|
||
func (inv *Catalog) RegisterFromSource(module string, backend Backend, source string) {
|
||
if inv == nil {
|
||
return
|
||
}
|
||
inv.registerInternal(Registration{
|
||
Module: normalizeModule(module),
|
||
Backend: backend,
|
||
Source: strings.TrimSpace(source),
|
||
})
|
||
}
|
||
|
||
func (inv *Catalog) registerInternal(reg Registration) {
|
||
if reg.Module == "" {
|
||
return
|
||
}
|
||
inv.mu.Lock()
|
||
defer inv.mu.Unlock()
|
||
for i := range inv.entries {
|
||
if inv.entries[i].Module == reg.Module {
|
||
inv.entries[i] = reg
|
||
return
|
||
}
|
||
}
|
||
inv.entries = append(inv.entries, reg)
|
||
}
|
||
|
||
// Snapshot 返回当前清单的副本,按 module 名排序,便于测试和 admin 报告。
|
||
func Snapshot() []Registration {
|
||
return defaultCatalog.Snapshot()
|
||
}
|
||
|
||
func (inv *Catalog) Snapshot() []Registration {
|
||
if inv == nil {
|
||
return nil
|
||
}
|
||
inv.mu.RLock()
|
||
out := make([]Registration, len(inv.entries))
|
||
copy(out, inv.entries)
|
||
inv.mu.RUnlock()
|
||
sort.Slice(out, func(i, j int) bool { return out[i].Module < out[j].Module })
|
||
return out
|
||
}
|
||
|
||
// Verify 对清单里每条注册都跑一次 Check。所有违规聚合成 errors.Join,
|
||
// 调用方可以用 errors.Is(err, ErrMemoryDistProfile) 判断是否有
|
||
// 至少一个内存后端被拒。
|
||
//
|
||
// 调用方应在装配完成、http server / bus subscribe 启动之前调用一次。
|
||
func Verify(profile Profile) error {
|
||
return defaultCatalog.Verify(profile)
|
||
}
|
||
|
||
func (inv *Catalog) Verify(profile Profile) error {
|
||
snapshot := inv.Snapshot()
|
||
var errs []error
|
||
for _, reg := range snapshot {
|
||
if err := Check(reg.Module, reg.Backend, profile); err != nil {
|
||
if reg.Source != "" {
|
||
err = wrapWithSource(err, reg.Source)
|
||
}
|
||
errs = append(errs, err)
|
||
}
|
||
}
|
||
if len(errs) == 0 {
|
||
return nil
|
||
}
|
||
return errors.Join(errs...)
|
||
}
|
||
|
||
// Reset 清空清单。仅用于测试 / hot-reload。生产代码不应该调用。
|
||
func Reset() {
|
||
defaultCatalog.Reset()
|
||
}
|
||
|
||
func (inv *Catalog) Reset() {
|
||
if inv == nil {
|
||
return
|
||
}
|
||
inv.mu.Lock()
|
||
inv.entries = nil
|
||
inv.mu.Unlock()
|
||
}
|
||
|
||
// Has 返回 module 是否已注册。便于测试断言。
|
||
func Has(module string) bool {
|
||
return defaultCatalog.Has(module)
|
||
}
|
||
|
||
func (inv *Catalog) Has(module string) bool {
|
||
if inv == nil {
|
||
return false
|
||
}
|
||
module = normalizeModule(module)
|
||
inv.mu.RLock()
|
||
defer inv.mu.RUnlock()
|
||
for _, r := range inv.entries {
|
||
if r.Module == module {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func normalizeModule(name string) string {
|
||
return strings.TrimSpace(name)
|
||
}
|
||
|
||
// sourceWrappedError 让 errors.Is 仍能命中底层 sentinel,同时携带 Source。
|
||
type sourceWrappedError struct {
|
||
inner error
|
||
source string
|
||
}
|
||
|
||
func (e *sourceWrappedError) Error() string {
|
||
if e.source == "" {
|
||
return e.inner.Error()
|
||
}
|
||
return e.inner.Error() + " (registered at " + e.source + ")"
|
||
}
|
||
|
||
func (e *sourceWrappedError) Unwrap() error { return e.inner }
|
||
|
||
func wrapWithSource(inner error, source string) error {
|
||
source = strings.TrimSpace(source)
|
||
if inner == nil || source == "" {
|
||
return inner
|
||
}
|
||
return &sourceWrappedError{inner: inner, source: source}
|
||
}
|