服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
283 lines
7.4 KiB
Go
283 lines
7.4 KiB
Go
package dnf
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
_ "github.com/go-sql-driver/mysql"
|
|
|
|
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
|
"longheng.io/server/internal/modules/dnf/repository/mysql"
|
|
sqliterepo "longheng.io/server/internal/modules/dnf/repository/sqlite"
|
|
appkit "longheng.io/server/internal/platform/app"
|
|
platformdb "longheng.io/server/internal/platform/db"
|
|
"longheng.io/server/internal/platform/servergroup"
|
|
)
|
|
|
|
var openSQL = sql.Open
|
|
|
|
type Runtime struct {
|
|
Repository *RepoComponent
|
|
}
|
|
|
|
type RepoComponent struct {
|
|
component repositoryLifecycle
|
|
driver string
|
|
db *sql.DB
|
|
redis *platformdb.RedigoExecutor
|
|
cacheOpts dnfrepo.RedisCacheOptions
|
|
}
|
|
|
|
type repositoryLifecycle interface {
|
|
Name() string
|
|
Preflight(context.Context) error
|
|
Start(context.Context) error
|
|
Stop(context.Context) error
|
|
Group() (dnfrepo.Group, bool)
|
|
}
|
|
|
|
type RepoSnapshot struct {
|
|
Driver string `json:"driver"`
|
|
MySQL *mysql.ComponentSnapshot `json:"mysql,omitempty"`
|
|
SQLite *sqliterepo.Snapshot `json:"sqlite,omitempty"`
|
|
}
|
|
|
|
// NewRuntime 创建 DNF logic 运行时装配对象。
|
|
// 它只负责项目依赖装配,不注册协议 handler,也不直接修改玩家状态。
|
|
func NewRuntime(env *appkit.Env, cfg Config) (*Runtime, error) {
|
|
cfg.Normalize(env)
|
|
if err := cfg.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
runtime := &Runtime{}
|
|
if cfg.Repository.Enabled {
|
|
component, err := newRepoComponent(env, cfg.Repository)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
runtime.Repository = component
|
|
}
|
|
return runtime, nil
|
|
}
|
|
|
|
// Group 返回启动后的 DNF 玩家仓储聚合。
|
|
// owner/service 只能通过该聚合访问账号、角色、背包、技能、设置和 packet 模板。
|
|
func (r *Runtime) Group() (dnfrepo.Group, bool) {
|
|
if r == nil || r.Repository == nil {
|
|
return dnfrepo.Group{}, false
|
|
}
|
|
return r.Repository.Group()
|
|
}
|
|
|
|
// NewRepoComponent 按 DNF 配置打开 MySQL 连接池并创建仓储组件。
|
|
// 连接池由返回的组件在 Stop 阶段关闭。
|
|
func NewRepoComponent(env *appkit.Env, cfg RepositoryConfig) (*RepoComponent, error) {
|
|
envCfg := Config{Repository: cfg}
|
|
envCfg.Normalize(env)
|
|
if err := envCfg.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
cfg = envCfg.Repository
|
|
return newRepoComponent(env, cfg)
|
|
}
|
|
|
|
func newRepoComponent(env *appkit.Env, cfg RepositoryConfig) (*RepoComponent, error) {
|
|
if openSQL == nil {
|
|
return nil, fmt.Errorf("%w: sql opener is required", ErrConfigInvalid)
|
|
}
|
|
driver := strings.ToLower(strings.TrimSpace(cfg.Driver))
|
|
dataSource := cfg.MySQLDSN
|
|
if driver == "sqlite" {
|
|
dataSource = filepath.Clean(cfg.SQLitePath)
|
|
parent := filepath.Dir(dataSource)
|
|
if parent != "." && parent != "" {
|
|
if err := os.MkdirAll(parent, 0o755); err != nil {
|
|
return nil, fmt.Errorf("create sqlite repository directory: %w", err)
|
|
}
|
|
}
|
|
}
|
|
db, err := openSQL(driver, dataSource)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if db == nil {
|
|
return nil, fmt.Errorf("%w: sql db is nil", ErrConfigInvalid)
|
|
}
|
|
if driver == "sqlite" {
|
|
db.SetMaxOpenConns(1)
|
|
db.SetMaxIdleConns(1)
|
|
} else {
|
|
db.SetMaxOpenConns(cfg.MySQLMaxOpenConns)
|
|
db.SetMaxIdleConns(cfg.MySQLMaxIdleConns)
|
|
db.SetConnMaxLifetime(cfg.connMaxLife())
|
|
}
|
|
|
|
var redisExec *platformdb.RedigoExecutor
|
|
if cfg.RedisEnabled {
|
|
redisExec = platformdb.NewRedigoExecutor(platformdb.RedigoOptions{
|
|
Address: cfg.RedisAddress,
|
|
Password: cfg.RedisPassword,
|
|
DB: cfg.RedisDB,
|
|
PoolSize: cfg.RedisPoolSize,
|
|
Timeout: cfg.redisTimeout(),
|
|
})
|
|
}
|
|
var component repositoryLifecycle
|
|
if driver == "sqlite" {
|
|
component = sqliterepo.NewComponent(db, dataSource)
|
|
} else {
|
|
var manager *servergroup.Manager
|
|
if env != nil {
|
|
manager = env.ServerGroup
|
|
}
|
|
if manager == nil && cfg.ServerGroupPlanFile != "" {
|
|
var loadErr error
|
|
manager, loadErr = loadServerGroupPlan(cfg.ServerGroupPlanFile)
|
|
if loadErr != nil {
|
|
_ = db.Close()
|
|
return nil, loadErr
|
|
}
|
|
}
|
|
component = mysql.NewComponentFromServerGroup(mysql.SQLDBAdapter{DB: db}, manager, mysql.ComponentOptions{
|
|
ShardID: cfg.ShardID,
|
|
TablePrefix: cfg.TablePrefix,
|
|
AutoCreateSchema: cfg.AutoCreateSchema,
|
|
IncludeCSharpLegacySchema: cfg.CSharpLegacySchema,
|
|
CreateDatabases: cfg.CreateDatabases,
|
|
Now: time.Now,
|
|
})
|
|
}
|
|
return &RepoComponent{
|
|
component: component,
|
|
driver: driver,
|
|
db: db,
|
|
redis: redisExec,
|
|
cacheOpts: dnfrepo.RedisCacheOptions{
|
|
KeyPrefix: cfg.RedisKeyPrefix,
|
|
TTL: cfg.redisTTL(),
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// loadServerGroupPlan 从外部区服计划文件构造只读 manager。
|
|
// DNF manifest 可能早于框架 servergroup 注入运行,仓储仍必须按同一份 route meta 派生库名。
|
|
func loadServerGroupPlan(path string) (*servergroup.Manager, error) {
|
|
store, err := servergroup.NewFileStore(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
plan, err := store.Load(context.Background())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return servergroup.New(plan)
|
|
}
|
|
|
|
// Name 返回框架生命周期组件名。
|
|
func (c *RepoComponent) Name() string {
|
|
if c == nil || c.component == nil {
|
|
return "dnf-repository"
|
|
}
|
|
return c.component.Name()
|
|
}
|
|
|
|
// Preflight 检查 DNF 仓储启动依赖,不创建 schema。
|
|
func (c *RepoComponent) Preflight(ctx context.Context) error {
|
|
if c == nil || c.component == nil {
|
|
return dnfrepo.ErrRepoMissing
|
|
}
|
|
if err := c.component.Preflight(ctx); err != nil {
|
|
return err
|
|
}
|
|
return c.pingRedis(ctx)
|
|
}
|
|
|
|
// Start 启动 DNF 仓储组件。
|
|
func (c *RepoComponent) Start(ctx context.Context) error {
|
|
if c == nil || c.component == nil {
|
|
return dnfrepo.ErrRepoMissing
|
|
}
|
|
if err := c.component.Start(ctx); err != nil {
|
|
return err
|
|
}
|
|
return c.pingRedis(ctx)
|
|
}
|
|
|
|
// Stop 停止 DNF 仓储组件并关闭项目侧 SQL 连接池。
|
|
func (c *RepoComponent) Stop(ctx context.Context) error {
|
|
if c == nil {
|
|
return nil
|
|
}
|
|
var errs []error
|
|
if c.component != nil {
|
|
errs = append(errs, c.component.Stop(ctx))
|
|
}
|
|
if c.db != nil {
|
|
errs = append(errs, c.db.Close())
|
|
}
|
|
if c.redis != nil {
|
|
errs = append(errs, c.redis.Close())
|
|
}
|
|
return errors.Join(errs...)
|
|
}
|
|
|
|
// Group 返回启动后的 DNF 玩家仓储聚合。
|
|
func (c *RepoComponent) Group() (dnfrepo.Group, bool) {
|
|
if c == nil || c.component == nil {
|
|
return dnfrepo.Group{}, false
|
|
}
|
|
group, ok := c.component.Group()
|
|
if !ok {
|
|
return dnfrepo.Group{}, false
|
|
}
|
|
if c.redis != nil {
|
|
group = dnfrepo.NewCachedGroup(group, c.redis, c.cacheOpts)
|
|
}
|
|
return group, true
|
|
}
|
|
|
|
// Snapshot 返回 DNF 仓储装配状态。
|
|
func (c *RepoComponent) Snapshot() RepoSnapshot {
|
|
if c == nil || c.component == nil {
|
|
return RepoSnapshot{}
|
|
}
|
|
snapshot := RepoSnapshot{Driver: c.driver}
|
|
switch component := c.component.(type) {
|
|
case *mysql.Component:
|
|
value := component.Snapshot()
|
|
snapshot.MySQL = &value
|
|
case *sqliterepo.Component:
|
|
value := component.Snapshot()
|
|
snapshot.SQLite = &value
|
|
}
|
|
return snapshot
|
|
}
|
|
|
|
func (c *RepoComponent) pingRedis(ctx context.Context) error {
|
|
if c == nil || c.redis == nil {
|
|
return nil
|
|
}
|
|
_, err := c.redis.Do(ctx, "PING")
|
|
return err
|
|
}
|
|
|
|
func (c RepositoryConfig) redisTimeout() time.Duration {
|
|
if c.RedisTimeoutSeconds <= 0 {
|
|
return 2 * time.Second
|
|
}
|
|
return time.Duration(c.RedisTimeoutSeconds) * time.Second
|
|
}
|
|
|
|
func (c RepositoryConfig) redisTTL() time.Duration {
|
|
if c.RedisTTLSeconds <= 0 {
|
|
return 0
|
|
}
|
|
return time.Duration(c.RedisTTLSeconds) * time.Second
|
|
}
|