服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
197 lines
6.5 KiB
Go
197 lines
6.5 KiB
Go
// Package dnf 提供 DNF 项目接入 logic 服务的装配入口。
|
|
package dnf
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/BurntSushi/toml"
|
|
|
|
appkit "longheng.io/server/internal/platform/app"
|
|
)
|
|
|
|
const (
|
|
// EnvConfigPath 是 DNF logic manifest 使用的外部配置路径环境变量。
|
|
EnvConfigPath = "LONGHENG_DNF_CONFIG"
|
|
// DefaultConfigPath 是本地 DNF logic manifest 配置路径。
|
|
DefaultConfigPath = "configs/dnf/logic.toml"
|
|
)
|
|
|
|
var ErrConfigInvalid = errors.New("dnf logic config is invalid")
|
|
|
|
type Config struct {
|
|
Repository RepositoryConfig `toml:"repository"`
|
|
}
|
|
|
|
type RepositoryConfig struct {
|
|
Enabled bool `toml:"enabled"`
|
|
Driver string `toml:"driver"`
|
|
SQLitePath string `toml:"sqlite_path"`
|
|
MySQLDSN string `toml:"mysql_dsn"`
|
|
MySQLMaxOpenConns int `toml:"mysql_max_open_conns"`
|
|
MySQLMaxIdleConns int `toml:"mysql_max_idle_conns"`
|
|
MySQLConnMaxLifetime int `toml:"mysql_conn_max_lifetime_seconds"`
|
|
RedisEnabled bool `toml:"redis_enabled"`
|
|
RedisAddress string `toml:"redis_address"`
|
|
RedisPassword string `toml:"redis_password"`
|
|
RedisDB int `toml:"redis_db"`
|
|
RedisPoolSize int `toml:"redis_pool_size"`
|
|
RedisTimeoutSeconds int `toml:"redis_timeout_seconds"`
|
|
RedisKeyPrefix string `toml:"redis_key_prefix"`
|
|
RedisTTLSeconds int `toml:"redis_ttl_seconds"`
|
|
ShardID string `toml:"shard_id"`
|
|
TablePrefix string `toml:"table_prefix"`
|
|
ServerGroupPlanFile string `toml:"server_group_plan_file"`
|
|
AutoCreateSchema bool `toml:"auto_create_schema"`
|
|
CSharpLegacySchema bool `toml:"csharp_legacy_schema"`
|
|
CreateDatabases bool `toml:"create_databases"`
|
|
}
|
|
|
|
// LoadConfig 读取 DNF logic manifest 外部配置。
|
|
// 该配置只属于 DNF 项目装配层,不进入平台通用 ServiceConfig;环境相关默认值由 LoadConfigForEnv 补齐。
|
|
func LoadConfig(path string) (Config, error) {
|
|
path = strings.TrimSpace(path)
|
|
if path == "" {
|
|
path = DefaultConfigPath
|
|
}
|
|
var cfg Config
|
|
meta, err := toml.DecodeFile(path, &cfg)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
if keys := meta.Undecoded(); len(keys) > 0 {
|
|
names := make([]string, 0, len(keys))
|
|
for _, key := range keys {
|
|
names = append(names, key.String())
|
|
}
|
|
return Config{}, fmt.Errorf("%w: unknown keys: %s", ErrConfigInvalid, strings.Join(names, ", "))
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
// LoadConfigForEnv 读取并校验 DNF logic manifest 配置。
|
|
// 它会从框架 Env 补齐当前区服 shard_id,再执行配置校验。
|
|
func LoadConfigForEnv(path string, env *appkit.Env) (Config, error) {
|
|
cfg, err := LoadConfig(path)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
cfg.Normalize(env)
|
|
return cfg, cfg.Validate()
|
|
}
|
|
|
|
// Normalize 补齐 DNF 仓储装配默认值。
|
|
// ShardID 默认读取框架区服 ID,读写库名仍由 servergroup route meta 派生。
|
|
func (c *Config) Normalize(env *appkit.Env) {
|
|
if c == nil {
|
|
return
|
|
}
|
|
repo := &c.Repository
|
|
repo.Driver = strings.ToLower(strings.TrimSpace(repo.Driver))
|
|
repo.SQLitePath = strings.TrimSpace(repo.SQLitePath)
|
|
repo.MySQLDSN = strings.TrimSpace(repo.MySQLDSN)
|
|
repo.RedisAddress = strings.TrimSpace(repo.RedisAddress)
|
|
repo.RedisPassword = strings.TrimSpace(repo.RedisPassword)
|
|
repo.RedisKeyPrefix = strings.TrimSpace(repo.RedisKeyPrefix)
|
|
repo.ShardID = strings.TrimSpace(repo.ShardID)
|
|
repo.TablePrefix = strings.TrimSpace(repo.TablePrefix)
|
|
repo.ServerGroupPlanFile = strings.TrimSpace(repo.ServerGroupPlanFile)
|
|
if repo.ShardID == "" && env != nil && env.Config.Cluster.ShardID != 0 {
|
|
repo.ShardID = fmt.Sprintf("%d", env.Config.Cluster.ShardID)
|
|
}
|
|
if repo.ServerGroupPlanFile == "" && env != nil {
|
|
repo.ServerGroupPlanFile = strings.TrimSpace(env.Config.ServerGroup.PlanFile)
|
|
}
|
|
if repo.TablePrefix == "" {
|
|
repo.TablePrefix = "dnf"
|
|
}
|
|
// An omitted driver preserves every existing installation's MySQL
|
|
// behavior. New one-click templates opt in to SQLite explicitly.
|
|
if repo.Driver == "" {
|
|
repo.Driver = "mysql"
|
|
}
|
|
if repo.Driver == "sqlite" && repo.SQLitePath == "" {
|
|
repo.SQLitePath = "data/dnf90.db"
|
|
}
|
|
if repo.MySQLMaxOpenConns <= 0 {
|
|
repo.MySQLMaxOpenConns = 32
|
|
}
|
|
if repo.MySQLMaxIdleConns <= 0 {
|
|
repo.MySQLMaxIdleConns = 8
|
|
}
|
|
if repo.MySQLConnMaxLifetime <= 0 {
|
|
repo.MySQLConnMaxLifetime = 300
|
|
}
|
|
if repo.RedisEnabled {
|
|
if repo.RedisAddress == "" {
|
|
repo.RedisAddress = "127.0.0.1:6379"
|
|
}
|
|
if repo.RedisPoolSize <= 0 {
|
|
repo.RedisPoolSize = 8
|
|
}
|
|
if repo.RedisTimeoutSeconds <= 0 {
|
|
repo.RedisTimeoutSeconds = 2
|
|
}
|
|
if repo.RedisKeyPrefix == "" {
|
|
repo.RedisKeyPrefix = "dnf:repository"
|
|
if repo.ShardID != "" {
|
|
repo.RedisKeyPrefix += ":" + repo.ShardID
|
|
}
|
|
}
|
|
if repo.RedisTTLSeconds < 0 {
|
|
repo.RedisTTLSeconds = 0
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate 校验 DNF 仓储装配配置。
|
|
// 开启仓储时必须显式配置 MySQL DSN,并通过 servergroup 提供 shard 路由。
|
|
func (c Config) Validate() error {
|
|
repo := c.Repository
|
|
if !repo.Enabled {
|
|
return nil
|
|
}
|
|
switch strings.ToLower(strings.TrimSpace(repo.Driver)) {
|
|
case "mysql":
|
|
if strings.TrimSpace(repo.MySQLDSN) == "" {
|
|
return fmt.Errorf("%w: repository mysql_dsn is required", ErrConfigInvalid)
|
|
}
|
|
if strings.TrimSpace(repo.ShardID) == "" {
|
|
return fmt.Errorf("%w: repository shard_id is required", ErrConfigInvalid)
|
|
}
|
|
if repo.MySQLMaxOpenConns <= 0 || repo.MySQLMaxIdleConns <= 0 || repo.MySQLConnMaxLifetime <= 0 {
|
|
return fmt.Errorf("%w: repository mysql pool values must be positive", ErrConfigInvalid)
|
|
}
|
|
case "sqlite":
|
|
if strings.TrimSpace(repo.SQLitePath) == "" {
|
|
return fmt.Errorf("%w: repository sqlite_path is required", ErrConfigInvalid)
|
|
}
|
|
default:
|
|
return fmt.Errorf("%w: repository driver must be mysql or sqlite", ErrConfigInvalid)
|
|
}
|
|
if repo.RedisEnabled {
|
|
if strings.TrimSpace(repo.RedisAddress) == "" {
|
|
return fmt.Errorf("%w: repository redis_address is required", ErrConfigInvalid)
|
|
}
|
|
if repo.RedisDB < 0 {
|
|
return fmt.Errorf("%w: repository redis_db must be non-negative", ErrConfigInvalid)
|
|
}
|
|
if repo.RedisPoolSize <= 0 || repo.RedisTimeoutSeconds <= 0 {
|
|
return fmt.Errorf("%w: repository redis pool values must be positive", ErrConfigInvalid)
|
|
}
|
|
if strings.TrimSpace(repo.RedisKeyPrefix) == "" {
|
|
return fmt.Errorf("%w: repository redis_key_prefix is required", ErrConfigInvalid)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c RepositoryConfig) connMaxLife() time.Duration {
|
|
if c.MySQLConnMaxLifetime <= 0 {
|
|
return 5 * time.Minute
|
|
}
|
|
return time.Duration(c.MySQLConnMaxLifetime) * time.Second
|
|
}
|