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,403 @@
|
||||
package playerloop
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"longheng.io/server/internal/platform/tracing"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrStopped 表示玩家循环管理器未启动或已经停止。
|
||||
ErrStopped = errors.New("player loop manager is stopped")
|
||||
|
||||
// ErrMissingAccount 表示提交事件时缺少账号 ID。
|
||||
ErrMissingAccount = errors.New("account id is required")
|
||||
|
||||
// ErrQueueFull 表示目标玩家循环队列已经写满。
|
||||
ErrQueueFull = errors.New("player loop queue is full")
|
||||
)
|
||||
|
||||
// Event 是按账号串行处理的玩家事件。
|
||||
type Event struct {
|
||||
AccountID string
|
||||
Payload any
|
||||
ReceivedAt time.Time
|
||||
}
|
||||
|
||||
// Handler 处理单个玩家事件,框架保证同一账号事件串行进入。
|
||||
type Handler func(context.Context, Event) error
|
||||
|
||||
// Options 描述玩家循环的空闲回收和 handler 超时策略。
|
||||
type Options struct {
|
||||
IdleTTL time.Duration
|
||||
SweepInterval time.Duration
|
||||
HandlerTimeout time.Duration
|
||||
}
|
||||
|
||||
// Snapshot 是单个账号玩家循环的队列快照。
|
||||
type Snapshot struct {
|
||||
AccountID string `json:"account_id"`
|
||||
Queued int `json:"queued"`
|
||||
}
|
||||
|
||||
// Manager 按账号维护独立事件队列,保证同一账号状态修改串行执行。
|
||||
type Manager struct {
|
||||
name string
|
||||
queue int
|
||||
handler Handler
|
||||
logger *slog.Logger
|
||||
|
||||
idleTTL time.Duration
|
||||
sweepInterval time.Duration
|
||||
handlerTimeout time.Duration
|
||||
|
||||
mu sync.RWMutex
|
||||
started bool
|
||||
loops map[string]*loop
|
||||
active *managerGeneration
|
||||
|
||||
sweepCount atomic.Uint64
|
||||
handlerErrorCount atomic.Uint64
|
||||
}
|
||||
|
||||
// managerGeneration 持有一次 Start 创建的全部运行资源,避免旧 Stop 干扰新一代循环。
|
||||
type managerGeneration struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
loops map[string]*loop
|
||||
wg sync.WaitGroup
|
||||
sweepCancel context.CancelFunc
|
||||
sweepDone chan struct{}
|
||||
}
|
||||
|
||||
type loop struct {
|
||||
accountID string
|
||||
ch chan Event
|
||||
closeOnce sync.Once
|
||||
|
||||
mu sync.RWMutex
|
||||
closed bool
|
||||
// processing 防止 idle sweep 在 handler 已取出事件、但仍在修改账号状态时关闭 loop。
|
||||
processing bool
|
||||
lastActive time.Time
|
||||
}
|
||||
|
||||
// New 使用默认选项创建玩家循环管理器。
|
||||
func New(name string, queue int, handler Handler, logger *slog.Logger) *Manager {
|
||||
return NewWithOptions(name, queue, handler, logger, Options{})
|
||||
}
|
||||
|
||||
// NewWithOptions 创建带空闲回收和 handler 超时配置的玩家循环管理器。
|
||||
func NewWithOptions(name string, queue int, handler Handler, logger *slog.Logger, options Options) *Manager {
|
||||
if queue <= 0 {
|
||||
queue = 64
|
||||
}
|
||||
if options.SweepInterval <= 0 && options.IdleTTL > 0 {
|
||||
options.SweepInterval = options.IdleTTL / 2
|
||||
if options.SweepInterval <= 0 {
|
||||
options.SweepInterval = time.Second
|
||||
}
|
||||
}
|
||||
return &Manager{
|
||||
name: name,
|
||||
queue: queue,
|
||||
handler: handler,
|
||||
logger: logger,
|
||||
idleTTL: options.IdleTTL,
|
||||
sweepInterval: options.SweepInterval,
|
||||
handlerTimeout: options.HandlerTimeout,
|
||||
loops: make(map[string]*loop),
|
||||
}
|
||||
}
|
||||
|
||||
// Name 返回玩家循环管理器名称。
|
||||
func (m *Manager) Name() string {
|
||||
return m.name
|
||||
}
|
||||
|
||||
// Start 启动玩家循环管理器和可选空闲回收任务。
|
||||
func (m *Manager) Start(ctx context.Context) error {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.started {
|
||||
return nil
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
runCtx, runCancel := context.WithCancel(context.Background())
|
||||
generation := &managerGeneration{
|
||||
ctx: runCtx,
|
||||
cancel: runCancel,
|
||||
loops: make(map[string]*loop),
|
||||
}
|
||||
m.started = true
|
||||
m.active = generation
|
||||
m.loops = generation.loops
|
||||
if m.idleTTL > 0 {
|
||||
sweepCtx, cancel := context.WithCancel(runCtx)
|
||||
generation.sweepCancel = cancel
|
||||
generation.sweepDone = make(chan struct{})
|
||||
go m.sweepIdleLoops(sweepCtx, generation)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop 关闭所有账号循环并等待已取出的事件处理完成。
|
||||
func (m *Manager) Stop(ctx context.Context) error {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
var stopErr error
|
||||
recordCtxErr := func() {
|
||||
if stopErr == nil {
|
||||
stopErr = ctx.Err()
|
||||
}
|
||||
}
|
||||
m.mu.Lock()
|
||||
if !m.started || m.active == nil {
|
||||
m.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
generation := m.active
|
||||
loops := make([]*loop, 0, len(generation.loops))
|
||||
for _, playerLoop := range generation.loops {
|
||||
loops = append(loops, playerLoop)
|
||||
}
|
||||
m.started = false
|
||||
m.active = nil
|
||||
m.loops = make(map[string]*loop)
|
||||
m.mu.Unlock()
|
||||
|
||||
if generation.sweepCancel != nil {
|
||||
generation.sweepCancel()
|
||||
}
|
||||
if generation.sweepDone != nil {
|
||||
select {
|
||||
case <-generation.sweepDone:
|
||||
case <-ctx.Done():
|
||||
recordCtxErr()
|
||||
}
|
||||
}
|
||||
|
||||
for _, playerLoop := range loops {
|
||||
playerLoop.close()
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
generation.wg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
generation.cancel()
|
||||
return stopErr
|
||||
case <-ctx.Done():
|
||||
generation.cancel()
|
||||
recordCtxErr()
|
||||
return stopErr
|
||||
}
|
||||
}
|
||||
|
||||
// Submit 将事件提交到指定账号的串行队列。
|
||||
func (m *Manager) Submit(ctx context.Context, accountID string, payload any) error {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
accountID = strings.TrimSpace(accountID)
|
||||
if accountID == "" {
|
||||
return ErrMissingAccount
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
m.mu.Lock()
|
||||
if !m.started || m.active == nil {
|
||||
m.mu.Unlock()
|
||||
return ErrStopped
|
||||
}
|
||||
generation := m.active
|
||||
playerLoop := generation.loops[accountID]
|
||||
if playerLoop == nil {
|
||||
playerLoop = &loop{
|
||||
accountID: accountID,
|
||||
ch: make(chan Event, m.queue),
|
||||
lastActive: time.Now().UTC(),
|
||||
}
|
||||
generation.loops[accountID] = playerLoop
|
||||
generation.wg.Add(1)
|
||||
go m.run(generation, playerLoop)
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
event := Event{
|
||||
AccountID: accountID,
|
||||
Payload: payload,
|
||||
ReceivedAt: time.Now().UTC(),
|
||||
}
|
||||
return playerLoop.submit(ctx, event)
|
||||
}
|
||||
|
||||
// Snapshot 返回所有账号循环的队列长度快照。
|
||||
func (m *Manager) Snapshot() []Snapshot {
|
||||
m.mu.RLock()
|
||||
out := make([]Snapshot, 0, len(m.loops))
|
||||
for accountID, playerLoop := range m.loops {
|
||||
out = append(out, Snapshot{
|
||||
AccountID: accountID,
|
||||
Queued: len(playerLoop.ch),
|
||||
})
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
return out[i].AccountID < out[j].AccountID
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
func (m *Manager) run(generation *managerGeneration, playerLoop *loop) {
|
||||
defer generation.wg.Done()
|
||||
for event := range playerLoop.ch {
|
||||
m.handle(generation.ctx, playerLoop, event)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) handle(ctx context.Context, playerLoop *loop, event Event) {
|
||||
playerLoop.markProcessing(true)
|
||||
defer playerLoop.markProcessing(false)
|
||||
|
||||
if m.handler == nil {
|
||||
return
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
handlerCtx := ctx
|
||||
var cancel context.CancelFunc
|
||||
if m.handlerTimeout > 0 {
|
||||
handlerCtx, cancel = context.WithTimeout(ctx, m.handlerTimeout)
|
||||
}
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
defer func() {
|
||||
if rec := recover(); rec != nil {
|
||||
m.handlerErrorCount.Add(1)
|
||||
if m.logger != nil {
|
||||
tracing.LogPanic(handlerCtx, m.logger, "playerloop.handler", rec, map[string]string{"account_id": event.AccountID})
|
||||
}
|
||||
}
|
||||
}()
|
||||
if err := m.handler(handlerCtx, event); err != nil {
|
||||
m.handlerErrorCount.Add(1)
|
||||
if m.logger != nil && !errors.Is(err, context.Canceled) {
|
||||
m.logger.Error("player loop handler failed", "account_id", event.AccountID, "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) sweepIdleLoops(ctx context.Context, generation *managerGeneration) {
|
||||
defer close(generation.sweepDone)
|
||||
ticker := time.NewTicker(m.sweepInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
m.reclaimGenLoops(generation, time.Now().UTC())
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) reclaimIdleLoops(now time.Time) {
|
||||
m.mu.RLock()
|
||||
generation := m.active
|
||||
m.mu.RUnlock()
|
||||
m.reclaimGenLoops(generation, now)
|
||||
}
|
||||
|
||||
func (m *Manager) reclaimGenLoops(generation *managerGeneration, now time.Time) {
|
||||
if m.idleTTL <= 0 {
|
||||
return
|
||||
}
|
||||
m.sweepCount.Add(1)
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if !m.started || m.active != generation {
|
||||
return
|
||||
}
|
||||
for accountID, playerLoop := range generation.loops {
|
||||
if !playerLoop.canReclaim(now, m.idleTTL) {
|
||||
continue
|
||||
}
|
||||
delete(generation.loops, accountID)
|
||||
playerLoop.close()
|
||||
}
|
||||
}
|
||||
|
||||
func (l *loop) close() {
|
||||
l.closeOnce.Do(func() {
|
||||
l.mu.Lock()
|
||||
l.closed = true
|
||||
close(l.ch)
|
||||
l.mu.Unlock()
|
||||
})
|
||||
}
|
||||
|
||||
func (l *loop) submit(ctx context.Context, event Event) error {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if l.closed {
|
||||
return ErrStopped
|
||||
}
|
||||
select {
|
||||
case l.ch <- event:
|
||||
if event.ReceivedAt.After(l.lastActive) {
|
||||
l.lastActive = event.ReceivedAt
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
return ErrQueueFull
|
||||
}
|
||||
}
|
||||
|
||||
func (l *loop) markProcessing(processing bool) {
|
||||
l.mu.Lock()
|
||||
l.processing = processing
|
||||
if !processing {
|
||||
l.lastActive = time.Now().UTC()
|
||||
}
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *loop) canReclaim(now time.Time, idleTTL time.Duration) bool {
|
||||
l.mu.RLock()
|
||||
processing := l.processing
|
||||
lastActive := l.lastActive
|
||||
l.mu.RUnlock()
|
||||
if processing || len(l.ch) > 0 || lastActive.IsZero() {
|
||||
return false
|
||||
}
|
||||
return now.Sub(lastActive) >= idleTTL
|
||||
}
|
||||
Reference in new issue
Block a user