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,300 @@
|
||||
package scheduler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math/rand"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Job struct {
|
||||
Name string
|
||||
Interval time.Duration
|
||||
Jitter time.Duration
|
||||
Run func(context.Context) error
|
||||
}
|
||||
|
||||
type JobSnapshot struct {
|
||||
Name string `json:"name"`
|
||||
IntervalMS int64 `json:"interval_ms"`
|
||||
JitterMS int64 `json:"jitter_ms,omitempty"`
|
||||
Running bool `json:"running"`
|
||||
Runs int64 `json:"runs"`
|
||||
Errors int64 `json:"errors"`
|
||||
LastStarted *time.Time `json:"last_started,omitempty"`
|
||||
LastEnded *time.Time `json:"last_ended,omitempty"`
|
||||
LastError string `json:"last_error,omitempty"`
|
||||
NextRun *time.Time `json:"next_run,omitempty"`
|
||||
}
|
||||
|
||||
type Scheduler struct {
|
||||
name string
|
||||
logger *slog.Logger
|
||||
|
||||
mu sync.Mutex
|
||||
jobs map[string]*jobState
|
||||
running bool
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
now func() time.Time
|
||||
jitter func(time.Duration) time.Duration
|
||||
}
|
||||
|
||||
type jobState struct {
|
||||
job Job
|
||||
|
||||
running bool
|
||||
runs int64
|
||||
errors int64
|
||||
lastStarted time.Time
|
||||
lastEnded time.Time
|
||||
lastError string
|
||||
nextRun time.Time
|
||||
}
|
||||
|
||||
func New(name string, logger *slog.Logger) *Scheduler {
|
||||
if name == "" {
|
||||
name = "scheduler"
|
||||
}
|
||||
return &Scheduler{
|
||||
name: name,
|
||||
logger: logger,
|
||||
jobs: make(map[string]*jobState),
|
||||
now: time.Now,
|
||||
jitter: randomJitter,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scheduler) Name() string {
|
||||
if s == nil {
|
||||
return "scheduler"
|
||||
}
|
||||
return s.name
|
||||
}
|
||||
|
||||
func (s *Scheduler) Add(job Job) error {
|
||||
if s == nil {
|
||||
return fmt.Errorf("scheduler is nil")
|
||||
}
|
||||
job.Name = strings.TrimSpace(job.Name)
|
||||
if job.Name == "" {
|
||||
return fmt.Errorf("scheduler job name is required")
|
||||
}
|
||||
if job.Interval <= 0 {
|
||||
return fmt.Errorf("scheduler job %q interval must be positive", job.Name)
|
||||
}
|
||||
if job.Jitter < 0 {
|
||||
return fmt.Errorf("scheduler job %q jitter must not be negative", job.Name)
|
||||
}
|
||||
if job.Run == nil {
|
||||
return fmt.Errorf("scheduler job %q run function is required", job.Name)
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.running {
|
||||
return fmt.Errorf("scheduler job %q cannot be added after start", job.Name)
|
||||
}
|
||||
if _, exists := s.jobs[job.Name]; exists {
|
||||
return fmt.Errorf("scheduler job %q already exists", job.Name)
|
||||
}
|
||||
s.jobs[job.Name] = &jobState{job: job}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Scheduler) Start(ctx context.Context) error {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
s.mu.Lock()
|
||||
if s.running {
|
||||
s.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
s.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
runCtx, cancel := context.WithCancel(ctx)
|
||||
jobs := make([]*jobState, 0, len(s.jobs))
|
||||
for _, state := range s.jobs {
|
||||
jobs = append(jobs, state)
|
||||
}
|
||||
// worker 必须在 running 对 Stop 可见前全部登记,禁止 Wait 与 Add 交错。
|
||||
s.wg.Add(len(jobs))
|
||||
s.cancel = cancel
|
||||
s.running = true
|
||||
s.mu.Unlock()
|
||||
|
||||
for _, state := range jobs {
|
||||
go func() {
|
||||
defer s.wg.Done()
|
||||
s.loop(runCtx, state)
|
||||
}()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Scheduler) Stop(ctx context.Context) error {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
s.mu.Lock()
|
||||
cancel := s.cancel
|
||||
if cancel == nil && !s.running {
|
||||
s.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
s.mu.Unlock()
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
s.wg.Wait()
|
||||
s.mu.Lock()
|
||||
s.cancel = nil
|
||||
s.running = false
|
||||
s.mu.Unlock()
|
||||
close(done)
|
||||
}()
|
||||
select {
|
||||
case <-done:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scheduler) Snapshot() []JobSnapshot {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
out := make([]JobSnapshot, 0, len(s.jobs))
|
||||
for _, state := range s.jobs {
|
||||
out = append(out, state.snapshot())
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *Scheduler) loop(ctx context.Context, state *jobState) {
|
||||
interval := state.job.Interval
|
||||
delay := s.nextDelay(state)
|
||||
s.setNextRun(state, s.now().Add(delay))
|
||||
timer := time.NewTimer(delay)
|
||||
defer timer.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-timer.C:
|
||||
s.runJob(ctx, state)
|
||||
delay = interval + s.jitterDelay(state)
|
||||
next := s.now().Add(delay)
|
||||
s.setNextRun(state, next)
|
||||
timer.Reset(delay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scheduler) nextDelay(state *jobState) time.Duration {
|
||||
delay := state.job.Interval + s.jitterDelay(state)
|
||||
if delay <= 0 {
|
||||
return time.Nanosecond
|
||||
}
|
||||
return delay
|
||||
}
|
||||
|
||||
func (s *Scheduler) jitterDelay(state *jobState) time.Duration {
|
||||
if state == nil || state.job.Jitter <= 0 || s.jitter == nil {
|
||||
return 0
|
||||
}
|
||||
delay := s.jitter(state.job.Jitter)
|
||||
if delay < 0 {
|
||||
return 0
|
||||
}
|
||||
if delay > state.job.Jitter {
|
||||
return state.job.Jitter
|
||||
}
|
||||
return delay
|
||||
}
|
||||
|
||||
func (s *Scheduler) runJob(ctx context.Context, state *jobState) {
|
||||
now := s.now().UTC()
|
||||
s.mu.Lock()
|
||||
if state.running {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
state.running = true
|
||||
state.lastStarted = now
|
||||
s.mu.Unlock()
|
||||
|
||||
err := state.job.Run(ctx)
|
||||
ended := s.now().UTC()
|
||||
|
||||
s.mu.Lock()
|
||||
state.running = false
|
||||
state.runs++
|
||||
state.lastEnded = ended
|
||||
if err != nil {
|
||||
state.errors++
|
||||
state.lastError = err.Error()
|
||||
if s.logger != nil {
|
||||
s.logger.Error("scheduler job failed", "job", state.job.Name, "error", err)
|
||||
}
|
||||
} else {
|
||||
state.lastError = ""
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *Scheduler) setNextRun(state *jobState, next time.Time) {
|
||||
s.mu.Lock()
|
||||
state.nextRun = next.UTC()
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *jobState) snapshot() JobSnapshot {
|
||||
snap := JobSnapshot{
|
||||
Name: s.job.Name,
|
||||
IntervalMS: s.job.Interval.Milliseconds(),
|
||||
JitterMS: s.job.Jitter.Milliseconds(),
|
||||
Running: s.running,
|
||||
Runs: s.runs,
|
||||
Errors: s.errors,
|
||||
LastError: s.lastError,
|
||||
}
|
||||
if !s.lastStarted.IsZero() {
|
||||
t := s.lastStarted
|
||||
snap.LastStarted = &t
|
||||
}
|
||||
if !s.lastEnded.IsZero() {
|
||||
t := s.lastEnded
|
||||
snap.LastEnded = &t
|
||||
}
|
||||
if !s.nextRun.IsZero() {
|
||||
t := s.nextRun
|
||||
snap.NextRun = &t
|
||||
}
|
||||
return snap
|
||||
}
|
||||
|
||||
func randomJitter(max time.Duration) time.Duration {
|
||||
if max <= 0 {
|
||||
return 0
|
||||
}
|
||||
return time.Duration(rand.Int63n(int64(max) + 1)) // #nosec G404 -- 非安全用途:调度抖动,不用于凭证或加密。
|
||||
}
|
||||
Reference in new issue
Block a user