Files
90CNServer/server/internal/platform/playerloop/playerloop.go
T
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

404 lines
9.0 KiB
Go

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
}