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,515 @@
|
||||
package hotupdate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"longheng.io/server/internal/platform/eventlog"
|
||||
)
|
||||
|
||||
const (
|
||||
EventLogIntentStream = "hotupdate.intent"
|
||||
EventLogProgressStream = "hotupdate.progress"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrControlLogRequired = errors.New("hot update eventlog control requires eventlog")
|
||||
ErrControlConflict = errors.New("hot update control idempotency key conflicts with existing intent")
|
||||
ErrInvalidControlEvent = errors.New("hot update control event payload is invalid")
|
||||
ErrControlPollInterval = errors.New("hot update control poll interval is invalid")
|
||||
ErrControlInstanceID = errors.New("hot update control instance id generation failed")
|
||||
defControlPoll = 2 * time.Second
|
||||
)
|
||||
|
||||
var controlRandRead = rand.Read
|
||||
|
||||
type EventLogControlOptions struct {
|
||||
Log *eventlog.Log
|
||||
PollInterval time.Duration
|
||||
Now func() time.Time
|
||||
}
|
||||
|
||||
type EventLogControl struct {
|
||||
log *eventlog.Log
|
||||
pollInterval time.Duration
|
||||
now func() time.Time
|
||||
|
||||
ops sync.RWMutex
|
||||
mu sync.Mutex
|
||||
intents map[string]Intent
|
||||
progress map[string][]Progress
|
||||
watchers map[string]map[chan Intent]struct{}
|
||||
done chan struct{}
|
||||
closed bool
|
||||
instanceID string
|
||||
progressSequence uint64
|
||||
}
|
||||
|
||||
type intentEnvelope struct {
|
||||
Target string `json:"target"`
|
||||
Intent Intent `json:"intent"`
|
||||
}
|
||||
|
||||
type progressEnvelope struct {
|
||||
Progress Progress `json:"progress"`
|
||||
}
|
||||
|
||||
func NewEventLogControl(options EventLogControlOptions) (*EventLogControl, error) {
|
||||
if options.Log == nil {
|
||||
return nil, ErrControlLogRequired
|
||||
}
|
||||
pollInterval := options.PollInterval
|
||||
if pollInterval == 0 {
|
||||
pollInterval = defControlPoll
|
||||
}
|
||||
if pollInterval < 0 {
|
||||
return nil, ErrControlPollInterval
|
||||
}
|
||||
now := options.Now
|
||||
if now == nil {
|
||||
now = func() time.Time { return time.Now().UTC() }
|
||||
}
|
||||
instanceID, err := newControlInstanceID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &EventLogControl{
|
||||
log: options.Log,
|
||||
pollInterval: pollInterval,
|
||||
now: now,
|
||||
intents: make(map[string]Intent),
|
||||
progress: make(map[string][]Progress),
|
||||
watchers: make(map[string]map[chan Intent]struct{}),
|
||||
done: make(chan struct{}),
|
||||
instanceID: instanceID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *EventLogControl) Publish(ctx context.Context, target string, intent Intent) error {
|
||||
if err := contextErr(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
now, err := c.ready()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
target, err = normalizeTarget(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
intent, err = normalizeIntent(intent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
release, err := c.beginWrite()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer release()
|
||||
// EventLog 控制面把 intent 持久化为事件,再通知 watcher;watcher 丢通知时可通过轮询补回最新 intent。
|
||||
key := intentIdempotencyKey(target, intent)
|
||||
existing, ok, err := c.log.GetByIdempotencyKey(ctx, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ok {
|
||||
existingIntent, err := decodeIntentEvent(existing)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !reflect.DeepEqual(existingIntent, intent) {
|
||||
return fmt.Errorf("%w: target=%s version=%s", ErrControlConflict, target, intent.Version)
|
||||
}
|
||||
c.storeIntentAndNotify(ctx, target, existingIntent)
|
||||
return nil
|
||||
}
|
||||
payload, err := json.Marshal(intentEnvelope{Target: target, Intent: intent})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.log.Append(ctx, eventlog.Event{
|
||||
Stream: EventLogIntentStream,
|
||||
Type: string(intent.Action),
|
||||
AggregateID: target,
|
||||
IdempotencyKey: key,
|
||||
Payload: payload,
|
||||
Status: eventlog.StatusPublished,
|
||||
PublishedAt: now().UTC(),
|
||||
Headers: map[string]string{
|
||||
"hotupdate_target": target,
|
||||
"hotupdate_action": string(intent.Action),
|
||||
"hotupdate_version": intent.Version,
|
||||
},
|
||||
}); err != nil {
|
||||
if errors.Is(err, eventlog.ErrIdempotencyConflict) {
|
||||
return fmt.Errorf("%w: target=%s version=%s", ErrControlConflict, target, intent.Version)
|
||||
}
|
||||
return err
|
||||
}
|
||||
c.storeIntentAndNotify(ctx, target, intent)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *EventLogControl) Watch(ctx context.Context, target string) (<-chan Intent, error) {
|
||||
if err := contextErr(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if _, err := c.ready(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
target, err := normalizeTarget(target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Watch 先注册 channel,再加载最新 intent,避免注册窗口内发布的新 intent 被漏掉。
|
||||
ch := make(chan Intent, 1)
|
||||
c.mu.Lock()
|
||||
if c.closed {
|
||||
c.mu.Unlock()
|
||||
close(ch)
|
||||
return ch, nil
|
||||
}
|
||||
if c.watchers[target] == nil {
|
||||
c.watchers[target] = make(map[chan Intent]struct{})
|
||||
}
|
||||
c.watchers[target][ch] = struct{}{}
|
||||
if intent, ok := c.intents[target]; ok {
|
||||
ch <- intent
|
||||
}
|
||||
c.mu.Unlock()
|
||||
go c.watchLoop(ctx, target, ch)
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
func (c *EventLogControl) Report(ctx context.Context, progress Progress) error {
|
||||
if err := contextErr(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
now, err := c.ready()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
progress, err = normalizeProgress(progress, now)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
release, err := c.beginWrite()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer release()
|
||||
payload, err := json.Marshal(progressEnvelope{Progress: progress})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
instanceID, sequence := c.nextProgressKeyParts()
|
||||
if _, err := c.log.Append(ctx, eventlog.Event{
|
||||
Stream: EventLogProgressStream,
|
||||
Type: string(progress.Stage),
|
||||
AggregateID: progress.Target,
|
||||
IdempotencyKey: controlProgressKey(progress, instanceID, sequence),
|
||||
Payload: payload,
|
||||
Status: eventlog.StatusPublished,
|
||||
PublishedAt: now().UTC(),
|
||||
Headers: map[string]string{
|
||||
"hotupdate_target": progress.Target,
|
||||
"hotupdate_node": progress.NodeID,
|
||||
"hotupdate_version": progress.Version,
|
||||
"hotupdate_stage": string(progress.Stage),
|
||||
},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
c.mu.Lock()
|
||||
if !c.closed {
|
||||
c.progress[progress.Target] = append(c.progress[progress.Target], progress)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *EventLogControl) beginWrite() (func(), error) {
|
||||
c.ops.RLock()
|
||||
c.mu.Lock()
|
||||
if c.closed {
|
||||
c.mu.Unlock()
|
||||
c.ops.RUnlock()
|
||||
return nil, ErrControlClosed
|
||||
}
|
||||
c.mu.Unlock()
|
||||
return c.ops.RUnlock, nil
|
||||
}
|
||||
|
||||
func (c *EventLogControl) ready() (func() time.Time, error) {
|
||||
if c == nil || c.log == nil {
|
||||
return nil, ErrControlLogRequired
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.pollInterval < 0 {
|
||||
return nil, ErrControlPollInterval
|
||||
}
|
||||
if c.pollInterval == 0 {
|
||||
c.pollInterval = defControlPoll
|
||||
}
|
||||
if c.now == nil {
|
||||
c.now = func() time.Time { return time.Now().UTC() }
|
||||
}
|
||||
if c.intents == nil {
|
||||
c.intents = make(map[string]Intent)
|
||||
}
|
||||
if c.progress == nil {
|
||||
c.progress = make(map[string][]Progress)
|
||||
}
|
||||
if c.watchers == nil {
|
||||
c.watchers = make(map[string]map[chan Intent]struct{})
|
||||
}
|
||||
if c.done == nil {
|
||||
c.done = make(chan struct{})
|
||||
}
|
||||
if c.instanceID == "" {
|
||||
instanceID, err := newControlInstanceID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.instanceID = instanceID
|
||||
}
|
||||
return c.now, nil
|
||||
}
|
||||
|
||||
func (c *EventLogControl) Snapshot() ControlSnapshot {
|
||||
if c == nil {
|
||||
return ControlSnapshot{}
|
||||
}
|
||||
c.mu.Lock()
|
||||
intents := make(map[string]Intent, len(c.intents))
|
||||
for target, intent := range c.intents {
|
||||
intents[target] = intent
|
||||
}
|
||||
progress := make(map[string][]Progress, len(c.progress))
|
||||
for target, items := range c.progress {
|
||||
progress[target] = append([]Progress(nil), items...)
|
||||
}
|
||||
watchers := make(map[string]int, len(c.watchers))
|
||||
for target, list := range c.watchers {
|
||||
watchers[target] = len(list)
|
||||
}
|
||||
snapshot := ControlSnapshot{Intents: intents, Progress: progress, Watchers: watchers, Closed: c.closed}
|
||||
c.mu.Unlock()
|
||||
if len(snapshot.Intents) == 0 {
|
||||
snapshot.Intents = nil
|
||||
}
|
||||
if len(snapshot.Progress) == 0 {
|
||||
snapshot.Progress = nil
|
||||
}
|
||||
if len(snapshot.Watchers) == 0 {
|
||||
snapshot.Watchers = nil
|
||||
}
|
||||
return snapshot
|
||||
}
|
||||
|
||||
func (c *EventLogControl) Close() error {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
c.ops.Lock()
|
||||
defer c.ops.Unlock()
|
||||
c.mu.Lock()
|
||||
if c.closed {
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
c.closed = true
|
||||
// Close 关闭所有 watcher channel;EventLog 里的 intent/progress 仍保留用于审计。
|
||||
if c.done == nil {
|
||||
c.done = make(chan struct{})
|
||||
}
|
||||
close(c.done)
|
||||
for target, watchers := range c.watchers {
|
||||
for ch := range watchers {
|
||||
close(ch)
|
||||
}
|
||||
delete(c.watchers, target)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *EventLogControl) watchLoop(ctx context.Context, target string, ch chan Intent) {
|
||||
defer c.removeWatcher(target, ch)
|
||||
// watchLoop 定期从 EventLog 读取最新 intent,作为内存通知丢失后的补偿。
|
||||
if c.isDone() {
|
||||
return
|
||||
}
|
||||
_ = c.reloadLatestIntent(ctx, target)
|
||||
if c.pollInterval <= 0 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-c.done:
|
||||
}
|
||||
return
|
||||
}
|
||||
ticker := time.NewTicker(c.pollInterval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-c.done:
|
||||
return
|
||||
case <-ticker.C:
|
||||
_ = c.reloadLatestIntent(ctx, target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *EventLogControl) isDone() bool {
|
||||
select {
|
||||
case <-c.done:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (c *EventLogControl) removeWatcher(target string, ch chan Intent) {
|
||||
c.mu.Lock()
|
||||
if watchers := c.watchers[target]; watchers != nil {
|
||||
if _, ok := watchers[ch]; ok {
|
||||
delete(watchers, ch)
|
||||
close(ch)
|
||||
}
|
||||
if len(watchers) == 0 {
|
||||
delete(c.watchers, target)
|
||||
}
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *EventLogControl) reloadLatestIntent(ctx context.Context, target string) error {
|
||||
events, err := c.log.ListByStreamAggregate(ctx, EventLogIntentStream, target, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(events) == 0 {
|
||||
return nil
|
||||
}
|
||||
intent, err := decodeIntentEvent(events[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.storeIntentAndNotify(ctx, target, intent)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *EventLogControl) storeIntentAndNotify(_ context.Context, target string, intent Intent) {
|
||||
c.mu.Lock()
|
||||
// 内存 latest 是读优化,权威记录仍是 EventLog;通知失败不会删除持久化 intent。
|
||||
if c.closed {
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
if existing, ok := c.intents[target]; ok && reflect.DeepEqual(existing, intent) {
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
c.intents[target] = intent
|
||||
for ch := range c.watchers[target] {
|
||||
sendLatestIntent(ch, intent)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func decodeIntentEvent(event eventlog.Event) (Intent, error) {
|
||||
var envelope intentEnvelope
|
||||
if len(event.Payload) == 0 {
|
||||
return Intent{}, ErrInvalidControlEvent
|
||||
}
|
||||
if err := json.Unmarshal(event.Payload, &envelope); err != nil {
|
||||
return Intent{}, fmt.Errorf("%w: %w", ErrInvalidControlEvent, err)
|
||||
}
|
||||
intent, err := normalizeIntent(envelope.Intent)
|
||||
if err != nil {
|
||||
return Intent{}, err
|
||||
}
|
||||
return intent, nil
|
||||
}
|
||||
|
||||
func intentIdempotencyKey(target string, intent Intent) string {
|
||||
if intent.Sequence > 0 {
|
||||
return fmt.Sprintf("hotupdate:intent:v1:%s:%d", encodeEventKeyPart(target), intent.Sequence)
|
||||
}
|
||||
return fmt.Sprintf("hotupdate:intent:v1:%s:%s:%s", encodeEventKeyPart(target), encodeEventKeyPart(string(intent.Action)), encodeEventKeyPart(intent.Version))
|
||||
}
|
||||
|
||||
func progressIdemKey(progress Progress) string {
|
||||
parts := []string{
|
||||
"hotupdate:progress",
|
||||
"v1",
|
||||
encodeEventKeyPart(progress.Target),
|
||||
encodeEventKeyPart(progress.NodeID),
|
||||
encodeEventKeyPart(progress.Version),
|
||||
encodeEventKeyPart(string(progress.Stage)),
|
||||
progressFingerprint(progress),
|
||||
}
|
||||
return strings.Join(parts, ":")
|
||||
}
|
||||
|
||||
func controlProgressKey(progress Progress, instanceID string, sequence uint64) string {
|
||||
parts := []string{
|
||||
progressIdemKey(progress),
|
||||
encodeEventKeyPart(instanceID),
|
||||
fmt.Sprint(sequence),
|
||||
}
|
||||
return strings.Join(parts, ":")
|
||||
}
|
||||
|
||||
func (c *EventLogControl) nextProgressKeyParts() (string, uint64) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.progressSequence++
|
||||
return c.instanceID, c.progressSequence
|
||||
}
|
||||
|
||||
func newControlInstanceID() (string, error) {
|
||||
var raw [8]byte
|
||||
if _, err := controlRandRead(raw[:]); err != nil {
|
||||
return "", fmt.Errorf("%w: %w", ErrControlInstanceID, err)
|
||||
}
|
||||
return hex.EncodeToString(raw[:]), nil
|
||||
}
|
||||
|
||||
func encodeEventKeyPart(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return "_"
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString([]byte(value))
|
||||
}
|
||||
|
||||
func progressFingerprint(progress Progress) string {
|
||||
payload := strings.Join([]string{
|
||||
strings.TrimSpace(progress.Target),
|
||||
strings.TrimSpace(progress.NodeID),
|
||||
strings.TrimSpace(progress.Version),
|
||||
strings.TrimSpace(string(progress.Stage)),
|
||||
strings.TrimSpace(progress.Status),
|
||||
strings.TrimSpace(progress.Detail),
|
||||
progress.UpdatedAt.UTC().Format(time.RFC3339Nano),
|
||||
}, "\x00")
|
||||
sum := sha256.Sum256([]byte(payload))
|
||||
return hex.EncodeToString(sum[:8])
|
||||
}
|
||||
Reference in new issue
Block a user