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

154 lines
3.3 KiB
Go

package player
import (
"context"
"encoding/json"
"errors"
"log/slog"
"sync"
"sync/atomic"
"time"
"longheng.io/server/internal/platform/eventlog"
)
const (
defProfileEventQueue = 1024
profileEventTimeout = 3 * time.Second
)
var (
errEventClosed = errors.New("profile event queue is closed")
errEventFull = errors.New("profile event queue is full")
)
type asyncProfileEvents struct {
next ProfileEventAppender
logger *slog.Logger
errs *uint64
timeout time.Duration
queue chan eventlog.Event
mu sync.Mutex
closed bool
once sync.Once
stopped chan struct{}
}
func newAsyncEvents(next ProfileEventAppender, logger *slog.Logger, errs *uint64) *asyncProfileEvents {
events := &asyncProfileEvents{
next: next,
logger: logger,
errs: errs,
timeout: profileEventTimeout,
queue: make(chan eventlog.Event, defProfileEventQueue),
stopped: make(chan struct{}),
}
go events.run()
return events
}
func (a *asyncProfileEvents) Append(ctx context.Context, event eventlog.Event) (eventlog.Event, error) {
if a == nil || a.next == nil {
return eventlog.Event{}, errEventClosed
}
if ctx == nil {
ctx = context.Background()
}
if err := ctx.Err(); err != nil {
return eventlog.Event{}, err
}
event = cloneProfileEvent(event)
a.mu.Lock()
defer a.mu.Unlock()
if a.closed {
return eventlog.Event{}, errEventClosed
}
select {
case a.queue <- event:
return event, nil
default:
return eventlog.Event{}, errEventFull
}
}
func (a *asyncProfileEvents) Close(ctx context.Context) error {
if a == nil {
return nil
}
closeCtx, cancel := a.closeContext(ctx)
defer cancel()
a.once.Do(func() {
a.mu.Lock()
a.closed = true
close(a.queue)
a.mu.Unlock()
})
select {
case <-a.stopped:
return nil
case <-closeCtx.Done():
return closeCtx.Err()
}
}
func (a *asyncProfileEvents) run() {
defer close(a.stopped)
for event := range a.queue {
ctx, cancel := context.WithTimeout(context.Background(), a.timeoutOrDefault())
_, err := a.next.Append(ctx, event)
cancel()
if err != nil {
a.recordError(event, err)
}
}
}
func (a *asyncProfileEvents) closeContext(ctx context.Context) (context.Context, context.CancelFunc) {
if ctx == nil {
ctx = context.Background()
}
if _, ok := ctx.Deadline(); ok {
return ctx, func() {}
}
return context.WithTimeout(ctx, a.timeoutOrDefault())
}
func (a *asyncProfileEvents) timeoutOrDefault() time.Duration {
if a == nil || a.timeout <= 0 {
return profileEventTimeout
}
return a.timeout
}
func (a *asyncProfileEvents) recordError(event eventlog.Event, err error) {
if a.errs != nil {
atomic.AddUint64(a.errs, 1)
}
if a.logger != nil {
a.logger.Error("player profile async eventlog append failed", "account_id", event.AggregateID, "event_type", event.Type, "error", err)
}
}
func (m *Module) closeProfileEvents(ctx context.Context) error {
if m == nil || m.eventAsync == nil {
return nil
}
return m.eventAsync.Close(ctx)
}
func cloneProfileEvent(event eventlog.Event) eventlog.Event {
if event.Payload != nil {
event.Payload = append(json.RawMessage(nil), event.Payload...)
}
if event.Headers != nil {
headers := make(map[string]string, len(event.Headers))
for key, value := range event.Headers {
headers[key] = value
}
event.Headers = headers
}
return event
}