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

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

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
}