服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
423 lines
14 KiB
Go
423 lines
14 KiB
Go
package servergroup
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func normalizePlan(plan Plan) (Plan, planIndex, error) {
|
|
plan.Version = normalizeID(plan.Version)
|
|
if plan.UpdatedAt.IsZero() {
|
|
plan.UpdatedAt = time.Now().UTC()
|
|
} else {
|
|
plan.UpdatedAt = plan.UpdatedAt.UTC()
|
|
}
|
|
index := planIndex{
|
|
shards: make(map[string]Shard, len(plan.Shards)),
|
|
groups: make(map[string]Group, len(plan.Groups)),
|
|
warZones: make(map[string]WarZone, len(plan.WarZones)),
|
|
mergeGroups: make(map[string]MergeGroup, len(plan.MergeGroups)),
|
|
routes: make(map[string]Route, len(plan.Routes)),
|
|
conflicts: make(map[string]Conflict, len(plan.Conflicts)),
|
|
}
|
|
for _, group := range plan.Groups {
|
|
group = normalizeGroup(group)
|
|
if group.ID == "" {
|
|
return Plan{}, planIndex{}, fmt.Errorf("%w: group id is required", ErrInvalidPlan)
|
|
}
|
|
if !validState(group.State) {
|
|
return Plan{}, planIndex{}, fmt.Errorf("%w: group %s has invalid state %s", ErrInvalidPlan, group.ID, group.State)
|
|
}
|
|
if _, exists := index.groups[group.ID]; exists {
|
|
return Plan{}, planIndex{}, fmt.Errorf("%w: duplicate group %s", ErrInvalidPlan, group.ID)
|
|
}
|
|
index.groups[group.ID] = group
|
|
}
|
|
for _, shard := range plan.Shards {
|
|
shard = normalizeShard(shard)
|
|
if shard.ID == "" || shard.GroupID == "" {
|
|
return Plan{}, planIndex{}, fmt.Errorf("%w: shard id and group id are required", ErrInvalidPlan)
|
|
}
|
|
if !validState(shard.State) {
|
|
return Plan{}, planIndex{}, fmt.Errorf("%w: shard %s has invalid state %s", ErrInvalidPlan, shard.ID, shard.State)
|
|
}
|
|
if _, exists := index.shards[shard.ID]; exists {
|
|
return Plan{}, planIndex{}, fmt.Errorf("%w: duplicate shard %s", ErrInvalidPlan, shard.ID)
|
|
}
|
|
group, ok := index.groups[shard.GroupID]
|
|
if !ok {
|
|
return Plan{}, planIndex{}, fmt.Errorf("%w: shard %s references missing group %s", ErrInvalidPlan, shard.ID, shard.GroupID)
|
|
}
|
|
index.shards[shard.ID] = shard
|
|
group.Shards = appendUnique(group.Shards, shard.ID)
|
|
index.groups[shard.GroupID] = group
|
|
}
|
|
for _, group := range index.groups {
|
|
for _, shardID := range group.Shards {
|
|
shard, ok := index.shards[shardID]
|
|
if !ok {
|
|
return Plan{}, planIndex{}, fmt.Errorf("%w: group %s references missing shard %s", ErrInvalidPlan, group.ID, shardID)
|
|
}
|
|
if shard.GroupID != group.ID {
|
|
return Plan{}, planIndex{}, fmt.Errorf("%w: group %s references shard %s owned by group %s", ErrInvalidPlan, group.ID, shardID, shard.GroupID)
|
|
}
|
|
}
|
|
}
|
|
for _, warZone := range plan.WarZones {
|
|
warZone = normalizeWarZone(warZone)
|
|
if warZone.ID == "" {
|
|
return Plan{}, planIndex{}, fmt.Errorf("%w: warzone id is required", ErrInvalidPlan)
|
|
}
|
|
if !validWarZoneStatus(warZone.Status) {
|
|
return Plan{}, planIndex{}, fmt.Errorf("%w: warzone %s has invalid status %s", ErrInvalidPlan, warZone.ID, warZone.Status)
|
|
}
|
|
if !validState(warZone.State) {
|
|
return Plan{}, planIndex{}, fmt.Errorf("%w: warzone %s has invalid state %s", ErrInvalidPlan, warZone.ID, warZone.State)
|
|
}
|
|
if !warZone.NoticeAt.IsZero() && !warZone.OpenAt.IsZero() && warZone.NoticeAt.After(warZone.OpenAt) {
|
|
return Plan{}, planIndex{}, fmt.Errorf("%w: warzone %s notice_at must not be after open_at", ErrInvalidPlan, warZone.ID)
|
|
}
|
|
if _, exists := index.warZones[warZone.ID]; exists {
|
|
return Plan{}, planIndex{}, fmt.Errorf("%w: duplicate warzone %s", ErrInvalidPlan, warZone.ID)
|
|
}
|
|
if warZone.LeaderGroupID != "" {
|
|
if _, ok := index.groups[warZone.LeaderGroupID]; !ok {
|
|
return Plan{}, planIndex{}, fmt.Errorf("%w: warzone %s references missing leader group %s", ErrInvalidPlan, warZone.ID, warZone.LeaderGroupID)
|
|
}
|
|
}
|
|
for _, shardID := range warZone.Shards {
|
|
shard, ok := index.shards[shardID]
|
|
if !ok {
|
|
return Plan{}, planIndex{}, fmt.Errorf("%w: warzone %s references missing shard %s", ErrInvalidPlan, warZone.ID, shardID)
|
|
}
|
|
if shard.WarZoneID != "" && shard.WarZoneID != warZone.ID {
|
|
return Plan{}, planIndex{}, fmt.Errorf("%w: shard %s belongs to warzone %s and %s", ErrInvalidPlan, shardID, shard.WarZoneID, warZone.ID)
|
|
}
|
|
shard.WarZoneID = warZone.ID
|
|
index.shards[shardID] = shard
|
|
}
|
|
index.warZones[warZone.ID] = warZone
|
|
}
|
|
for shardID, shard := range index.shards {
|
|
if shard.WarZoneID == "" {
|
|
continue
|
|
}
|
|
warZone, ok := index.warZones[shard.WarZoneID]
|
|
if !ok {
|
|
return Plan{}, planIndex{}, fmt.Errorf("%w: shard %s references missing warzone %s", ErrInvalidPlan, shardID, shard.WarZoneID)
|
|
}
|
|
if !containsID(warZone.Shards, shardID) {
|
|
return Plan{}, planIndex{}, fmt.Errorf("%w: shard %s references warzone %s but is not listed by it", ErrInvalidPlan, shardID, shard.WarZoneID)
|
|
}
|
|
}
|
|
for _, mergeGroup := range plan.MergeGroups {
|
|
mergeGroup = normalizeMergeGroup(mergeGroup)
|
|
if mergeGroup.ID == "" || mergeGroup.MainShardID == "" {
|
|
return Plan{}, planIndex{}, fmt.Errorf("%w: merge group id and main shard id are required", ErrInvalidPlan)
|
|
}
|
|
if !validState(mergeGroup.State) {
|
|
return Plan{}, planIndex{}, fmt.Errorf("%w: merge group %s has invalid state %s", ErrInvalidPlan, mergeGroup.ID, mergeGroup.State)
|
|
}
|
|
if _, exists := index.mergeGroups[mergeGroup.ID]; exists {
|
|
return Plan{}, planIndex{}, fmt.Errorf("%w: duplicate merge group %s", ErrInvalidPlan, mergeGroup.ID)
|
|
}
|
|
if _, ok := index.shards[mergeGroup.MainShardID]; !ok {
|
|
return Plan{}, planIndex{}, fmt.Errorf("%w: merge group %s references missing main shard %s", ErrInvalidPlan, mergeGroup.ID, mergeGroup.MainShardID)
|
|
}
|
|
mergeGroup.Shards = appendUnique(mergeGroup.Shards, mergeGroup.MainShardID)
|
|
for _, shardID := range mergeGroup.Shards {
|
|
shard, ok := index.shards[shardID]
|
|
if !ok {
|
|
return Plan{}, planIndex{}, fmt.Errorf("%w: merge group %s references missing shard %s", ErrInvalidPlan, mergeGroup.ID, shardID)
|
|
}
|
|
if shard.MergeGroupID != "" && shard.MergeGroupID != mergeGroup.ID {
|
|
return Plan{}, planIndex{}, fmt.Errorf("%w: shard %s belongs to merge group %s and %s", ErrInvalidPlan, shardID, shard.MergeGroupID, mergeGroup.ID)
|
|
}
|
|
shard.MergeGroupID = mergeGroup.ID
|
|
index.shards[shardID] = shard
|
|
}
|
|
index.mergeGroups[mergeGroup.ID] = mergeGroup
|
|
}
|
|
for shardID, shard := range index.shards {
|
|
if shard.MergeGroupID == "" {
|
|
continue
|
|
}
|
|
mergeGroup, ok := index.mergeGroups[shard.MergeGroupID]
|
|
if !ok {
|
|
return Plan{}, planIndex{}, fmt.Errorf("%w: shard %s references missing merge group %s", ErrInvalidPlan, shardID, shard.MergeGroupID)
|
|
}
|
|
if !containsID(mergeGroup.Shards, shardID) {
|
|
return Plan{}, planIndex{}, fmt.Errorf("%w: shard %s references merge group %s but is not listed by it", ErrInvalidPlan, shardID, shard.MergeGroupID)
|
|
}
|
|
}
|
|
for _, route := range plan.Routes {
|
|
route = normalizeRoute(route)
|
|
if route.Feature == "" || route.ShardID == "" || route.GroupID == "" {
|
|
return Plan{}, planIndex{}, fmt.Errorf("%w: route feature, shard id and group id are required", ErrInvalidPlan)
|
|
}
|
|
if !validState(route.State) {
|
|
return Plan{}, planIndex{}, fmt.Errorf("%w: route %s/%s has invalid state %s", ErrInvalidPlan, route.Feature, route.ShardID, route.State)
|
|
}
|
|
if _, ok := index.shards[route.ShardID]; !ok {
|
|
return Plan{}, planIndex{}, fmt.Errorf("%w: route references missing shard %s", ErrInvalidPlan, route.ShardID)
|
|
}
|
|
if _, ok := index.groups[route.GroupID]; !ok {
|
|
return Plan{}, planIndex{}, fmt.Errorf("%w: route references missing group %s", ErrInvalidPlan, route.GroupID)
|
|
}
|
|
key := routeKey(route.Feature, route.ShardID)
|
|
if _, exists := index.routes[key]; exists {
|
|
return Plan{}, planIndex{}, fmt.Errorf("%w: duplicate route %s/%s", ErrInvalidPlan, route.Feature, route.ShardID)
|
|
}
|
|
index.routes[key] = route
|
|
}
|
|
for _, conflict := range plan.Conflicts {
|
|
conflict = normalizeConflict(conflict)
|
|
if conflict.Feature == "" || (conflict.ShardID == "" && conflict.GroupID == "") {
|
|
return Plan{}, planIndex{}, fmt.Errorf("%w: conflict feature and scope are required", ErrInvalidPlan)
|
|
}
|
|
if conflict.ShardID != "" {
|
|
if _, ok := index.shards[conflict.ShardID]; !ok {
|
|
return Plan{}, planIndex{}, fmt.Errorf("%w: conflict references missing shard %s", ErrInvalidPlan, conflict.ShardID)
|
|
}
|
|
}
|
|
if conflict.GroupID != "" {
|
|
if _, ok := index.groups[conflict.GroupID]; !ok {
|
|
return Plan{}, planIndex{}, fmt.Errorf("%w: conflict references missing group %s", ErrInvalidPlan, conflict.GroupID)
|
|
}
|
|
}
|
|
index.conflicts[conflictKey(conflict.Feature, conflict.ShardID, conflict.GroupID)] = conflict
|
|
}
|
|
normalized := Plan{
|
|
Version: plan.Version,
|
|
Shards: make([]Shard, 0, len(index.shards)),
|
|
Groups: make([]Group, 0, len(index.groups)),
|
|
WarZones: make([]WarZone, 0, len(index.warZones)),
|
|
MergeGroups: make([]MergeGroup, 0, len(index.mergeGroups)),
|
|
Routes: make([]Route, 0, len(index.routes)),
|
|
Conflicts: make([]Conflict, 0, len(index.conflicts)),
|
|
UpdatedAt: plan.UpdatedAt,
|
|
}
|
|
for _, shard := range index.shards {
|
|
normalized.Shards = append(normalized.Shards, cloneShard(shard))
|
|
}
|
|
for _, group := range index.groups {
|
|
sort.Strings(group.Shards)
|
|
index.groups[group.ID] = group
|
|
normalized.Groups = append(normalized.Groups, cloneGroup(group))
|
|
}
|
|
for _, warZone := range index.warZones {
|
|
sort.Strings(warZone.Shards)
|
|
index.warZones[warZone.ID] = warZone
|
|
normalized.WarZones = append(normalized.WarZones, cloneWarZone(warZone))
|
|
}
|
|
for _, mergeGroup := range index.mergeGroups {
|
|
sort.Strings(mergeGroup.Shards)
|
|
index.mergeGroups[mergeGroup.ID] = mergeGroup
|
|
normalized.MergeGroups = append(normalized.MergeGroups, cloneMergeGroup(mergeGroup))
|
|
}
|
|
for _, route := range index.routes {
|
|
normalized.Routes = append(normalized.Routes, cloneRoute(route))
|
|
}
|
|
for _, conflict := range index.conflicts {
|
|
normalized.Conflicts = append(normalized.Conflicts, conflict)
|
|
}
|
|
sortPlan(&normalized)
|
|
return normalized, index, nil
|
|
}
|
|
|
|
func normalizeShard(shard Shard) Shard {
|
|
shard.ID = normalizeID(shard.ID)
|
|
shard.GroupID = normalizeID(shard.GroupID)
|
|
shard.State = normalizeState(shard.State)
|
|
shard.OpenAt = normalizeTime(shard.OpenAt)
|
|
shard.PublicOpenAt = normalizeTime(shard.PublicOpenAt)
|
|
shard.WarZoneID = normalizeID(shard.WarZoneID)
|
|
shard.MergeGroupID = normalizeID(shard.MergeGroupID)
|
|
shard.UpdatedAt = normalizeTime(shard.UpdatedAt)
|
|
shard.Meta = normalizeStringMap(shard.Meta)
|
|
return shard
|
|
}
|
|
|
|
func normalizeGroup(group Group) Group {
|
|
group.ID = normalizeID(group.ID)
|
|
group.Service = normalizeID(group.Service)
|
|
group.MemberID = normalizeID(group.MemberID)
|
|
group.State = normalizeState(group.State)
|
|
group.Weight = normalizeWeight(group.Weight)
|
|
group.UpdatedAt = normalizeTime(group.UpdatedAt)
|
|
group.Shards = normalizeIDs(group.Shards)
|
|
group.Meta = normalizeStringMap(group.Meta)
|
|
return group
|
|
}
|
|
|
|
func normalizeWarZone(warZone WarZone) WarZone {
|
|
warZone.ID = normalizeID(warZone.ID)
|
|
warZone.LeaderGroupID = normalizeID(warZone.LeaderGroupID)
|
|
warZone.Shards = normalizeIDs(warZone.Shards)
|
|
warZone.State = normalizeState(warZone.State)
|
|
warZone.Status = normWarStatus(warZone.Status)
|
|
warZone.SeasonID = normalizeID(warZone.SeasonID)
|
|
warZone.NoticeAt = normalizeTime(warZone.NoticeAt)
|
|
warZone.OpenAt = normalizeTime(warZone.OpenAt)
|
|
warZone.UpdatedAt = normalizeTime(warZone.UpdatedAt)
|
|
warZone.Meta = normalizeStringMap(warZone.Meta)
|
|
return warZone
|
|
}
|
|
|
|
func normalizeMergeGroup(mergeGroup MergeGroup) MergeGroup {
|
|
mergeGroup.ID = normalizeID(mergeGroup.ID)
|
|
mergeGroup.MainShardID = normalizeID(mergeGroup.MainShardID)
|
|
mergeGroup.Shards = normalizeIDs(mergeGroup.Shards)
|
|
mergeGroup.State = normalizeState(mergeGroup.State)
|
|
mergeGroup.UpdatedAt = normalizeTime(mergeGroup.UpdatedAt)
|
|
mergeGroup.Meta = normalizeStringMap(mergeGroup.Meta)
|
|
return mergeGroup
|
|
}
|
|
|
|
func normalizeRoute(route Route) Route {
|
|
route.Feature = normalizeID(route.Feature)
|
|
route.ShardID = normalizeID(route.ShardID)
|
|
route.GroupID = normalizeID(route.GroupID)
|
|
route.Service = normalizeID(route.Service)
|
|
route.MemberID = normalizeID(route.MemberID)
|
|
route.State = normalizeState(route.State)
|
|
route.UpdatedAt = normalizeTime(route.UpdatedAt)
|
|
route.Meta = normalizeStringMap(route.Meta)
|
|
return route
|
|
}
|
|
|
|
func normalizeConflict(conflict Conflict) Conflict {
|
|
conflict.Feature = normalizeID(conflict.Feature)
|
|
conflict.ShardID = normalizeID(conflict.ShardID)
|
|
conflict.GroupID = normalizeID(conflict.GroupID)
|
|
conflict.Reason = strings.TrimSpace(conflict.Reason)
|
|
return conflict
|
|
}
|
|
|
|
func normalizeState(state string) string {
|
|
state = normalizeID(state)
|
|
if state == "" {
|
|
return StateOpen
|
|
}
|
|
return state
|
|
}
|
|
|
|
func validState(state string) bool {
|
|
switch state {
|
|
case StateOpen, StateClosed, StateDraining:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func normWarStatus(status string) string {
|
|
status = normalizeID(status)
|
|
if status == "" {
|
|
return ""
|
|
}
|
|
return status
|
|
}
|
|
|
|
func normalizeWeight(weight int) int {
|
|
if weight <= 0 {
|
|
return 1
|
|
}
|
|
return weight
|
|
}
|
|
|
|
func normalizeTime(value time.Time) time.Time {
|
|
if value.IsZero() {
|
|
return time.Time{}
|
|
}
|
|
return value.UTC()
|
|
}
|
|
|
|
func normalizeID(value string) string {
|
|
return strings.TrimSpace(value)
|
|
}
|
|
|
|
func normalizeIDs(values []string) []string {
|
|
seen := make(map[string]struct{}, len(values))
|
|
out := make([]string, 0, len(values))
|
|
for _, value := range values {
|
|
value = normalizeID(value)
|
|
if value == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[value]; ok {
|
|
continue
|
|
}
|
|
seen[value] = struct{}{}
|
|
out = append(out, value)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
func normalizeStringMap(values map[string]string) map[string]string {
|
|
if len(values) == 0 {
|
|
return nil
|
|
}
|
|
out := make(map[string]string, len(values))
|
|
for key, value := range values {
|
|
key = strings.TrimSpace(key)
|
|
if key == "" {
|
|
continue
|
|
}
|
|
out[key] = strings.TrimSpace(value)
|
|
}
|
|
if len(out) == 0 {
|
|
return nil
|
|
}
|
|
return out
|
|
}
|
|
|
|
func appendUnique(values []string, value string) []string {
|
|
value = normalizeID(value)
|
|
if value == "" {
|
|
return values
|
|
}
|
|
for _, existing := range values {
|
|
if existing == value {
|
|
return values
|
|
}
|
|
}
|
|
return append(values, value)
|
|
}
|
|
|
|
func containsID(values []string, value string) bool {
|
|
value = normalizeID(value)
|
|
for _, existing := range values {
|
|
if existing == value {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func targetFromGroup(group Group) Target {
|
|
target := Target{
|
|
GroupID: group.ID,
|
|
Service: group.Service,
|
|
MemberID: group.MemberID,
|
|
Shards: append([]string(nil), group.Shards...),
|
|
Available: group.State == StateOpen,
|
|
Meta: cloneStringMap(group.Meta),
|
|
}
|
|
if !target.Available {
|
|
target.Reason = "group_state:" + group.State
|
|
}
|
|
return target
|
|
}
|
|
|
|
func routeKey(feature, shardID string) string {
|
|
return feature + "\x00" + shardID
|
|
}
|
|
|
|
func conflictKey(feature, shardID, groupID string) string {
|
|
return feature + "\x00" + shardID + "\x00" + groupID
|
|
}
|