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,422 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user