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,567 @@
|
||||
// Package companion owns detached policies, not native APC calls.
|
||||
// No production activation is provided until the native contract is verified.
|
||||
package companion
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ErrorCode string
|
||||
|
||||
func (e ErrorCode) Error() string { return string(e) }
|
||||
|
||||
const (
|
||||
ErrSelection ErrorCode = "INVALID_SELECTION"
|
||||
ErrSelf ErrorCode = "SELF_SELECTED"
|
||||
ErrAccount ErrorCode = "ACCOUNT_MISMATCH"
|
||||
ErrSource ErrorCode = "SOURCE_UNAVAILABLE"
|
||||
ErrOnlineUnknown ErrorCode = "ONLINE_STATE_UNKNOWN"
|
||||
ErrOnline ErrorCode = "SOURCE_ONLINE"
|
||||
ErrProfile ErrorCode = "PROFILE_UNSUPPORTED"
|
||||
ErrAssets ErrorCode = "ASSET_MISMATCH"
|
||||
ErrSnapshot ErrorCode = "INVALID_SNAPSHOT"
|
||||
ErrStaleRun ErrorCode = "STALE_RUN"
|
||||
ErrTransition ErrorCode = "INVALID_TRANSITION"
|
||||
ErrCollision ErrorCode = "OWNER_COLLISION"
|
||||
ErrRoom ErrorCode = "ROOM_NOT_READY"
|
||||
ErrRevision ErrorCode = "REVISION_CONFLICT"
|
||||
)
|
||||
|
||||
func validCharacterID(value string) bool {
|
||||
n, err := strconv.ParseUint(value, 10, 16)
|
||||
return err == nil && n != 0 && strconv.FormatUint(n, 10) == value
|
||||
}
|
||||
|
||||
func validIdentity(value string) bool {
|
||||
if value == "" || len(value) > 128 || strings.TrimSpace(value) != value {
|
||||
return false
|
||||
}
|
||||
for _, r := range value {
|
||||
if r < 32 || r == 127 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
SchemaVersion int `json:"schema_version"`
|
||||
Revision uint64 `json:"revision"`
|
||||
Enabled bool `json:"enabled"`
|
||||
BalanceEnabled bool `json:"balance_enabled"`
|
||||
ProgressionEnabled bool `json:"progression_enabled"`
|
||||
RandomEquipmentEnabled bool `json:"random_equipment_enabled"`
|
||||
AI AIConfig `json:"ai"`
|
||||
AIProfiles map[string]AIProfile `json:"ai_profiles,omitempty"`
|
||||
SourceCharacterID string `json:"source_character_id,omitempty"`
|
||||
SourceCharacterIDs []string `json:"source_character_ids,omitempty"`
|
||||
}
|
||||
|
||||
// AITuning contains the bounded, user-facing timing and distance controls
|
||||
// used by the Lua-to-AIC strategy compiler. It intentionally contains no
|
||||
// raw skill ids or arbitrary PVF tokens; those are selected by the
|
||||
// profession compiler from a verified allow-list.
|
||||
//
|
||||
// A nil tuning pointer keeps old settings rows byte-compatible. Callers that
|
||||
// need effective values should use EffectiveAITuning, which supplies the
|
||||
// safe defaults below.
|
||||
type AITuning struct {
|
||||
ThinkIntervalMS int `json:"think_interval_ms,omitempty"`
|
||||
AssistAttackIntervalMS int `json:"assist_attack_interval_ms,omitempty"`
|
||||
AssistLeashX int `json:"assist_leash_x,omitempty"`
|
||||
AssistLeashY int `json:"assist_leash_y,omitempty"`
|
||||
ChaseRange int `json:"chase_range,omitempty"`
|
||||
VisionRange int `json:"vision_range,omitempty"`
|
||||
TargetStickinessMS int `json:"target_stickiness_ms,omitempty"`
|
||||
HealLeaderPercent int `json:"heal_leader_percent,omitempty"`
|
||||
HealSelfPercent int `json:"heal_self_percent,omitempty"`
|
||||
RetryMS int `json:"retry_ms,omitempty"`
|
||||
SupportRetryMS int `json:"support_retry_ms,omitempty"`
|
||||
BusyMoveMS int `json:"busy_move_ms,omitempty"`
|
||||
StuckMS int `json:"stuck_ms,omitempty"`
|
||||
DetourMS int `json:"detour_ms,omitempty"`
|
||||
GiveUpMS int `json:"give_up_ms,omitempty"`
|
||||
FollowDistance int `json:"follow_distance,omitempty"`
|
||||
}
|
||||
|
||||
func DefaultAITuning() AITuning {
|
||||
return AITuning{
|
||||
ThinkIntervalMS: 120,
|
||||
AssistAttackIntervalMS: 1800,
|
||||
AssistLeashX: 180,
|
||||
AssistLeashY: 90,
|
||||
ChaseRange: 50,
|
||||
VisionRange: 10000,
|
||||
TargetStickinessMS: 1200,
|
||||
HealLeaderPercent: 85,
|
||||
HealSelfPercent: 85,
|
||||
RetryMS: 300,
|
||||
SupportRetryMS: 1500,
|
||||
BusyMoveMS: 2500,
|
||||
StuckMS: 700,
|
||||
DetourMS: 900,
|
||||
GiveUpMS: 3500,
|
||||
FollowDistance: 75,
|
||||
}
|
||||
}
|
||||
|
||||
func EffectiveAITuning(t *AITuning) AITuning {
|
||||
if t == nil {
|
||||
return DefaultAITuning()
|
||||
}
|
||||
out := *t
|
||||
defaults := DefaultAITuning()
|
||||
if out.ThinkIntervalMS == 0 {
|
||||
out.ThinkIntervalMS = defaults.ThinkIntervalMS
|
||||
}
|
||||
if out.AssistAttackIntervalMS == 0 {
|
||||
out.AssistAttackIntervalMS = defaults.AssistAttackIntervalMS
|
||||
}
|
||||
if out.AssistLeashX == 0 {
|
||||
out.AssistLeashX = defaults.AssistLeashX
|
||||
}
|
||||
if out.AssistLeashY == 0 {
|
||||
out.AssistLeashY = defaults.AssistLeashY
|
||||
}
|
||||
if out.ChaseRange == 0 {
|
||||
out.ChaseRange = defaults.ChaseRange
|
||||
}
|
||||
if out.VisionRange == 0 {
|
||||
out.VisionRange = defaults.VisionRange
|
||||
}
|
||||
if out.TargetStickinessMS == 0 {
|
||||
out.TargetStickinessMS = defaults.TargetStickinessMS
|
||||
}
|
||||
if out.HealLeaderPercent == 0 {
|
||||
out.HealLeaderPercent = defaults.HealLeaderPercent
|
||||
}
|
||||
if out.HealSelfPercent == 0 {
|
||||
out.HealSelfPercent = defaults.HealSelfPercent
|
||||
}
|
||||
if out.RetryMS == 0 {
|
||||
out.RetryMS = defaults.RetryMS
|
||||
}
|
||||
if out.SupportRetryMS == 0 {
|
||||
out.SupportRetryMS = defaults.SupportRetryMS
|
||||
}
|
||||
if out.BusyMoveMS == 0 {
|
||||
out.BusyMoveMS = defaults.BusyMoveMS
|
||||
}
|
||||
if out.StuckMS == 0 {
|
||||
out.StuckMS = defaults.StuckMS
|
||||
}
|
||||
if out.DetourMS == 0 {
|
||||
out.DetourMS = defaults.DetourMS
|
||||
}
|
||||
if out.GiveUpMS == 0 {
|
||||
out.GiveUpMS = defaults.GiveUpMS
|
||||
}
|
||||
if out.FollowDistance == 0 {
|
||||
out.FollowDistance = defaults.FollowDistance
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Validate checks user-editable limits before a strategy can be compiled.
|
||||
func (t AITuning) Validate() error {
|
||||
positive := map[string]int{
|
||||
"think_interval_ms": t.ThinkIntervalMS,
|
||||
"assist_attack_interval_ms": t.AssistAttackIntervalMS,
|
||||
"assist_leash_x": t.AssistLeashX,
|
||||
"assist_leash_y": t.AssistLeashY,
|
||||
"chase_range": t.ChaseRange,
|
||||
"vision_range": t.VisionRange,
|
||||
"target_stickiness_ms": t.TargetStickinessMS,
|
||||
"retry_ms": t.RetryMS,
|
||||
"support_retry_ms": t.SupportRetryMS,
|
||||
"busy_move_ms": t.BusyMoveMS,
|
||||
"stuck_ms": t.StuckMS,
|
||||
"detour_ms": t.DetourMS,
|
||||
"give_up_ms": t.GiveUpMS,
|
||||
"follow_distance": t.FollowDistance,
|
||||
}
|
||||
limits := map[string][2]int{
|
||||
"think_interval_ms": {50, 2000}, "assist_attack_interval_ms": {200, 10000},
|
||||
"assist_leash_x": {30, 500}, "assist_leash_y": {20, 300},
|
||||
"chase_range": {20, 1000}, "vision_range": {100, 20000},
|
||||
"target_stickiness_ms": {0, 10000}, "retry_ms": {50, 5000},
|
||||
"support_retry_ms": {100, 10000}, "busy_move_ms": {100, 10000},
|
||||
"stuck_ms": {100, 5000}, "detour_ms": {100, 5000},
|
||||
"give_up_ms": {500, 20000}, "follow_distance": {20, 300},
|
||||
}
|
||||
for name, value := range positive {
|
||||
if value == 0 {
|
||||
continue
|
||||
}
|
||||
limit := limits[name]
|
||||
if value < limit[0] || value > limit[1] {
|
||||
return ErrSelection
|
||||
}
|
||||
}
|
||||
if t.HealLeaderPercent != 0 && (t.HealLeaderPercent < 1 || t.HealLeaderPercent > 100) {
|
||||
return ErrSelection
|
||||
}
|
||||
if t.HealSelfPercent != 0 && (t.HealSelfPercent < 1 || t.HealSelfPercent > 100) {
|
||||
return ErrSelection
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AIProfile is the user-facing strategy selector for one support soldier.
|
||||
type AIProfile struct {
|
||||
Behavior string `json:"behavior"` // assist or aggressive; kept for wire compatibility
|
||||
ClassID uint16 `json:"class_id,omitempty"`
|
||||
BuffPolicy string `json:"buff_policy"`
|
||||
ComboPreset string `json:"combo_preset"`
|
||||
ComboEnabled bool `json:"combo_enabled"`
|
||||
AOEMinTargets int `json:"aoe_min_targets"`
|
||||
ProtectLeader bool `json:"protect_leader"`
|
||||
Tuning *AITuning `json:"tuning,omitempty"`
|
||||
}
|
||||
|
||||
func DefaultAIProfile(classID uint16) AIProfile {
|
||||
return AIProfile{
|
||||
Behavior: "assist",
|
||||
ClassID: classID,
|
||||
BuffPolicy: "leader_first",
|
||||
ComboPreset: "auto",
|
||||
ComboEnabled: true,
|
||||
AOEMinTargets: 3,
|
||||
ProtectLeader: true,
|
||||
Tuning: nil,
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
AIBuffPolicyLeaderFirst = "leader_first"
|
||||
AIBuffPolicyAssistOnly = "assist_only"
|
||||
AIBuffPolicyOff = "off"
|
||||
)
|
||||
|
||||
func normalizeBehavior(value string) string {
|
||||
switch value {
|
||||
case "", "assist":
|
||||
return "assist"
|
||||
case "aggressive":
|
||||
return "aggressive"
|
||||
case "custom":
|
||||
// The old custom mode had no client owner. Existing rows are safely
|
||||
// migrated to the deterministic assist policy.
|
||||
return "assist"
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeBuffPolicy(value string) string {
|
||||
switch value {
|
||||
case "":
|
||||
return AIBuffPolicyLeaderFirst
|
||||
case AIBuffPolicyAssistOnly, AIBuffPolicyOff:
|
||||
return value
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeAOEMinTargets(value int) int {
|
||||
if value == 0 {
|
||||
return 3
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func NormalizeAIProfile(p AIProfile) AIProfile {
|
||||
p.Behavior = normalizeBehavior(p.Behavior)
|
||||
p.BuffPolicy = normalizeBuffPolicy(p.BuffPolicy)
|
||||
if strings.TrimSpace(p.ComboPreset) == "" {
|
||||
p.ComboPreset = "auto"
|
||||
}
|
||||
p.AOEMinTargets = normalizeAOEMinTargets(p.AOEMinTargets)
|
||||
return p
|
||||
}
|
||||
|
||||
func (p AIProfile) Validate() error {
|
||||
p = NormalizeAIProfile(p)
|
||||
if p.Behavior != "assist" && p.Behavior != "aggressive" {
|
||||
return ErrSelection
|
||||
}
|
||||
if p.BuffPolicy != AIBuffPolicyLeaderFirst &&
|
||||
p.BuffPolicy != AIBuffPolicyAssistOnly && p.BuffPolicy != AIBuffPolicyOff {
|
||||
return ErrSelection
|
||||
}
|
||||
if p.AOEMinTargets < 2 || p.AOEMinTargets > 6 {
|
||||
return ErrSelection
|
||||
}
|
||||
if p.ComboPreset == "" {
|
||||
return ErrSelection
|
||||
}
|
||||
if len(p.ComboPreset) > 64 || strings.TrimSpace(p.ComboPreset) != p.ComboPreset {
|
||||
return ErrSelection
|
||||
}
|
||||
for _, r := range p.ComboPreset {
|
||||
if r < 32 || r == 127 {
|
||||
return ErrSelection
|
||||
}
|
||||
}
|
||||
if p.Tuning != nil {
|
||||
if err := p.Tuning.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cloneAIProfiles(profiles map[string]AIProfile) map[string]AIProfile {
|
||||
if profiles == nil {
|
||||
return nil
|
||||
}
|
||||
cloned := make(map[string]AIProfile, len(profiles))
|
||||
for key, profile := range profiles {
|
||||
cloned[key] = NormalizeAIProfile(profile)
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
// AIConfig contains account-level defaults for the strategy compiler. A
|
||||
// profile's tuning takes precedence over this optional account default.
|
||||
type AIConfig struct {
|
||||
BuffPolicy string `json:"buff_policy"`
|
||||
ComboPreset string `json:"combo_preset"`
|
||||
ComboEnabled bool `json:"combo_enabled"`
|
||||
AOEMinTargets int `json:"aoe_min_targets"`
|
||||
ProtectLeader bool `json:"protect_leader"`
|
||||
Tuning *AITuning `json:"tuning,omitempty"`
|
||||
}
|
||||
|
||||
// DefaultAIConfig is the only account-level default exposed to GM. All
|
||||
// distance, timing, target and skill decisions belong to the client executor.
|
||||
func DefaultAIConfig() AIConfig {
|
||||
return AIConfig{
|
||||
BuffPolicy: AIBuffPolicyLeaderFirst,
|
||||
ComboPreset: "auto",
|
||||
ComboEnabled: true,
|
||||
AOEMinTargets: 3,
|
||||
ProtectLeader: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (a AIConfig) IsZero() bool { return a == (AIConfig{}) }
|
||||
|
||||
func NormalizeAIConfig(a AIConfig) AIConfig {
|
||||
if a.IsZero() {
|
||||
return DefaultAIConfig()
|
||||
}
|
||||
if a.BuffPolicy == "" {
|
||||
a.BuffPolicy = AIBuffPolicyLeaderFirst
|
||||
}
|
||||
if strings.TrimSpace(a.ComboPreset) == "" {
|
||||
a.ComboPreset = "auto"
|
||||
}
|
||||
if a.AOEMinTargets == 0 {
|
||||
a.AOEMinTargets = 3
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// EffectiveAI keeps legacy rows working without rewriting them on read.
|
||||
func (c Config) EffectiveAI() AIConfig {
|
||||
return NormalizeAIConfig(c.AI)
|
||||
}
|
||||
|
||||
func (a AIConfig) Validate() error {
|
||||
if a.IsZero() {
|
||||
return nil
|
||||
}
|
||||
if a.BuffPolicy != "" && a.BuffPolicy != AIBuffPolicyLeaderFirst &&
|
||||
a.BuffPolicy != AIBuffPolicyAssistOnly && a.BuffPolicy != AIBuffPolicyOff {
|
||||
return ErrSelection
|
||||
}
|
||||
if a.ComboPreset != "" && (len(a.ComboPreset) > 64 || strings.TrimSpace(a.ComboPreset) != a.ComboPreset) {
|
||||
return ErrSelection
|
||||
}
|
||||
if a.AOEMinTargets != 0 && (a.AOEMinTargets < 2 || a.AOEMinTargets > 6) {
|
||||
return ErrSelection
|
||||
}
|
||||
if a.Tuning != nil {
|
||||
if err := a.Tuning.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DefaultConfig() Config { return Config{SchemaVersion: 1} }
|
||||
|
||||
func (c Config) Sources() []string {
|
||||
values := c.SourceCharacterIDs
|
||||
if len(values) == 0 && c.SourceCharacterID != "" {
|
||||
values = []string{c.SourceCharacterID}
|
||||
}
|
||||
out := make([]string, 0, len(values))
|
||||
seen := make(map[string]struct{}, len(values))
|
||||
for _, value := range values {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[value]; ok {
|
||||
continue
|
||||
}
|
||||
seen[value] = struct{}{}
|
||||
out = append(out, value)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (c Config) Validate() error {
|
||||
if c.SchemaVersion != 1 {
|
||||
return ErrSelection
|
||||
}
|
||||
sources := c.Sources()
|
||||
if c.Enabled && len(sources) == 0 {
|
||||
return ErrSelection
|
||||
}
|
||||
for _, source := range sources {
|
||||
if !validCharacterID(source) {
|
||||
return ErrSelection
|
||||
}
|
||||
}
|
||||
if c.SourceCharacterID != "" {
|
||||
if !validCharacterID(c.SourceCharacterID) ||
|
||||
len(sources) == 0 || sources[0] != c.SourceCharacterID {
|
||||
return ErrSelection
|
||||
}
|
||||
}
|
||||
if err := c.AI.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
for characterID, profile := range c.AIProfiles {
|
||||
if !validCharacterID(characterID) {
|
||||
return ErrSelection
|
||||
}
|
||||
if err := profile.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func canonicalSourceCharacters(sources []string) ([]string, error) {
|
||||
out := make([]string, 0, len(sources))
|
||||
seen := make(map[string]struct{}, len(sources))
|
||||
for _, source := range sources {
|
||||
source = strings.TrimSpace(source)
|
||||
if source == "" || !validCharacterID(source) {
|
||||
return nil, ErrSelection
|
||||
}
|
||||
if _, ok := seen[source]; ok {
|
||||
continue
|
||||
}
|
||||
seen[source] = struct{}{}
|
||||
out = append(out, source)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// NextConfigMany is pure validation, NOT an atomic persistent compare-and-swap.
|
||||
// The account-scoped policy supports any number of distinct source characters.
|
||||
// Selection is frozen for the next dungeon and no source respawns during a run.
|
||||
func NextConfigMany(c Config, expected uint64, enabled bool, sources []string, owner string, switches ...bool) (Config, error) {
|
||||
return NextConfigManyWithAI(c, expected, enabled, sources, owner, c.AI, switches...)
|
||||
}
|
||||
|
||||
// NextConfigManyWithAI updates the account policy and freezes the requested AI
|
||||
// controls for the next dungeon. Passing a zero AIConfig preserves the
|
||||
// current value, which keeps older callers source-compatible.
|
||||
func NextConfigManyWithAI(c Config, expected uint64, enabled bool, sources []string, owner string, ai AIConfig, switches ...bool) (Config, error) {
|
||||
return NextConfigManyWithAIProfiles(c, expected, enabled, sources, owner, ai, nil, switches...)
|
||||
}
|
||||
|
||||
// NextConfigManyWithAIProfiles is the account-scoped update entry point used
|
||||
// by the GM when it edits individual support-soldier roles. A nil profile map
|
||||
// preserves the existing map for legacy callers; a non-nil map replaces it
|
||||
// atomically with the same revision as the source selection and AI controls.
|
||||
func NextConfigManyWithAIProfiles(c Config, expected uint64, enabled bool, sources []string, owner string, ai AIConfig, profiles map[string]AIProfile, switches ...bool) (Config, error) {
|
||||
if err := c.Validate(); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
if c.Revision != expected || c.Revision == ^uint64(0) {
|
||||
return Config{}, ErrRevision
|
||||
}
|
||||
if !validCharacterID(owner) {
|
||||
return Config{}, ErrSelection
|
||||
}
|
||||
normalized, err := canonicalSourceCharacters(sources)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
if enabled && len(normalized) == 0 {
|
||||
return Config{}, ErrSelection
|
||||
}
|
||||
if ai.IsZero() {
|
||||
ai = c.AI
|
||||
}
|
||||
if !ai.IsZero() {
|
||||
ai = NormalizeAIConfig(ai)
|
||||
}
|
||||
if err := ai.Validate(); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
if profiles != nil {
|
||||
for characterID, profile := range profiles {
|
||||
if !validCharacterID(characterID) {
|
||||
return Config{}, ErrSelection
|
||||
}
|
||||
if err := profile.Validate(); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
}
|
||||
}
|
||||
balanced := c.BalanceEnabled
|
||||
if len(switches) > 0 {
|
||||
balanced = switches[0]
|
||||
}
|
||||
progressionEnabled := c.ProgressionEnabled
|
||||
if len(switches) > 1 {
|
||||
progressionEnabled = switches[1]
|
||||
}
|
||||
randomEquipmentEnabled := c.RandomEquipmentEnabled
|
||||
if len(switches) > 2 {
|
||||
randomEquipmentEnabled = switches[2]
|
||||
}
|
||||
next := Config{
|
||||
SchemaVersion: 1,
|
||||
Revision: c.Revision + 1,
|
||||
Enabled: enabled,
|
||||
BalanceEnabled: balanced,
|
||||
ProgressionEnabled: progressionEnabled,
|
||||
RandomEquipmentEnabled: randomEquipmentEnabled,
|
||||
AI: ai,
|
||||
AIProfiles: cloneAIProfiles(c.AIProfiles),
|
||||
SourceCharacterIDs: normalized,
|
||||
}
|
||||
if profiles != nil {
|
||||
next.AIProfiles = cloneAIProfiles(profiles)
|
||||
}
|
||||
if len(normalized) != 0 {
|
||||
next.SourceCharacterID = normalized[0]
|
||||
}
|
||||
return next, nil
|
||||
}
|
||||
|
||||
// NextConfig preserves the original single-source API for older callers/tests.
|
||||
func NextConfig(c Config, expected uint64, enabled bool, source, owner string, eligibility error, switches ...bool) (Config, error) {
|
||||
if enabled && source == owner {
|
||||
return Config{}, ErrSelf
|
||||
}
|
||||
if enabled && eligibility != nil {
|
||||
return Config{}, eligibility
|
||||
}
|
||||
sources := []string{}
|
||||
if strings.TrimSpace(source) != "" {
|
||||
sources = append(sources, source)
|
||||
}
|
||||
return NextConfigMany(c, expected, enabled, sources, owner, switches...)
|
||||
}
|
||||
Reference in new issue
Block a user