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,196 @@
|
||||
// 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
|
||||
}
|
||||
Reference in new issue
Block a user