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

272 lines
8.1 KiB
Go

package servergroup
import (
"context"
"fmt"
"sort"
"strings"
"time"
)
const (
// OperationOpenShard 表示打开分片。
OperationOpenShard = "open_shard"
// OperationDrainShard 表示排空分片。
OperationDrainShard = "drain_shard"
// OperationCloseShard 表示关闭分片。
OperationCloseShard = "close_shard"
)
// ShardOperationRequest 是分片开关服或排空操作请求。
type ShardOperationRequest struct {
ShardID string `json:"shard_id"`
State string `json:"state"`
NextVersion string `json:"next_version,omitempty"`
Reason string `json:"reason,omitempty"`
OperatorID string `json:"operator_id,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
}
// ShardOperationPlan 是分片操作预演后的计划变更。
type ShardOperationPlan struct {
Operation string `json:"operation"`
Request ShardOperationRequest `json:"request"`
Valid bool `json:"valid"`
Previous Plan `json:"previous"`
Next Plan `json:"next"`
RollbackPoint RollbackPoint `json:"rollback_point"`
AffectedTargets []Target `json:"affected_targets,omitempty"`
Warnings []string `json:"warnings,omitempty"`
}
// DryRunShardOperation 预演分片状态变更并生成回滚点。
func DryRunShardOperation(ctx context.Context, current Plan, request ShardOperationRequest) (ShardOperationPlan, error) {
if err := ctxErr(ctx); err != nil {
return ShardOperationPlan{}, err
}
request = normShardOpReq(request)
if request.ShardID == "" || !validShardOpState(request.State) {
return ShardOperationPlan{}, fmt.Errorf("%w: shard_id and supported state are required", ErrInvalidPlan)
}
normalized, _, err := normalizePlan(current)
if err != nil {
return ShardOperationPlan{}, err
}
next := clonePlan(normalized)
updated := false
for idx := range next.Shards {
if next.Shards[idx].ID != request.ShardID {
continue
}
next.Shards[idx].State = request.State
next.Shards[idx].UpdatedAt = request.UpdatedAt
next.Shards[idx].Meta = mergeOperationMeta(next.Shards[idx].Meta, request)
updated = true
break
}
if !updated {
return ShardOperationPlan{}, ErrNotFound
}
next.Version = operationNextVersion(normalized.Version, request)
next.UpdatedAt = request.UpdatedAt
manager, err := New(next)
if err != nil {
return ShardOperationPlan{}, err
}
affected, err := shardOpTargets(ctx, normalized, manager, request.ShardID)
if err != nil {
return ShardOperationPlan{}, err
}
normalizedNext := manager.Snapshot()
return ShardOperationPlan{
Operation: shardOperationName(request.State),
Request: request,
Valid: true,
Previous: normalized,
Next: normalizedNext,
RollbackPoint: newRollbackPoint(RollbackOperationShardOperation, normalized.Version, normalizedNext.Version, request.Reason, request.OperatorID, normalized, normalizedNext.UpdatedAt),
AffectedTargets: affected,
Warnings: shardOpWarnings(request, affected),
}, nil
}
// ApplyShardOperation 应用分片状态变更到管理器。
func ApplyShardOperation(ctx context.Context, manager *Manager, request ShardOperationRequest) (ShardOperationPlan, error) {
if manager == nil {
return ShardOperationPlan{}, ErrNotFound
}
plan, err := DryRunShardOperation(ctx, manager.Snapshot(), request)
if err != nil {
return ShardOperationPlan{}, err
}
if err := manager.Replace(ctx, plan.Next); err != nil {
return ShardOperationPlan{}, err
}
return plan, nil
}
// OpenShard 打开指定分片。
func OpenShard(ctx context.Context, manager *Manager, shardID, nextVersion, reason, operatorID string) (ShardOperationPlan, error) {
return ApplyShardOperation(ctx, manager, ShardOperationRequest{
ShardID: shardID,
State: StateOpen,
NextVersion: nextVersion,
Reason: reason,
OperatorID: operatorID,
})
}
// DrainShard 将指定分片切为排空状态。
func DrainShard(ctx context.Context, manager *Manager, shardID, nextVersion, reason, operatorID string) (ShardOperationPlan, error) {
return ApplyShardOperation(ctx, manager, ShardOperationRequest{
ShardID: shardID,
State: StateDraining,
NextVersion: nextVersion,
Reason: reason,
OperatorID: operatorID,
})
}
// CloseShard 关闭指定分片。
func CloseShard(ctx context.Context, manager *Manager, shardID, nextVersion, reason, operatorID string) (ShardOperationPlan, error) {
return ApplyShardOperation(ctx, manager, ShardOperationRequest{
ShardID: shardID,
State: StateClosed,
NextVersion: nextVersion,
Reason: reason,
OperatorID: operatorID,
})
}
func normShardOpReq(request ShardOperationRequest) ShardOperationRequest {
request.ShardID = normalizeID(request.ShardID)
request.State = normalizeState(request.State)
request.NextVersion = normalizeID(request.NextVersion)
request.Reason = strings.TrimSpace(request.Reason)
request.OperatorID = strings.TrimSpace(request.OperatorID)
request.Metadata = normalizeStringMap(request.Metadata)
request.UpdatedAt = normalizeTime(request.UpdatedAt)
if request.UpdatedAt.IsZero() {
request.UpdatedAt = time.Now().UTC()
}
return request
}
func validShardOpState(state string) bool {
return validState(state)
}
func shardOperationName(state string) string {
switch state {
case StateOpen:
return OperationOpenShard
case StateDraining:
return OperationDrainShard
case StateClosed:
return OperationCloseShard
default:
return "shard_operation"
}
}
func operationNextVersion(current string, request ShardOperationRequest) string {
if request.NextVersion != "" {
return request.NextVersion
}
current = normalizeID(current)
suffix := request.ShardID + "-" + request.State
if current == "" {
return suffix
}
return current + "-" + suffix
}
func mergeOperationMeta(base map[string]string, request ShardOperationRequest) map[string]string {
out := cloneStringMap(base)
if out == nil {
out = make(map[string]string)
}
for key, value := range request.Metadata {
out[key] = value
}
if request.Reason != "" {
out["operation.reason"] = request.Reason
}
if request.OperatorID != "" {
out["operation.operator_id"] = request.OperatorID
}
out["operation.state"] = request.State
return out
}
func shardOpTargets(ctx context.Context, previous Plan, manager *Manager, shardID string) ([]Target, error) {
features := shardOpFeatures(previous, shardID)
out := make([]Target, 0, len(features))
for _, feature := range features {
target, ok, err := manager.Resolve(ctx, feature, shardID)
if err != nil {
return nil, err
}
if ok {
out = append(out, target)
}
}
return out, nil
}
func shardOpFeatures(plan Plan, shardID string) []string {
seen := map[string]struct{}{}
var features []string
for _, route := range plan.Routes {
if route.ShardID != shardID {
continue
}
if route.Feature == "" {
continue
}
if _, ok := seen[route.Feature]; ok {
continue
}
seen[route.Feature] = struct{}{}
features = append(features, route.Feature)
}
for _, shard := range plan.Shards {
if shard.ID != shardID {
continue
}
for _, group := range plan.Groups {
if group.ID != shard.GroupID {
continue
}
feature := firstNonEmpty(group.Service, "default")
if _, ok := seen[feature]; !ok {
seen[feature] = struct{}{}
features = append(features, feature)
}
break
}
}
if len(features) == 0 {
features = append(features, "default")
}
sort.Strings(features)
return features
}
func shardOpWarnings(request ShardOperationRequest, targets []Target) []string {
var warnings []string
if request.State != StateOpen && request.Reason == "" {
warnings = append(warnings, "non-open shard operation should include reason")
}
for _, target := range targets {
if request.State == StateOpen && !target.Available {
warnings = append(warnings, "opened shard still unavailable:"+target.Reason)
}
if request.State != StateOpen && target.Available {
warnings = append(warnings, "non-open shard still routes as available:"+target.Feature)
}
}
return warnings
}