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

234 lines
8.4 KiB
Go

package logic
import (
"context"
"fmt"
"strings"
"time"
appkit "longheng.io/server/internal/platform/app"
"longheng.io/server/internal/platform/config"
"longheng.io/server/internal/platform/idempotency"
"longheng.io/server/internal/reference/player"
)
func newPlayerStore(cfg config.ProfileStoreSection, env *appkit.Env) player.Store {
store, _ := newPlayerStores(cfg, env)
return store
}
func newIdempotencyGuard(cfg config.IdempotencySection) *idempotency.Guard {
ttl := time.Duration(cfg.TTLSeconds) * time.Second
switch strings.ToLower(strings.TrimSpace(cfg.Kind)) {
case "redis":
return idempotency.NewRedis(idempotency.RedisOptions{
Address: cfg.RedisAddress,
Password: cfg.RedisPassword,
DB: cfg.RedisDB,
KeyPrefix: cfg.KeyPrefix,
PoolSize: cfg.RedisPoolSize,
Timeout: time.Duration(cfg.RedisTimeoutSeconds) * time.Second,
ConnectTimeout: time.Duration(cfg.RedisConnectTimeoutSecs) * time.Second,
ReadTimeout: time.Duration(cfg.RedisReadTimeoutSecs) * time.Second,
WriteTimeout: time.Duration(cfg.RedisWriteTimeoutSecs) * time.Second,
TTL: ttl,
})
case "mysql":
return idempotency.NewMySQL(idempotency.MySQLOptions{
DSN: cfg.MySQLDSN,
MaxOpenConns: cfg.MySQLMaxOpenConns,
MaxIdleConns: cfg.MySQLMaxIdleConns,
ConnMaxLifetime: time.Duration(cfg.MySQLConnMaxLifetimeSec) * time.Second,
KeyPrefix: cfg.KeyPrefix,
TTL: ttl,
EnsureSchema: true,
})
case "mysql_redis":
return idempotency.NewMySQLRedis(idempotency.MySQLRedisOptions{
TTL: ttl,
Redis: idempotency.RedisOptions{
Address: cfg.RedisAddress,
Password: cfg.RedisPassword,
DB: cfg.RedisDB,
KeyPrefix: cfg.KeyPrefix,
PoolSize: cfg.RedisPoolSize,
Timeout: time.Duration(cfg.RedisTimeoutSeconds) * time.Second,
ConnectTimeout: time.Duration(cfg.RedisConnectTimeoutSecs) * time.Second,
ReadTimeout: time.Duration(cfg.RedisReadTimeoutSecs) * time.Second,
WriteTimeout: time.Duration(cfg.RedisWriteTimeoutSecs) * time.Second,
},
MySQL: idempotency.MySQLOptions{
DSN: cfg.MySQLDSN,
MaxOpenConns: cfg.MySQLMaxOpenConns,
MaxIdleConns: cfg.MySQLMaxIdleConns,
ConnMaxLifetime: time.Duration(cfg.MySQLConnMaxLifetimeSec) * time.Second,
KeyPrefix: cfg.KeyPrefix,
EnsureSchema: true,
},
})
default:
return idempotency.New(idempotency.Options{TTL: ttl})
}
}
func newPlayerStores(cfg config.ProfileStoreSection, env *appkit.Env) (player.Store, player.SummaryStore) {
return newProfileStore(cfg, env), newSummaryStore(cfg, env)
}
func newProfileStore(cfg config.ProfileStoreSection, env *appkit.Env) player.Store {
var store player.Store
switch strings.ToLower(strings.TrimSpace(cfg.StoreKind)) {
case "", "memory":
store = player.NewMemoryStore()
case "file", "json":
store = player.NewFileStore(cfg.StoreDirectory)
case "redis", "pika", "profiledb", "profile_db":
store = player.NewRedisStore(player.RedisStoreOptions{
Address: cfg.StoreAddress,
Password: cfg.StorePassword,
DB: cfg.StoreDB,
KeyPrefix: cfg.StoreKeyPrefix,
PoolSize: cfg.StorePoolSize,
Timeout: time.Duration(cfg.StoreTimeoutSeconds) * time.Second,
ConnectTimeout: time.Duration(cfg.StoreConnectTimeout) * time.Second,
ReadTimeout: time.Duration(cfg.StoreReadTimeout) * time.Second,
WriteTimeout: time.Duration(cfg.StoreWriteTimeout) * time.Second,
TTL: time.Duration(cfg.StoreTTLSeconds) * time.Second,
})
case "mysql", "mysql_redis":
if cfg.MySQLShardingEnabled || len(cfg.MySQLShards) > 0 {
sharded, err := newShardedStore(cfg)
if err != nil {
if env != nil && env.Logger != nil {
env.Logger.Error("invalid sharded mysql player store config", "error", err)
}
return failingPlayerStore{err: fmt.Errorf("invalid sharded mysql player store config: %w", err)}
} else {
store = sharded
}
} else {
store = player.NewMySQLStore(player.MySQLStoreOptions{
DSN: cfg.MySQLDSN,
MaxOpenConns: cfg.MySQLMaxOpenConns,
MaxIdleConns: cfg.MySQLMaxIdleConns,
ConnMaxLifetime: time.Duration(cfg.MySQLConnMaxLifetime) * time.Second,
EnsureSchema: true,
})
}
default:
if env != nil && env.Logger != nil {
env.Logger.Warn("unsupported player store kind, falling back to memory", "kind", cfg.StoreKind)
}
store = player.NewMemoryStore()
}
switch strings.ToLower(strings.TrimSpace(cfg.SaveMode)) {
case "", "sync":
return store
case "async", "writebehind", "write_behind":
asyncStore := player.NewAsyncStore(store, player.AsyncStoreOptions{
FlushInterval: time.Duration(cfg.AsyncFlushIntervalSeconds) * time.Second,
MaxPending: cfg.AsyncMaxPending,
RetryBackoff: time.Duration(cfg.AsyncRetryBackoffSeconds) * time.Second,
MaxRetries: cfg.AsyncMaxRetries,
AutoExpireTTL: time.Duration(cfg.AsyncAutoExpireSeconds) * time.Second,
DeadLetterLimit: cfg.AsyncDeadLetterLimit,
DeadLetterStore: player.NewFileDeadLetterStore(cfg.AsyncDeadLetterDirectory),
})
if roleStore, ok := store.(player.RoleProfileStore); ok {
return player.NewRoleAwareAsyncStore(asyncStore, roleStore)
}
return asyncStore
default:
if env != nil && env.Logger != nil {
env.Logger.Warn("unsupported player save mode, using sync store", "mode", cfg.SaveMode)
}
return store
}
}
type failingPlayerStore struct {
err error
}
// Load 返回配置错误,阻止损坏的玩家存储继续启动。
func (s failingPlayerStore) Load(context.Context, string) (player.Profile, bool, error) {
return player.Profile{}, false, s.err
}
// Save 返回配置错误,避免玩家资料写入未知后端。
func (s failingPlayerStore) Save(context.Context, player.Profile) error {
return s.err
}
// Check 返回配置错误,供启动前检查暴露存储配置问题。
func (s failingPlayerStore) Check(context.Context) error {
return s.err
}
func newShardedStore(cfg config.ProfileStoreSection) (*player.ShardedMySQLStore, error) {
shards := make([]player.MySQLShardOptions, 0, len(cfg.MySQLShards))
for _, shard := range cfg.MySQLShards {
maxOpen := shard.MaxOpenConns
if maxOpen == 0 {
maxOpen = cfg.MySQLMaxOpenConns
}
maxIdle := shard.MaxIdleConns
if maxIdle == 0 {
maxIdle = cfg.MySQLMaxIdleConns
}
lifetime := shard.ConnMaxLifetime
if lifetime == 0 {
lifetime = cfg.MySQLConnMaxLifetime
}
shards = append(shards, player.MySQLShardOptions{
ID: shard.ID,
DSN: shard.DSN,
TableName: shard.TableName,
TablePrefix: shard.TablePrefix,
HashSlots: shard.HashSlots,
MaxOpenConns: maxOpen,
MaxIdleConns: maxIdle,
ConnMaxLifetime: time.Duration(lifetime) * time.Second,
EnsureSchema: true,
})
}
return player.NewShardedMySQLStore(player.ShardedMySQLStoreOptions{
DefaultDSN: cfg.MySQLDSN,
DefaultMaxOpenConns: cfg.MySQLMaxOpenConns,
DefaultMaxIdleConns: cfg.MySQLMaxIdleConns,
DefaultConnMaxLifetime: time.Duration(cfg.MySQLConnMaxLifetime) * time.Second,
EnsureSchema: true,
Shards: shards,
})
}
func newSummaryStore(cfg config.ProfileStoreSection, env *appkit.Env) player.SummaryStore {
kind := strings.ToLower(strings.TrimSpace(cfg.SummaryStoreKind))
if kind == "" && strings.ToLower(strings.TrimSpace(cfg.StoreKind)) == "mysql_redis" {
kind = "redis"
}
switch kind {
case "", "memory":
return player.NewMemorySummaryStore()
case "redis", "pika":
return player.NewRedisSummaryStore(player.RedisSummaryStoreOptions{
Address: cfg.SummaryStoreAddress,
Password: cfg.SummaryStorePassword,
DB: cfg.SummaryStoreDB,
KeyPrefix: cfg.SummaryStoreKeyPrefix,
PoolSize: cfg.SummaryStorePoolSize,
Timeout: time.Duration(cfg.SummaryStoreTimeout) * time.Second,
ConnectTimeout: time.Duration(cfg.SummaryStoreConnectTimeout) * time.Second,
ReadTimeout: time.Duration(cfg.SummaryStoreReadTimeout) * time.Second,
WriteTimeout: time.Duration(cfg.SummaryStoreWriteTimeout) * time.Second,
TTL: time.Duration(cfg.SummaryStoreTTL) * time.Second,
})
default:
if env != nil && env.Logger != nil {
env.Logger.Warn("unsupported player summary store kind, falling back to memory", "kind", cfg.SummaryStoreKind)
}
return player.NewMemorySummaryStore()
}
}