DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
hanfuit committed 2026-11-04 14:31:55 +08:00
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
}