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,531 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"longheng.io/server/internal/platform/db"
|
||||
)
|
||||
|
||||
const (
|
||||
sumQueueMax = 4096
|
||||
sumFlushTimeout = 3 * time.Second
|
||||
sumRetryInterval = 200 * time.Millisecond
|
||||
)
|
||||
|
||||
var (
|
||||
errSumStoreNil = errors.New("player summary store is nil")
|
||||
errSumClosed = errors.New("player summary queue is closed")
|
||||
errSumFull = errors.New("player summary queue is full")
|
||||
)
|
||||
|
||||
// AsyncSummaryStore 把玩家摘要写入合并到后台,避免远端读模型拖慢玩家热路径。
|
||||
type AsyncSummaryStore struct {
|
||||
base SummaryStore
|
||||
max int
|
||||
timeout time.Duration
|
||||
|
||||
mu sync.RWMutex
|
||||
pending map[string]sumPending
|
||||
roles map[string]string
|
||||
seq uint64
|
||||
closed bool
|
||||
|
||||
wake chan struct{}
|
||||
stop chan struct{}
|
||||
done chan struct{}
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
// NewAsyncSummaryStore 创建异步玩家摘要读模型存储。
|
||||
func NewAsyncSummaryStore(base SummaryStore) *AsyncSummaryStore {
|
||||
store := &AsyncSummaryStore{
|
||||
base: base,
|
||||
max: sumQueueMax,
|
||||
timeout: sumFlushTimeout,
|
||||
pending: make(map[string]sumPending),
|
||||
roles: make(map[string]string),
|
||||
wake: make(chan struct{}, 1),
|
||||
stop: make(chan struct{}),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
go store.run()
|
||||
return store
|
||||
}
|
||||
|
||||
// SavePlayerSummary 合并玩家摘要并唤醒后台刷新。
|
||||
func (s *AsyncSummaryStore) SavePlayerSummary(ctx context.Context, summary PlayerSummary) error {
|
||||
if s == nil || s.base == nil {
|
||||
return errSumStoreNil
|
||||
}
|
||||
ctx = summaryCtx(ctx)
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
summary = normPlayerSummaryID(summary)
|
||||
if summary.AccountID == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.closed {
|
||||
return errSumClosed
|
||||
}
|
||||
if _, ok := s.pending[summary.AccountID]; !ok && len(s.pending) >= s.max {
|
||||
return errSumFull
|
||||
}
|
||||
s.putLocked(summary)
|
||||
s.wakeLocked()
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPlayerSummary 优先读取尚未落远端的最新摘要。
|
||||
func (s *AsyncSummaryStore) GetPlayerSummary(ctx context.Context, accountID string) (PlayerSummary, bool, error) {
|
||||
if s == nil || s.base == nil {
|
||||
return PlayerSummary{}, false, errSumStoreNil
|
||||
}
|
||||
ctx = summaryCtx(ctx)
|
||||
if err := ctx.Err(); err != nil {
|
||||
return PlayerSummary{}, false, err
|
||||
}
|
||||
accountID = strings.TrimSpace(accountID)
|
||||
if accountID == "" {
|
||||
return PlayerSummary{}, false, nil
|
||||
}
|
||||
if summary, ok := s.pendingByAcct(accountID); ok {
|
||||
return summary, true, nil
|
||||
}
|
||||
return s.base.GetPlayerSummary(ctx, accountID)
|
||||
}
|
||||
|
||||
// GetPlayerSummaryByRoleID 优先读取尚未落远端的角色索引。
|
||||
func (s *AsyncSummaryStore) GetPlayerSummaryByRoleID(ctx context.Context, roleID string) (PlayerSummary, bool, error) {
|
||||
if s == nil || s.base == nil {
|
||||
return PlayerSummary{}, false, errSumStoreNil
|
||||
}
|
||||
ctx = summaryCtx(ctx)
|
||||
if err := ctx.Err(); err != nil {
|
||||
return PlayerSummary{}, false, err
|
||||
}
|
||||
roleID = strings.TrimSpace(roleID)
|
||||
if roleID == "" {
|
||||
return PlayerSummary{}, false, nil
|
||||
}
|
||||
if summary, ok := s.pendingByRole(roleID); ok {
|
||||
return summary, true, nil
|
||||
}
|
||||
summary, ok, err := s.base.GetPlayerSummaryByRoleID(ctx, roleID)
|
||||
if err != nil || !ok {
|
||||
return summary, ok, err
|
||||
}
|
||||
if pending, pendingOK := s.pendingByAcct(summary.AccountID); pendingOK && pending.RoleID != roleID {
|
||||
return PlayerSummary{}, false, nil
|
||||
}
|
||||
return summary, true, nil
|
||||
}
|
||||
|
||||
// ListPlayerSummariesByAccountIDs 批量读取时合并 pending 摘要。
|
||||
func (s *AsyncSummaryStore) ListPlayerSummariesByAccountIDs(ctx context.Context, accountIDs []string) ([]PlayerSummary, error) {
|
||||
if s == nil || s.base == nil {
|
||||
return nil, errSumStoreNil
|
||||
}
|
||||
ctx = summaryCtx(ctx)
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
base, err := s.base.ListPlayerSummariesByAccountIDs(ctx, accountIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids := normSummaryIDs(accountIDs)
|
||||
byAcct := make(map[string]PlayerSummary, len(base)+len(ids))
|
||||
for _, summary := range base {
|
||||
summary = normPlayerSummaryID(summary)
|
||||
if summary.AccountID != "" {
|
||||
byAcct[summary.AccountID] = clonePlayerSummary(summary)
|
||||
}
|
||||
}
|
||||
for _, summary := range s.pendingByAccts(ids) {
|
||||
byAcct[summary.AccountID] = summary
|
||||
}
|
||||
out := make([]PlayerSummary, 0, len(ids))
|
||||
for _, accountID := range ids {
|
||||
if summary, ok := byAcct[accountID]; ok {
|
||||
out = append(out, clonePlayerSummary(summary))
|
||||
}
|
||||
}
|
||||
sortPlayerSummaries(out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ListByRoleIDs 批量读取角色索引时合并 pending 摘要并屏蔽旧索引。
|
||||
func (s *AsyncSummaryStore) ListByRoleIDs(ctx context.Context, roleIDs []string) ([]PlayerSummary, error) {
|
||||
if s == nil || s.base == nil {
|
||||
return nil, errSumStoreNil
|
||||
}
|
||||
ctx = summaryCtx(ctx)
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
base, err := s.base.ListByRoleIDs(ctx, roleIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids := normSummaryIDs(roleIDs)
|
||||
byRole := make(map[string]PlayerSummary, len(base)+len(ids))
|
||||
for _, summary := range base {
|
||||
summary = normPlayerSummaryID(summary)
|
||||
if summary.RoleID != "" {
|
||||
byRole[summary.RoleID] = clonePlayerSummary(summary)
|
||||
}
|
||||
}
|
||||
pending := s.pendingAll()
|
||||
pendingByAcct := make(map[string]PlayerSummary, len(pending))
|
||||
for _, summary := range pending {
|
||||
pendingByAcct[summary.AccountID] = summary
|
||||
}
|
||||
for roleID, summary := range byRole {
|
||||
if pending, ok := pendingByAcct[summary.AccountID]; ok && pending.RoleID != roleID {
|
||||
delete(byRole, roleID)
|
||||
}
|
||||
}
|
||||
for _, pending := range pending {
|
||||
if pending.RoleID != "" {
|
||||
byRole[pending.RoleID] = pending
|
||||
}
|
||||
}
|
||||
out := make([]PlayerSummary, 0, len(ids))
|
||||
for _, roleID := range ids {
|
||||
if summary, ok := byRole[roleID]; ok {
|
||||
out = append(out, clonePlayerSummary(summary))
|
||||
}
|
||||
}
|
||||
sortPlayerSummaries(out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// SearchPlayerSummaries 搜索时合并 pending 摘要,保证读到最新公开状态。
|
||||
func (s *AsyncSummaryStore) SearchPlayerSummaries(ctx context.Context, query PlayerSummaryQuery) ([]PlayerSummary, error) {
|
||||
if s == nil || s.base == nil {
|
||||
return nil, errSumStoreNil
|
||||
}
|
||||
if len(query.AccountIDs) > 0 {
|
||||
summaries, err := s.ListPlayerSummariesByAccountIDs(ctx, query.AccountIDs)
|
||||
return filterSummaries(summaries, query), err
|
||||
}
|
||||
if len(query.RoleIDs) > 0 {
|
||||
summaries, err := s.ListByRoleIDs(ctx, query.RoleIDs)
|
||||
return filterSummaries(summaries, query), err
|
||||
}
|
||||
ctx = summaryCtx(ctx)
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
base, err := s.base.SearchPlayerSummaries(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
byAcct := make(map[string]PlayerSummary, len(base))
|
||||
for _, summary := range base {
|
||||
summary = normPlayerSummaryID(summary)
|
||||
if summary.AccountID != "" {
|
||||
byAcct[summary.AccountID] = clonePlayerSummary(summary)
|
||||
}
|
||||
}
|
||||
for _, pending := range s.pendingAll() {
|
||||
if summaryMatches(pending, query) {
|
||||
byAcct[pending.AccountID] = pending
|
||||
continue
|
||||
}
|
||||
delete(byAcct, pending.AccountID)
|
||||
}
|
||||
out := make([]PlayerSummary, 0, len(byAcct))
|
||||
for _, summary := range byAcct {
|
||||
out = append(out, clonePlayerSummary(summary))
|
||||
}
|
||||
sortPlayerSummaries(out)
|
||||
if query.Limit > 0 && len(out) > query.Limit {
|
||||
out = out[:query.Limit]
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Flush 同步刷新所有待写摘要。
|
||||
func (s *AsyncSummaryStore) Flush(ctx context.Context) error {
|
||||
if s == nil || s.base == nil {
|
||||
return errSumStoreNil
|
||||
}
|
||||
ctx = summaryCtx(ctx)
|
||||
for {
|
||||
batch := s.takePending()
|
||||
if len(batch) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, summary := range batch {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.base.SavePlayerSummary(ctx, summary.summary); err != nil {
|
||||
return err
|
||||
}
|
||||
s.removeSaved(summary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close 停止后台刷新,排空 pending 后关闭底层存储。
|
||||
func (s *AsyncSummaryStore) Close(ctx context.Context) error {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
closeCtx, cancel := s.closeContext(ctx)
|
||||
defer cancel()
|
||||
s.once.Do(func() {
|
||||
s.mu.Lock()
|
||||
s.closed = true
|
||||
s.mu.Unlock()
|
||||
close(s.stop)
|
||||
})
|
||||
select {
|
||||
case <-s.done:
|
||||
case <-closeCtx.Done():
|
||||
return closeCtx.Err()
|
||||
}
|
||||
return errors.Join(s.Flush(closeCtx), db.CloseOrFlush(closeCtx, s.base))
|
||||
}
|
||||
|
||||
func (s *AsyncSummaryStore) closeContext(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
ctx = summaryCtx(ctx)
|
||||
if _, ok := ctx.Deadline(); ok {
|
||||
return ctx, func() {}
|
||||
}
|
||||
timeout := sumFlushTimeout
|
||||
if s.timeout > 0 {
|
||||
timeout = s.timeout
|
||||
}
|
||||
return context.WithTimeout(ctx, timeout)
|
||||
}
|
||||
|
||||
// Check 透传底层摘要存储预检。
|
||||
func (s *AsyncSummaryStore) Check(ctx context.Context) error {
|
||||
if s == nil || s.base == nil {
|
||||
return errSumStoreNil
|
||||
}
|
||||
return db.Check(ctx, s.base)
|
||||
}
|
||||
|
||||
func (s *AsyncSummaryStore) run() {
|
||||
defer close(s.done)
|
||||
retry := time.NewTimer(time.Hour)
|
||||
stopSumTimer(retry)
|
||||
var retryC <-chan time.Time
|
||||
defer stopSumTimer(retry)
|
||||
for {
|
||||
select {
|
||||
case <-s.wake:
|
||||
if err := s.flushBackground(); err != nil {
|
||||
resetSumTimer(retry, sumRetryInterval)
|
||||
retryC = retry.C
|
||||
} else {
|
||||
retryC = nil
|
||||
}
|
||||
case <-retryC:
|
||||
retryC = nil
|
||||
if err := s.flushBackground(); err != nil {
|
||||
resetSumTimer(retry, sumRetryInterval)
|
||||
retryC = retry.C
|
||||
}
|
||||
case <-s.stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AsyncSummaryStore) flushBackground() error {
|
||||
timeout := s.timeout
|
||||
if timeout <= 0 {
|
||||
timeout = sumFlushTimeout
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
err := s.Flush(ctx)
|
||||
cancel()
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *AsyncSummaryStore) putLocked(summary PlayerSummary) {
|
||||
if old, ok := s.pending[summary.AccountID]; ok && old.summary.RoleID != "" && old.summary.RoleID != summary.RoleID {
|
||||
delete(s.roles, old.summary.RoleID)
|
||||
}
|
||||
s.seq++
|
||||
s.pending[summary.AccountID] = sumPending{
|
||||
summary: clonePlayerSummary(summary),
|
||||
seq: s.seq,
|
||||
}
|
||||
if summary.RoleID != "" {
|
||||
s.roles[summary.RoleID] = summary.AccountID
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AsyncSummaryStore) wakeLocked() {
|
||||
select {
|
||||
case s.wake <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AsyncSummaryStore) pendingByAcct(accountID string) (PlayerSummary, bool) {
|
||||
s.mu.RLock()
|
||||
pending, ok := s.pending[accountID]
|
||||
s.mu.RUnlock()
|
||||
return clonePlayerSummary(pending.summary), ok
|
||||
}
|
||||
|
||||
func (s *AsyncSummaryStore) pendingByRole(roleID string) (PlayerSummary, bool) {
|
||||
s.mu.RLock()
|
||||
accountID := s.roles[roleID]
|
||||
pending, ok := s.pending[accountID]
|
||||
s.mu.RUnlock()
|
||||
if !ok || pending.summary.RoleID != roleID {
|
||||
return PlayerSummary{}, false
|
||||
}
|
||||
return clonePlayerSummary(pending.summary), true
|
||||
}
|
||||
|
||||
func (s *AsyncSummaryStore) pendingByAccts(accountIDs []string) []PlayerSummary {
|
||||
if len(accountIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]PlayerSummary, 0, len(accountIDs))
|
||||
s.mu.RLock()
|
||||
for _, accountID := range accountIDs {
|
||||
if pending, ok := s.pending[accountID]; ok {
|
||||
out = append(out, clonePlayerSummary(pending.summary))
|
||||
}
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
sortPlayerSummaries(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *AsyncSummaryStore) pendingAll() []PlayerSummary {
|
||||
s.mu.RLock()
|
||||
out := make([]PlayerSummary, 0, len(s.pending))
|
||||
for _, pending := range s.pending {
|
||||
out = append(out, clonePlayerSummary(pending.summary))
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
sortPlayerSummaries(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *AsyncSummaryStore) takePending() []sumWrite {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
if len(s.pending) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]sumWrite, 0, len(s.pending))
|
||||
for _, pending := range s.pending {
|
||||
out = append(out, sumWrite{
|
||||
summary: clonePlayerSummary(pending.summary),
|
||||
seq: pending.seq,
|
||||
})
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
if out[i].summary.AccountID == out[j].summary.AccountID {
|
||||
return out[i].summary.RoleID < out[j].summary.RoleID
|
||||
}
|
||||
return out[i].summary.AccountID < out[j].summary.AccountID
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *AsyncSummaryStore) removeSaved(saved sumWrite) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
pending, ok := s.pending[saved.summary.AccountID]
|
||||
if !ok || pending.seq != saved.seq {
|
||||
return
|
||||
}
|
||||
delete(s.pending, saved.summary.AccountID)
|
||||
if saved.summary.RoleID != "" && s.roles[saved.summary.RoleID] == saved.summary.AccountID {
|
||||
delete(s.roles, saved.summary.RoleID)
|
||||
}
|
||||
}
|
||||
|
||||
func wrapSummaryStore(store SummaryStore) SummaryStore {
|
||||
switch store.(type) {
|
||||
case nil, *MemorySummaryStore, *AsyncSummaryStore:
|
||||
return store
|
||||
default:
|
||||
return NewAsyncSummaryStore(store)
|
||||
}
|
||||
}
|
||||
|
||||
func normSummaryIDs(ids []string) []string {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
seen := make(map[string]struct{}, len(ids))
|
||||
out := make([]string, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
id = strings.TrimSpace(id)
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
out = append(out, id)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func summaryCtx(ctx context.Context) context.Context {
|
||||
if ctx == nil {
|
||||
return context.Background()
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
func resetSumTimer(timer *time.Timer, delay time.Duration) {
|
||||
if timer == nil {
|
||||
return
|
||||
}
|
||||
if !timer.Stop() {
|
||||
select {
|
||||
case <-timer.C:
|
||||
default:
|
||||
}
|
||||
}
|
||||
timer.Reset(delay)
|
||||
}
|
||||
|
||||
func stopSumTimer(timer *time.Timer) {
|
||||
if timer == nil {
|
||||
return
|
||||
}
|
||||
if !timer.Stop() {
|
||||
select {
|
||||
case <-timer.C:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type sumPending struct {
|
||||
summary PlayerSummary
|
||||
seq uint64
|
||||
}
|
||||
|
||||
type sumWrite struct {
|
||||
summary PlayerSummary
|
||||
seq uint64
|
||||
}
|
||||
Reference in new issue
Block a user