// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.GameLogic.Bots; using System.ComponentModel.DataAnnotations; /// /// The admin-panel editable configuration of the . /// public class BotConfiguration { /// /// The hard limit of characters a single account can hold in the game. /// public const int MaxCharactersPerAccountLimit = 5; /// /// Gets or sets a value indicating whether the bot feature is enabled. /// Disabled by default so that enabling bots is always an explicit, deliberate action. /// [Display(Name = "Enabled", Description = "If enabled, bots are spawned after the server has started.")] public bool Enabled { get; set; } /// /// Gets or sets a value indicating whether all bot accounts and characters should be deleted. /// When set, the feature purges every bot account on the next startup before generating fresh /// ones, and then automatically clears this flag again. Use it to reset the bot population. /// [Display(Name = "Reset bots", Description = "Deletes all bot accounts and characters on the next start, then regenerates them. Clears itself afterwards.")] public bool ResetBots { get; set; } /// /// Gets or sets a value indicating whether all bot accounts and characters should be deleted /// WITHOUT being regenerated. Unlike this also turns /// off - otherwise the very same pass would generate the population again - so it is the single /// switch for "I do not want bots on this server anymore". Clears itself afterwards. /// [Display(Name = "Purge bots", Description = "Deletes all bot accounts and characters and turns the bot feature off, without generating new ones. Clears itself afterwards.")] public bool PurgeBots { get; set; } /// /// Gets or sets a value indicating whether the bot population rotates its presence over the day: /// fewer bots are online at night, most in the evening, with bots smoothly logging in and out - /// like a real player base, instead of the same characters being online 24/7. /// [Display(Name = "Presence rotation", Description = "Bots log in and out over the day (fewest at night, most in the evening) instead of all being online 24/7.")] public bool PresenceRotation { get; set; } = true; /// /// Gets or sets the share (in percent) of bots which stays online at the quietest time of day. /// 100 effectively disables the rotation effect. /// [Display(Name = "Min. online share %", Description = "Percentage of the bot population which stays online at the quietest hour (100 = no rotation effect).")] public int MinOnlineSharePercent { get; set; } = 60; /// /// Gets or sets the number of bot accounts. Together with /// this defines the generated bot population, e.g. 10 accounts × 5 characters = 50 bot characters. /// [Display(Name = "Number of accounts", Description = "How many bot accounts to maintain.")] [Range(0, 1000)] public int NumberOfAccounts { get; set; } = 10; /// /// Gets or sets the number of characters per bot account. An account can hold at most /// (5) characters, so this value is clamped on use. /// [Display(Name = "Characters per account", Description = "How many characters each bot account holds (max 5).")] [Range(1, MaxCharactersPerAccountLimit)] public int MaxCharactersPerAccount { get; set; } = MaxCharactersPerAccountLimit; /// /// Gets or sets the share (in percent) of a game server's maximum player count which its bots may /// occupy. Bots count towards that limit like players do, and a full server turns new clients away - /// so the rest of the capacity stays reserved for real players, who must never be denied a slot by a /// bot. The population is split over all configured game servers accordingly (see /// ); accounts which do not fit stay offline until the servers offer /// the room for them. /// [Display(Name = "Bot capacity %", Description = "Share of a game server's maximum player count which its bots may occupy; the rest stays reserved for real players.")] [Range(1, 100)] public int BotCapacityPercent { get; set; } = 60; /// /// Gets or sets a value indicating whether bots pay the configured reset costs (zen, reset items) /// when they reset their character on a server with the reset feature enabled. Off by default: /// bots don't take part in the player economy the costs are balanced for, so charging them only /// stalls their progression (a bot can't farm zen for a billion-zen reset the way players trade). /// [Display(Name = "Bots pay reset costs", Description = "If enabled, bots consume the configured zen/item costs for their resets like human players (default: free bot resets).")] public bool BotsPayResetCosts { get; set; } /// /// Gets or sets how many Jewels of Bless, Soul and Life a bot keeps of each kind. Bots only pick up /// the jewels they can actually spend on their own gear (see BotJewelHandler) and stop /// collecting a kind once they hold this many; whatever they carry above it is sold on the next /// merchant visit. The sensible value depends entirely on the server's drop rates - on a high rate /// server a bot refills a big stock within hours, so a low limit keeps its backpack usable. /// [Display(Name = "Jewel stock per kind", Description = "How many Jewels of Bless/Soul/Life a bot keeps of each kind; above this it stops picking them up and sells the surplus.")] [Range(0, 100)] public int JewelStockPerKind { get; set; } = 10; /// /// Gets or sets the number of potion charges (per healing and per mana potions) a bot stocks up to /// at a merchant. Merchants sell potions in stacks of different sizes, so this is the target the bot /// buys towards, not a stack count. /// [Display(Name = "Potion stock (charges)", Description = "How many healing and mana potion charges a bot buys up to at a merchant.")] [Range(10, 255)] public int PotionStockCharges { get; set; } = 60; /// /// Gets or sets a comma separated list of login names of existing accounts to animate as bots. /// This is an optional extra hook alongside the generated population (see /// ): every listed account gets a bot driving its first character. /// These accounts are animated as-is and are not part of the partitioned, capacity-limited /// population, so leave it empty unless you specifically want to drive existing accounts. /// [Display(Name = "Extra accounts to animate", Description = "Comma separated login names of existing accounts to animate as bots, in addition to the generated population.")] public string ProofOfConceptAccounts { get; set; } = string.Empty; /// /// Gets the effective, clamped number of characters per account. /// /// A value between 1 and . /// Deliberately a method: a get-only property would end up in the serialized plugin configuration JSON. public int GetEffectiveCharactersPerAccount() => Math.Clamp(this.MaxCharactersPerAccount, 1, MaxCharactersPerAccountLimit); /// /// Gets the effective, clamped share of a server's player capacity which its bots may occupy. /// /// A value between 1 and 100. /// Deliberately a method, like . public int GetEffectiveBotCapacityPercent() => Math.Clamp(this.BotCapacityPercent, 1, 100); /// /// Gets the effective, clamped jewel stock a bot keeps of each usable kind. /// /// A value between 0 and 100. /// Deliberately a method, like . public int GetEffectiveJewelStockPerKind() => Math.Clamp(this.JewelStockPerKind, 0, 100); /// /// Gets the effective, clamped potion charges a bot stocks up to. /// /// A value between 10 and 255. /// Deliberately a method, like . public int GetEffectivePotionStockCharges() => Math.Clamp(this.PotionStockCharges, 10, 255); /// /// Parses into the distinct, trimmed login names. /// /// The list of login names. /// Deliberately a method: a get-only property would end up in the serialized plugin configuration JSON. public IReadOnlyList ParseProofOfConceptAccounts() => this.ProofOfConceptAccounts .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); }