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

304 lines
6.3 KiB
Go

package notice
import (
"context"
"errors"
"sync"
"sync/atomic"
"time"
)
const (
liveQueueMax = 4096
liveFlushTimeout = 3 * time.Second
liveRetryInterval = 200 * time.Millisecond
)
var (
errLiveClosed = errors.New("notice live publisher queue is closed")
errLiveFull = errors.New("notice live publisher queue is full")
)
// AsyncLivePublisher 把公告在线推送移出发布热路径。
// Store 和 EventLog 仍由 Service.Publish 同步完成,后台只处理可丢弃的 live fanout。
type AsyncLivePublisher struct {
next LivePublisher
max int
timeout time.Duration
mu sync.Mutex
pending []PublishResult
closed bool
flushing bool
flushDone chan struct{}
errs uint64
wake chan struct{}
stop chan struct{}
done chan struct{}
once sync.Once
}
// NewAsyncLivePublisher 创建异步 live publisher。
func NewAsyncLivePublisher(next LivePublisher) *AsyncLivePublisher {
publisher := &AsyncLivePublisher{
next: next,
max: liveQueueMax,
timeout: liveFlushTimeout,
wake: make(chan struct{}, 1),
stop: make(chan struct{}),
done: make(chan struct{}),
}
go publisher.run()
return publisher
}
// WrapAsyncLivePublisher 按需把同步 live publisher 包成异步队列。
func WrapAsyncLivePublisher(publisher LivePublisher) LivePublisher {
switch publisher.(type) {
case nil, *AsyncLivePublisher:
return publisher
default:
return NewAsyncLivePublisher(publisher)
}
}
// PublishNotice 只做入队,后台 worker 负责真正推送。
func (p *AsyncLivePublisher) PublishNotice(ctx context.Context, result PublishResult) error {
if p == nil || p.next == nil {
return ErrLivePublisherMissing
}
if err := ctxErr(ctx); err != nil {
return err
}
result = clonePublishResult(result)
p.mu.Lock()
defer p.mu.Unlock()
if p.closed {
return errLiveClosed
}
if len(p.pending) >= p.max {
atomic.AddUint64(&p.errs, 1)
return errLiveFull
}
p.pending = append(p.pending, result)
p.wakeLocked()
return nil
}
// Flush 同步排空当前待推送队列,主要给测试、管理面和服务停止使用。
func (p *AsyncLivePublisher) Flush(ctx context.Context) error {
if p == nil || p.next == nil {
return ErrLivePublisherMissing
}
if ctx == nil {
ctx = context.Background()
}
for {
batch, wait := p.beginFlush()
if wait != nil {
if err := waitLiveFlush(ctx, wait); err != nil {
return err
}
continue
}
if len(batch) == 0 {
return nil
}
if err := p.writeBatch(ctx, batch); err != nil {
return err
}
}
}
// Close 停止后台 worker,并在返回前尽量排空 live 队列。
func (p *AsyncLivePublisher) Close(ctx context.Context) error {
if p == nil {
return nil
}
closeCtx, cancel := p.closeContext(ctx)
defer cancel()
p.once.Do(func() {
p.mu.Lock()
p.closed = true
p.mu.Unlock()
close(p.stop)
})
select {
case <-p.done:
case <-closeCtx.Done():
return closeCtx.Err()
}
return errors.Join(p.Flush(closeCtx), closeLivePublisher(closeCtx, p.next))
}
func (p *AsyncLivePublisher) closeContext(ctx context.Context) (context.Context, context.CancelFunc) {
if ctx == nil {
ctx = context.Background()
}
if _, ok := ctx.Deadline(); ok {
return ctx, func() {}
}
timeout := liveFlushTimeout
if p.timeout > 0 {
timeout = p.timeout
}
return context.WithTimeout(ctx, timeout)
}
// Errors 返回后台 live fanout 失败次数。
func (p *AsyncLivePublisher) Errors() uint64 {
if p == nil {
return 0
}
return atomic.LoadUint64(&p.errs)
}
func (p *AsyncLivePublisher) run() {
defer close(p.done)
retry := time.NewTimer(time.Hour)
stopLiveTimer(retry)
var retryC <-chan time.Time
defer stopLiveTimer(retry)
for {
select {
case <-p.wake:
if err := p.flushBackground(); err != nil {
resetLiveTimer(retry, liveRetryInterval)
retryC = retry.C
} else {
retryC = nil
}
case <-retryC:
retryC = nil
if err := p.flushBackground(); err != nil {
resetLiveTimer(retry, liveRetryInterval)
retryC = retry.C
}
case <-p.stop:
return
}
}
}
func (p *AsyncLivePublisher) flushBackground() error {
timeout := p.timeout
if timeout <= 0 {
timeout = liveFlushTimeout
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
err := p.Flush(ctx)
cancel()
return err
}
func (p *AsyncLivePublisher) beginFlush() ([]PublishResult, <-chan struct{}) {
p.mu.Lock()
defer p.mu.Unlock()
if p.flushing {
return nil, p.flushDone
}
if len(p.pending) == 0 {
return nil, nil
}
batch := clonePublishResults(p.pending)
p.pending = nil
p.flushing = true
p.flushDone = make(chan struct{})
return batch, nil
}
func (p *AsyncLivePublisher) writeBatch(ctx context.Context, batch []PublishResult) error {
for idx, result := range batch {
if err := ctxErr(ctx); err != nil {
p.finishFlush(batch[idx:], err)
return err
}
if err := p.next.PublishNotice(ctx, result); err != nil {
atomic.AddUint64(&p.errs, 1)
p.finishFlush(batch[idx:], err)
return err
}
}
p.finishFlush(nil, nil)
return nil
}
func (p *AsyncLivePublisher) finishFlush(failed []PublishResult, _ error) {
p.mu.Lock()
if len(failed) > 0 {
restored := clonePublishResults(failed)
restored = append(restored, p.pending...)
p.pending = restored
}
done := p.flushDone
p.flushing = false
p.flushDone = nil
p.mu.Unlock()
if done != nil {
close(done)
}
}
func (p *AsyncLivePublisher) wakeLocked() {
select {
case p.wake <- struct{}{}:
default:
}
}
func clonePublishResults(in []PublishResult) []PublishResult {
if len(in) == 0 {
return nil
}
out := make([]PublishResult, len(in))
for idx, result := range in {
out[idx] = clonePublishResult(result)
}
return out
}
func waitLiveFlush(ctx context.Context, done <-chan struct{}) error {
select {
case <-done:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func closeLivePublisher(ctx context.Context, publisher LivePublisher) error {
if closer, ok := publisher.(interface {
Close(context.Context) error
}); ok {
return closer.Close(ctx)
}
if flusher, ok := publisher.(interface {
Flush(context.Context) error
}); ok {
return flusher.Flush(ctx)
}
return nil
}
func stopLiveTimer(timer *time.Timer) {
if timer == nil {
return
}
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
}
func resetLiveTimer(timer *time.Timer, d time.Duration) {
if d <= 0 {
d = liveRetryInterval
}
stopLiveTimer(timer)
timer.Reset(d)
}