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,261 @@
|
||||
package readmodel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type RebuildOptions struct {
|
||||
AfterID string `json:"after_id,omitempty"`
|
||||
BatchSize int `json:"batch_size,omitempty"`
|
||||
MaxRecords int `json:"max_records,omitempty"`
|
||||
}
|
||||
|
||||
type RebuildResult struct {
|
||||
Scanned int `json:"scanned"`
|
||||
Rebuilt int `json:"rebuilt"`
|
||||
ActiveRebuilt int `json:"active_rebuilt,omitempty"`
|
||||
LastID string `json:"last_id,omitempty"`
|
||||
Complete bool `json:"complete"`
|
||||
}
|
||||
|
||||
type StreamRebuildResult struct {
|
||||
Scanned int `json:"scanned"`
|
||||
Rebuilt int `json:"rebuilt"`
|
||||
ActiveRebuilt int `json:"active_rebuilt,omitempty"`
|
||||
LastID string `json:"last_id,omitempty"`
|
||||
Complete bool `json:"complete"`
|
||||
Pages int `json:"pages"`
|
||||
}
|
||||
|
||||
type RebuildRunner[T any, V any] struct {
|
||||
Scan func(context.Context, string, int) ([]T, error)
|
||||
Flush func(context.Context) error
|
||||
ActiveSnapshot func() []T
|
||||
RecordID func(T) string
|
||||
Build func(T) V
|
||||
Save func(context.Context, V) error
|
||||
}
|
||||
|
||||
type RebuildProgressFunc func(RebuildResult) error
|
||||
|
||||
func Rebuild[T any, V any](ctx context.Context, options RebuildOptions, runner RebuildRunner[T, V]) (RebuildResult, error) {
|
||||
if err := runner.validate(); err != nil {
|
||||
return RebuildResult{}, err
|
||||
}
|
||||
ctx = contextOrBackground(ctx)
|
||||
options = normRebuildOpts(options)
|
||||
|
||||
after := strings.TrimSpace(options.AfterID)
|
||||
result := RebuildResult{}
|
||||
activeAfterID := after
|
||||
rebuiltActive := make(map[string]struct{})
|
||||
if runner.Flush != nil {
|
||||
if err := runner.Flush(ctx); err != nil {
|
||||
return result, fmt.Errorf("flush source before read model rebuild: %w", err)
|
||||
}
|
||||
}
|
||||
activeRecords, activeByRecordID := snapshotActive(runner)
|
||||
|
||||
for result.Scanned < options.MaxRecords {
|
||||
limit := options.BatchSize
|
||||
if remaining := options.MaxRecords - result.Scanned; remaining < limit {
|
||||
limit = remaining
|
||||
}
|
||||
records, err := runner.Scan(ctx, after, limit)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
if len(records) == 0 {
|
||||
result.Complete = true
|
||||
return result, rebuildActive(ctx, runner, &result, activeRecords, activeAfterID, "", rebuiltActive)
|
||||
}
|
||||
for _, record := range records {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return result, err
|
||||
}
|
||||
recordID := strings.TrimSpace(runner.RecordID(record))
|
||||
if recordID == "" {
|
||||
return result, errors.New("read model rebuild source returned blank id")
|
||||
}
|
||||
result.Scanned++
|
||||
result.LastID = recordID
|
||||
after = recordID
|
||||
summaryRecord := record
|
||||
if active, ok := activeByRecordID[recordID]; ok {
|
||||
summaryRecord = active
|
||||
result.ActiveRebuilt++
|
||||
rebuiltActive[recordID] = struct{}{}
|
||||
}
|
||||
if err := runner.Save(ctx, runner.Build(summaryRecord)); err != nil {
|
||||
return result, err
|
||||
}
|
||||
result.Rebuilt++
|
||||
}
|
||||
if len(records) < limit {
|
||||
result.Complete = true
|
||||
return result, rebuildActive(ctx, runner, &result, activeRecords, activeAfterID, "", rebuiltActive)
|
||||
}
|
||||
}
|
||||
return result, rebuildActive(ctx, runner, &result, activeRecords, activeAfterID, result.LastID, rebuiltActive)
|
||||
}
|
||||
|
||||
func StreamRebuild[T any, V any](ctx context.Context, options RebuildOptions, runner RebuildRunner[T, V], progress RebuildProgressFunc) (StreamRebuildResult, error) {
|
||||
if err := runner.validate(); err != nil {
|
||||
return StreamRebuildResult{}, err
|
||||
}
|
||||
ctx = contextOrBackground(ctx)
|
||||
options = normStreamOpts(options)
|
||||
|
||||
after := strings.TrimSpace(options.AfterID)
|
||||
remaining := options.MaxRecords
|
||||
streamRunner := runner
|
||||
if runner.ActiveSnapshot != nil {
|
||||
activeSnapshot := runner.ActiveSnapshot()
|
||||
streamRunner.ActiveSnapshot = func() []T {
|
||||
return append([]T(nil), activeSnapshot...)
|
||||
}
|
||||
}
|
||||
var result StreamRebuildResult
|
||||
for {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return result, err
|
||||
}
|
||||
pageLimit := options.BatchSize
|
||||
if remaining > 0 && remaining < pageLimit {
|
||||
pageLimit = remaining
|
||||
}
|
||||
page, err := Rebuild(ctx, RebuildOptions{
|
||||
AfterID: after,
|
||||
BatchSize: options.BatchSize,
|
||||
MaxRecords: pageLimit,
|
||||
}, streamRunner)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
if page.Scanned == 0 && page.Rebuilt == 0 && page.Complete {
|
||||
result.Complete = true
|
||||
return result, nil
|
||||
}
|
||||
result.Pages++
|
||||
result.Scanned += page.Scanned
|
||||
result.Rebuilt += page.Rebuilt
|
||||
result.ActiveRebuilt += page.ActiveRebuilt
|
||||
if page.LastID != "" {
|
||||
result.LastID = page.LastID
|
||||
after = page.LastID
|
||||
}
|
||||
result.Complete = page.Complete
|
||||
if progress != nil {
|
||||
if err := progress(page); err != nil {
|
||||
return result, err
|
||||
}
|
||||
}
|
||||
if page.Complete {
|
||||
return result, nil
|
||||
}
|
||||
if page.Scanned == 0 || page.LastID == "" {
|
||||
return result, errors.New("read model stream rebuild made no cursor progress")
|
||||
}
|
||||
if remaining > 0 {
|
||||
remaining -= page.Scanned
|
||||
if remaining <= 0 {
|
||||
result.Complete = false
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
streamRunner.Flush = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r RebuildRunner[T, V]) validate() error {
|
||||
if r.Scan == nil {
|
||||
return errors.New("read model rebuild scan function is required")
|
||||
}
|
||||
if r.RecordID == nil {
|
||||
return errors.New("read model rebuild record id function is required")
|
||||
}
|
||||
if r.Build == nil {
|
||||
return errors.New("read model rebuild build function is required")
|
||||
}
|
||||
if r.Save == nil {
|
||||
return errors.New("read model rebuild save function is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normRebuildOpts(options RebuildOptions) RebuildOptions {
|
||||
options.AfterID = strings.TrimSpace(options.AfterID)
|
||||
if options.BatchSize <= 0 {
|
||||
options.BatchSize = 100
|
||||
}
|
||||
if options.BatchSize > 1000 {
|
||||
options.BatchSize = 1000
|
||||
}
|
||||
if options.MaxRecords <= 0 {
|
||||
options.MaxRecords = options.BatchSize
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
func normStreamOpts(options RebuildOptions) RebuildOptions {
|
||||
options.AfterID = strings.TrimSpace(options.AfterID)
|
||||
if options.BatchSize <= 0 {
|
||||
options.BatchSize = 100
|
||||
}
|
||||
if options.BatchSize > 1000 {
|
||||
options.BatchSize = 1000
|
||||
}
|
||||
if options.MaxRecords < 0 {
|
||||
options.MaxRecords = 0
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
func rebuildActive[T any, V any](ctx context.Context, runner RebuildRunner[T, V], result *RebuildResult, records []T, afterID, throughID string, skip map[string]struct{}) error {
|
||||
if len(records) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, record := range records {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
recordID := strings.TrimSpace(runner.RecordID(record))
|
||||
if recordID == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := skip[recordID]; ok {
|
||||
continue
|
||||
}
|
||||
if afterID != "" && recordID <= afterID {
|
||||
continue
|
||||
}
|
||||
if throughID != "" && recordID > throughID {
|
||||
continue
|
||||
}
|
||||
if err := runner.Save(ctx, runner.Build(record)); err != nil {
|
||||
return err
|
||||
}
|
||||
result.Rebuilt++
|
||||
result.ActiveRebuilt++
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func snapshotActive[T any, V any](runner RebuildRunner[T, V]) ([]T, map[string]T) {
|
||||
byID := make(map[string]T)
|
||||
if runner.ActiveSnapshot == nil {
|
||||
return nil, byID
|
||||
}
|
||||
records := runner.ActiveSnapshot()
|
||||
for _, record := range records {
|
||||
recordID := strings.TrimSpace(runner.RecordID(record))
|
||||
if recordID == "" {
|
||||
continue
|
||||
}
|
||||
byID[recordID] = record
|
||||
}
|
||||
return records, byID
|
||||
}
|
||||
Reference in new issue
Block a user