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,115 @@
|
||||
// Package runtimeguard 提供"多节点部署是否使用了进程内内存后端"这一类启动期断言。
|
||||
//
|
||||
// 历史背景:matchmaker、party、crossserver、aoi、framesync 等 runtime 模块的默认实现是内存版,
|
||||
// 仅在单节点演示和测试场景下正确。生产部署一旦超过一个 logic 节点,内存后端会
|
||||
// 静默产生分裂脑(同一 user 被两个节点分别匹配成功、队伍状态读到不同副本等)。
|
||||
// 框架历史上只在源码注释里提示"多节点请改用 Redis 版本",没有任何启动期校验。
|
||||
//
|
||||
// 本包暴露 Backend 枚举、BackendDescriber 接口和 Check / MustCheck 入口,让各
|
||||
// runtime 模块声明自己当前的后端类型;wiring 层可在启动时统一调用 Check,对
|
||||
// 多节点 Profile 下仍然挂着内存后端的模块直接 fail-fast。
|
||||
package runtimeguard
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Backend 标识一个 runtime 模块当前实际使用的存储后端。
|
||||
type Backend string
|
||||
|
||||
const (
|
||||
// BackendMemory 表示纯进程内 map / sync.Mutex 实现,仅单节点安全。
|
||||
BackendMemory Backend = "memory"
|
||||
// BackendRedis 表示通过 Redis Lua / Hash / Sorted Set 维护跨节点一致性的实现。
|
||||
BackendRedis Backend = "redis"
|
||||
// BackendMySQL 表示通过 MySQL 事务 + 唯一索引承载权威状态的实现。
|
||||
BackendMySQL Backend = "mysql"
|
||||
// BackendHybrid 表示同时使用多种后端(如 MySQL 权威 + Redis 缓存)的复合实现。
|
||||
BackendHybrid Backend = "hybrid"
|
||||
// BackendUnknown 表示模块未声明后端类型,等价于内存级的不安全实现。
|
||||
BackendUnknown Backend = ""
|
||||
)
|
||||
|
||||
// IsDistributed 返回 true 表示该后端在多节点部署下是安全的(跨节点一致)。
|
||||
func (b Backend) IsDistributed() bool {
|
||||
switch b {
|
||||
case BackendRedis, BackendMySQL, BackendHybrid:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// BackendDescriber 是一个 runtime 模块需要选择性实现的小接口;
|
||||
// 任何 Manager / RedisManager 都可以通过它告诉 wiring 层自己的后端类型。
|
||||
// 故意做成可选实现而非塞进 Runtime 接口,避免影响下游已有实现。
|
||||
type BackendDescriber interface {
|
||||
BackendKind() Backend
|
||||
}
|
||||
|
||||
// Profile 描述部署形态。Distributed=true 表示存在多个 logic / scene / social
|
||||
// 节点同时承担同一 runtime 模块的写入。
|
||||
type Profile struct {
|
||||
// Distributed 为 true 时本包会拒绝任何非分布式安全后端。
|
||||
Distributed bool
|
||||
// AllowMemoryModules 列出仍允许使用内存后端的模块名。常用于灰度上线、
|
||||
// 单元测试或显式接受"该模块在多节点下功能降级"的项目决策。
|
||||
// 列表中的名字与 Check 的 module 参数完全匹配(区分大小写)。
|
||||
AllowMemoryModules []string
|
||||
}
|
||||
|
||||
func (p Profile) allowsMemoryFor(module string) bool {
|
||||
module = strings.ToLower(strings.TrimSpace(module))
|
||||
for _, name := range p.AllowMemoryModules {
|
||||
if strings.ToLower(strings.TrimSpace(name)) == module {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ErrMemoryDistProfile 表示在多节点 Profile 下检测到了内存后端。
|
||||
// 通过 errors.Is 可以稳定识别。
|
||||
var ErrMemoryDistProfile = errors.New("runtimeguard: memory backend rejected by distributed profile")
|
||||
|
||||
// Check 在多节点 Profile 下拒绝内存 / 未知后端;其余情况返回 nil。
|
||||
// module 用于错误消息和 AllowMemoryModules 白名单匹配。
|
||||
func Check(module string, backend Backend, profile Profile) error {
|
||||
module = strings.TrimSpace(module)
|
||||
if module == "" {
|
||||
return fmt.Errorf("runtimeguard: module name is required")
|
||||
}
|
||||
if !profile.Distributed {
|
||||
return nil
|
||||
}
|
||||
if backend.IsDistributed() {
|
||||
return nil
|
||||
}
|
||||
if profile.allowsMemoryFor(module) {
|
||||
return nil
|
||||
}
|
||||
kind := backend
|
||||
if kind == BackendUnknown {
|
||||
kind = BackendMemory
|
||||
}
|
||||
return fmt.Errorf("%w: module %q uses backend %q which is single-node only",
|
||||
ErrMemoryDistProfile, module, kind)
|
||||
}
|
||||
|
||||
// CheckDescriber 是 Check 的便捷重载:如果 describer 为 nil 视为未知后端。
|
||||
func CheckDescriber(module string, describer BackendDescriber, profile Profile) error {
|
||||
var backend Backend
|
||||
if describer != nil {
|
||||
backend = describer.BackendKind()
|
||||
}
|
||||
return Check(module, backend, profile)
|
||||
}
|
||||
|
||||
// MustCheck 行为同 Check,但在检测失败时 panic。仅供启动早期、且失败即应停机的 wiring 路径使用。
|
||||
func MustCheck(module string, backend Backend, profile Profile) {
|
||||
if err := Check(module, backend, profile); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
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}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package runtimeguard
|
||||
|
||||
import "strings"
|
||||
|
||||
// 生产类环境集合:与 SECURITY.md / config validation 中的"生产基线"对齐。
|
||||
// 任何一项命中即视为多节点 Profile。命中条件做成显式列表(而不是"非 dev 即生产"),
|
||||
// 避免出现"prod-canary" 这种新词被悄悄当成 dev 的情况——这种情形应该明确加进来。
|
||||
var prodEnvTokens = []string{
|
||||
"prod",
|
||||
"production",
|
||||
"staging",
|
||||
"stage",
|
||||
"preprod",
|
||||
"pre-production",
|
||||
"k8s",
|
||||
"kubernetes",
|
||||
"helm",
|
||||
"cluster",
|
||||
"live",
|
||||
}
|
||||
|
||||
// IsProductionEnvironment 判断给定的环境名是否落在"生产类"集合内。匹配规则:
|
||||
// 字符串包含 prodEnvTokens 中任意一个 token(不区分大小写)。
|
||||
// 例如 "prod-canary"、"k8s-staging"、"helm-cluster-2" 都视为生产类。
|
||||
func IsProductionEnvironment(env string) bool {
|
||||
env = strings.ToLower(strings.TrimSpace(env))
|
||||
if env == "" {
|
||||
return false
|
||||
}
|
||||
for _, token := range prodEnvTokens {
|
||||
if strings.Contains(env, token) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ProfileFromEnvironment 从环境名 + topology 开关推导出一个默认 Profile。
|
||||
// 规则:
|
||||
// - 命中生产类环境名 -> Distributed=true(即使 topology 关着,因为多 logic 实例
|
||||
// 是生产部署的常态,要求项目显式声明"这不是多节点"才允许 memory 后端)
|
||||
// - topology 显式开启 -> Distributed=true
|
||||
// - 否则 Distributed=false(local / dev / smoke 默认放行内存后端)
|
||||
//
|
||||
// 项目侧可以拿到这个 Profile 之后再调 WithAllowMemory 把已知"暂时不能上分布式后端"
|
||||
// 的模块加入白名单。
|
||||
func ProfileFromEnvironment(env string, topologyEnabled bool) Profile {
|
||||
distributed := topologyEnabled || IsProductionEnvironment(env)
|
||||
return Profile{Distributed: distributed}
|
||||
}
|
||||
|
||||
// ProfileFromEnvironmentAndBackends 从环境名、topology 开关和实际存储后端共同推导 Profile。
|
||||
//
|
||||
// 这个入口用于项目侧 runtime wiring。不要只因为 service.environment 写成 local
|
||||
// 就把共享 runtime 当成单节点安全:如果 Profile / guild / chat 等 owner 已经接入
|
||||
// MySQL、Redis 或 hybrid 后端,说明这条链路正在使用跨进程权威/缓存,启动期也应该
|
||||
// 按分布式 Profile 检查内存 runtime。
|
||||
func ProfileFromEnvironmentAndBackends(env string, topologyEnabled bool, backends ...Backend) Profile {
|
||||
distributed := topologyEnabled || IsProductionEnvironment(env)
|
||||
for _, backend := range backends {
|
||||
if backend.IsDistributed() {
|
||||
distributed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return Profile{Distributed: distributed}
|
||||
}
|
||||
|
||||
// BackendFromStoreKind 把通用配置里的 store_kind / kind 字段映射为 runtimeguard 后端。
|
||||
// 项目侧在接入自定义 owner 时可以复用它,避免散落的字符串判断只看 environment。
|
||||
func BackendFromStoreKind(kind string) Backend {
|
||||
switch strings.ToLower(strings.TrimSpace(kind)) {
|
||||
case "redis", "pika", "profiledb", "profile_db":
|
||||
return BackendRedis
|
||||
case "mysql":
|
||||
return BackendMySQL
|
||||
case "mysql_redis", "redis_mysql", "hybrid":
|
||||
return BackendHybrid
|
||||
case "memory", "file", "json", "":
|
||||
return BackendMemory
|
||||
default:
|
||||
return BackendUnknown
|
||||
}
|
||||
}
|
||||
|
||||
// WithAllowMemory 是 Profile 链式构造的便捷方法:返回一份拷贝,附加 module 到
|
||||
// AllowMemoryModules 白名单。
|
||||
func (p Profile) WithAllowMemory(modules ...string) Profile {
|
||||
if len(modules) == 0 {
|
||||
return p
|
||||
}
|
||||
out := Profile{
|
||||
Distributed: p.Distributed,
|
||||
AllowMemoryModules: make([]string, 0, len(p.AllowMemoryModules)+len(modules)),
|
||||
}
|
||||
out.AllowMemoryModules = append(out.AllowMemoryModules, p.AllowMemoryModules...)
|
||||
for _, m := range modules {
|
||||
m = strings.TrimSpace(m)
|
||||
if m == "" {
|
||||
continue
|
||||
}
|
||||
if containsString(out.AllowMemoryModules, m) {
|
||||
continue
|
||||
}
|
||||
out.AllowMemoryModules = append(out.AllowMemoryModules, m)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func containsString(list []string, target string) bool {
|
||||
for _, item := range list {
|
||||
if item == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in new issue
Block a user