[Feature] OpenMU Bots#820
Conversation
Reuse the connection-less OfflinePlayer and its MU Helper AI for standalone server-side bots: - Make the hunting origin dynamic (OfflinePlayer.HuntingOrigin) so a bot can roam between hunting grounds; the combat and movement handlers read it instead of a fixed spawn position. - Add BotPlayer (respawns and keeps going) and BotNavigator, which picks a level-appropriate hunting ground from the map's monster spawns and walks there with a full-grid path finder, then hunts locally. The travel stop range matches the combat hunting range, so a bot never stalls next to a monster it cannot reach. - Add BotMuHelperSettings giving the bot a real hunting range so it actually fights; auto-repair is disabled to avoid the offline zen drain.
…e 2) - Add the Account.IsBot flag (and its migration) as the reliable marker for bot accounts, independent of their names. - Generate a configurable population (N accounts with up to 5 characters each) with unique, realistic procedurally-generated character names and random creatable classes and levels. Generation is idempotent and the accounts are persistent, so they survive restarts and are loaded instead of regenerated. - Spawn every bot character on startup. Several bots animate different characters of the same account at once; this is safe because the shared account row is only attached, never modified. - Add a "Reset bots" configuration flag which purges all bot accounts and regenerates them on the next start, then clears itself again. - The bot feature plugin appears in the admin panel "Features" section and keeps the proof-of-concept account hook as an optional extra.
When the current map has no monster spawns within the bot's full- experience, reasonably safe level band, the navigator warps the bot to a random enterable map whose monsters do fit its level, arriving at the destination map's safezone and then walking out to a hunting ground like a real player. A cooldown prevents bouncing between maps. This stops over-levelled bots from grinding starter-map monsters far below their level for almost no experience. Known limitation: the map change is not yet persisted (the assignment of the shared game-configuration map entity is not tracked by the bot's own context), so after a restart a bot reloads on its home map and warps again. Character progress (level, experience, items) is unaffected.
Generated bots were created naked with unspent level-up points, so a high-level bot fought with level-1 base stats and no gear and died instantly without earning any experience. - Spend the level-up points at generation (half into vitality for health, half into the class's main damage stat), so a bot actually has the combat power of its level. - Equip a basic, class-appropriate weapon and matching armor set (Small Axe + Leather, Skull Staff + Pad, Short Bow + arrows + Vine), each piece in its correct equipment slot. - Recalibrate the navigator: a monster is far tougher than a character of the same level, so bots now hunt monsters at roughly half their own level (tunable via SafeMonsterFactor) and warp to the map offering the strongest monsters they can still safely handle. Result on the live lab: deaths dropped by roughly an order of magnitude and the bots gain experience instead of dying in a loop.
…nd robust navigation - pick a class-correct weapon by stat archetype (bow for agility, staff for energy, melee otherwise) instead of letting every class end up with a Small Axe, and choose an armor set the class is actually qualified to wear - carry a stack of healing potions and top it up at runtime, so the offline healing handler has something to drink; bots were dying with no way to heal - stop bots standing in town: aim the combat centre at the destination while in a safezone and walk out, rather than swinging at monsters they cannot damage - recover from wedged walks and unreachable hunting grounds via a watchdog and an immediate re-pick of a nearer, reachable ground - only warp once a bot reaches level 30, to avoid sending low-level bots to maps far above their level
Bots don't accumulate Zen fast enough to cover the periodic PC-Cafe fee, so they went bankrupt and their offline sessions stopped, leaving few bots online. Skip the fee in ZenConsumptionHandler when Account.IsBot is true. Human offline-leveling players (IsBot == false) keep paying as before.
- BotManager: OfflinePlayer.InitializeAsync now takes (loginName, characterName) and loads the account fresh (dualcontext fix from MUnique#806), instead of captured Account/Character references. - ZenConsumptionHandler.DeductZenAsync now returns bool; the bot exemption returns true (bot may continue) instead of a bare return.
Scale (250 bots): the single shared long-distance pathfinder serialized navigation for all bots, so at 250 they starved and got stuck in town. Replace it with a small pooled set (SemaphoreSlim + ConcurrentBag, size clamped to the core count). Also fix loot: PickSelectItems was false, which made the pickup handler bail out before the selective Pick* flags (zen/jewels) could take effect, so bots collected nothing. Skills: bots fought with only their weapon because the offline combat picks its skill from explicit config IDs, which bots never set. Add an AutoSelectBestSkill flag to IMuHelperSettings (bots only; human MU Helper sessions keep their explicit configuration). When set, the combat AI casts the strongest learned attack skill the character can currently afford, so it scales with level and mana. BotGenerator now teaches each bot the attack skills of its own class up to a per-skill learn level derived from the skill's attack damage, and a new ICharacterLevelUpPlugIn (BotSkillProgressionPlugIn) grants further class skills as the bot levels up during play.
At 250 bots most stood around in town and never fought: only ~13% gained any experience. The navigator dropped each bot on a random tile inside a monster spawn area, but MU spawn areas span almost the whole map (Lorencia's are ~86x233 / ~105x68 tiles) with only a few dozen monsters, so a random point was almost never within the 6-tile combat range of an actual monster. The bot arrived, found nothing to fight, waited, and re-picked another random tile forever. Now, once the bot is anywhere in the region, it scans a 40-tile radius via the area-of-interest manager for the nearest live monster it can safely fight and heads straight for it; the coarse spawn-area / warp logic remains only as a fallback for when nothing is loaded nearby (still in town, or all monsters too strong). Unreachable monsters are dropped so the bot relocates instead of re-targeting them. Measured at 250 bots: actively-fighting fraction 13% -> 73%, with the rest purposefully travelling; CPU/RAM unchanged (~31%/760MB).
… hunting Buffs/heals: bots now learn and use their class's support skills like a real player - elf Heal/Greater Defense/Greater Damage, Rage Fighter Increase Block/Increase Health, and (once high enough) DK Swell Life etc. Skill learning (generation and level-up) is now gated by the skills' real learn requirements from the game configuration (total energy, leadership, character level) instead of a damage heuristic, so a bot knows exactly what a human of the same build could know. Stat points are invested per a class build (elves and rage fighters keep enough energy for their support skills, dark lords raise leadership), and points earned at runtime are now spent on level-up too - previously they accumulated unused. The offline buff handler can auto-rotate the learned buffs (AutoSelectBuffs), the heal handler casts the class heal before drinking potions, and enemy debuffs (summoner Sleep/Weakness/Innovation) and the shield-granted Defense skill are excluded so a bot never puts itself to sleep. Casters also carry and drink mana potions now - before, they degraded to weak melee once their mana ran dry. Equipment progression: dropped gear is evaluated before pickup and only class-appropriate upgrades are collected; a new BotEquipmentHandler periodically equips the best of them through the regular MoveItemAction (which enforces the usual requirements) and drops the replaced piece, so backpacks don't silt up. Weapons must match the class's fighting style (an elf only considers bows, a wizard staves) and ammunition is never displaced. Bots also get the four inventory extensions for buffer space. Hunting intelligence (from a review of the bot AI): the combat AI now only engages monsters up to the navigator's safe level cap (bots no longer pick suicidal fights while travelling - deaths dropped to zero in validation); unreachable targets (across a wall/river) are briefly blacklisted instead of freezing the bot; monster homing also works inside the safezone, so bots leave town straight toward the nearest monsters; travel routes are cached and consumed hop by hop instead of re-planning a whole-map A* every second; target selection is randomized among the nearest candidates to avoid dogpiling; and navigator start times are jittered. Validated live with 250 bots: 100% of them gained experience in a 6-minute window (73% before this change, 13% before the homing fix), zero deaths, zero errors, weapon archetypes clean across all classes, mana potions consumed by all skill-using classes.
A bot account animates several of its characters concurrently, each in its own persistence context with its own stale copy of the account. When two sibling characters crossed a class-unlock level at (nearly) the same time, both passed the duplicate check of UnlockCharacterAtLevelBase - neither saw the other's insert - and both added the same unlock row. From then on every periodic save of that account failed with a duplicate key on PK_AccountCharacterClass, so the affected bots stopped persisting any progress until the next server restart. Observed live with 250 bots: 17 failed saves across 5 accounts within half an hour (the Summoner unlock triggers at level 1, so every bot level-up was a candidate). Bots never create new characters, so they don't need class unlocks at all - skip them for bot accounts. Human accounts are unaffected; with one character online per account, the race cannot occur for them. Validated live: zero unlock rows and zero failed saves for bot accounts after the change (previously growing within minutes).
Fixes: - The warp destination map is now re-resolved through the bot's own persistence context after warping; the warp assigns the global configuration instance which the bot's context does not track, so the CurrentMapId foreign key silently never saved and every restarted bot woke up on its home map and warped all over again. - Bots that have outgrown their map now warp away with priority. The nearest-monster homing always found SOMETHING huntable on the starter map, so the warp logic was never reached and high-level bots farmed level-5 mobs on Lorencia forever (validated: 0 warps in 8 minutes before, 115 in 10 minutes after, with the map population spreading out again). - Skill learning on level-up is queued into the bot's AI tick instead of running concurrently with it, so the skill list is never mutated while the combat handler enumerates it. - Archers keep their ammunition topped up (an upgraded bow can consume arrows; the starter bow does not). - The auto-buff list is cached and only rebuilt when a skill is learned - building it with LINQ on every 500ms tick of 250 bots was measurable CPU. Human behavior: - Self-defense: a new plugin notes when a player attacks a bot, and the combat AI prioritizes that aggressor over its monster targets (plain attacks only - the area-skill path deals damage exclusively to monsters). Previously a bot placidly kept farming while a player killed it. - A small randomized reaction delay before engaging a fresh target (the bot faces it first), instead of the giveaway instant metronomic strike. - Closing in on a target now walks straight along the line towards it instead of re-randomizing a nearby point every tick (visible zig-zag). - The AI tick phase is randomized per player, so hundreds of bots don't act on the same 500ms boundary.
A share (~60%) of the bots now forms hunting parties of 2-5 level-wise similar characters at startup, through the regular party manager. The party leader is the lowest-level member, so it only ever picks maps the whole group can hunt on; the other members follow it - warping to the leader's map when it warped away (with a short cooldown, so the group regroups quickly) and walking back to the leader when they drift more than a few tiles off. Near the leader they hunt normally; followers never warp on their own. With the group staying together, the already existing offline party mechanics kick in: the elf heals party members below threshold, buffs are shared with the party, and the party experience bonus applies - a group of bots now reads like a group of players. Validated live: 58 parties formed, members converged on their leaders across maps (e.g. follower and leader 6 tiles apart on Elvenland), no new error kinds, resources unchanged (~37% of one core, 680 MB for 250 bots).
Instead of the same 250 characters being online 24/7 - a giveaway no real server has - the bot population now ebbs and flows with the time of day: an hourly activity curve (quietest in the early morning, busiest in the evening) scales the online share between a configurable minimum (default 60%) and 100%. A maintenance pass adjusts by at most one bot per minute, so logins and logouts trickle in smoothly. A rotated-out bot leaves its party cleanly and disconnects like a regular logout (saving its progress); parties are re-formed hourly, so bots which rotated back in (or lost their leader) get grouped again. Validated live: population converged from 250 towards the midday target of 200 at exactly one logout per minute, with clean party exits and no new error kinds.
Bots now trade with town merchants like real players instead of materializing supplies out of thin air. When the backpack fills up with sellable junk or a potion kind runs low, the bot heads to a merchant (warping to the map's safezone first when out in the field, like using a town portal), walks up to it and trades through the regular player actions: junk gear is sold for Zen (jewels and excellent/ancient pieces stay - that's the bot's wealth), and healing/mana potions are bought with Zen up to a stock target. While the shop dialog is open, the player state pauses the combat AI - the bot visibly shops. The out-of-thin-air top-up remains only as an emergency fallback at a much lower threshold, so a broke bot far from town still never dies over an empty bottle. Freshly generated bots start with only a handful of potion charges and their starting Zen, so the shopping economy kicks in from minute one. Validated live with 250 bots: 249 shopping trips at Potion Girl Amy on the first wave, a second wave restocking to ~100-150 charges per kind for real Zen (~8k per stack of ~30), no errors in the NPC dialog flow.
The generated level range grows from 10-80 to 10-250, with a skewed distribution (low and mid levels more common, like a real population pyramid). With the higher levels, the upper maps finally get a resident bot population - validated: bots spread across 14 maps including Lost Tower, Tarkan, Aida, Kanturu, Vulcanus and Karutan, instead of only the four starter towns. At level 200 a bot changes into its second-generation class - Dark Knight to Blade Knight, Dark Wizard to Soul Master, Fairy Elf to Muse Elf, Summoner to Bloody Summoner - the exact assignment the class-change quest performs for a human player; simulating the quest run itself would be invisible to observers. Bots generated beyond 200 are created as the evolved class right away, bots that grow past 200 in play evolve on level-up. Skills, buffs and gear of the new class follow automatically, since all bot progression keys off the current class's qualifications (the stat builds cover the evolved classes too). The Magic Gladiator, Dark Lord and Rage Fighter have no second generation - their next step is the level-400 master evolution, out of bot scope. Shopping keeps a Zen reserve now, so a bot stops buying rather than spend its last coin.
Mechanical cleanup only - member reordering (constants before fields, public before private, static before instance, nested types last), missing XML parameter docs, using-directive order and blank lines. No logic changes; verified by a sorted-line diff (only doc lines and blank lines added) and a clean build with zero analyzer warnings left in the bot feature files.
The starter weapon for archer classes was picked from item group 4 by lowest drop level. Bolts and arrows share that group with drop level 0, so every generated archer got a bolt stack as its "weapon" and fought with bare fists. Exclude ammunition from the weapon selection (also in the fallback query, which covers the same groups).
The safe-hunt rule capped targets at half the bot's level, but a monster's level says nothing about its strength: the high-end maps field "level ~120" monsters hitting for 1000-2300 base damage (Swamp of Calmness, LaCleon) or with ~100k health behind ~340 defense (Vulcanus tanks), several times what regular maps' monsters of the same level deal. High-level bots in modest gear were sent straight into death loops there. A monster now counts as safe when all three hold, computed from its actual attributes against the bot's own: - its average hit (minus the bot's PvM defense) stays under a fraction of the bot's maximum health, - the bot's attack power exceeds the monster's defense with a margin, - the monster dies within a bounded number of net hits, so a bot never besieges a tank monster for ten minutes until its potions run dry. This also scales naturally with equipment: better looted gear raises the bot's defense and damage and unlocks tougher maps, like for a real player. The map-warp evaluation uses the same verdicts; its improvement margin and cooldown grew into a hysteresis band, because borderline safety verdicts flip with the bot's buffs and made two maps leapfrog each other - the bot ping-ponged between them on every cooldown. Validated at 250 bots: deaths dropped from ~9/min to ~1/min while total experience gain went up, high-level bots settled on maps they can actually farm, no more map ping-pong.
The offline MovementHandler and CombatHandler no longer take a fixed origin position - the hunting origin became a mutable property of the OfflinePlayer, so the bot navigator can move it between hunting grounds. The tests now set the player's HuntingOrigin instead of passing it to the constructors. The combat test also accounts for the human-like reaction delay: a fresh target is engaged only after a short randomized pause (the bot first turns towards it), so the walk towards an out-of-range monster starts on a subsequent AI tick.
- Format the bot name suffix with the invariant culture. - Rename ParseProofOfConceptAccounts so it is distinguishable from the ProofOfConceptAccounts property, and note why the computed config values are methods (a get-only property would end up in the serialized plugin configuration). - Name the navigator timer callback parameter properly, so assigning the fire-and-forget task actually targets a discard instead of reusing the parameter.
|
That's crazy, I'll try it when I can 😅 |
Fifteen seconds of aggression memory meant an attacker was forgiven almost immediately after breaking off. The memory still counts from the last received hit and the bot still only engages an aggressor near its position - the longer window just makes a returning attacker get engaged again on sight.
Self-defense against a player was limited to plain physical attacks, because the area-skill execution path deals its damage exclusively to monsters - a cast at a player would look flashy and hit nothing. Now the bot selects its best affordable skill against a player aggressor like it does against monsters: targeted skills go through the regular targeted path, and the area path additionally strikes the player target itself - and only the target, so a bot's self-defense never splashes uninvolved bystanders.
A bot which shrugs off being killed and calmly heads for the next hunting ground is an obvious bot giveaway - a real player comes back angry. When a human player kills a bot, the bot now marches from its respawn back to the place of its death (same map only) with re-armed aggressor memory, so it engages the killer on sight. One attempt per death, expiring after a few minutes or on arrival. If the same player kills the bot again shortly after, the bot gives up instead of feeding the killer free kills in a death loop: it avoids hunting grounds near the death site for a while and farms somewhere else. A revenge march takes priority over following the party leader, so a killed leader drags its group back with it; afterwards the routine (and party following) resumes.
Yea 🙃 The core framework and general functionality are already there. Right now, I'm working on a minor BvP fix and getting servers with the reset plugin enabled so that the bots work properly. |
An unused local and an unused parameter, both reported by the static analysis. The calls stay: the stronger piece of gear has to be equipped for the jewel test to mean anything, and the capacity callback keeps its signature.
|
The Codacy findings I went through all 19 of them. Two were real and are fixed (an unused local and an unused parameter,
So the gate stays red on those 17 no matter what I do — if you agree with the reasoning, they are |
The static analysis kept flagging the unused parameter of the local function, which cannot go: the callback signature belongs to SplitPoints. A lambda says the same thing and does not pretend the parameter matters.
The attribute system is not thread-safe, and a lost race can corrupt a character's attribute graph for good: from then on every AI tick of that bot throws the same exception, the bot stops playing, and it floods the log with a few exceptions per second until the server is restarted. We saw it twice on a 1100-bot run. A bot now counts the ticks failing in a row and, after twenty of them, asks the maintenance pass to restart it - a fresh login rebuilds the attribute graph and heals it, which is what a player would do. A single failing tick is skipped like before; one successful tick forgets the earlier failures. The two hooks on OfflinePlayer do nothing there, so the human offline mode behaves exactly as before.
Once the useful picks were exhausted, a bot started spending its master points on strengtheners of weapon types it never carries: a bow bonus for a sword swinger. Those passives target one of the weapon-specific master attributes, so they are now matched against the weapon the bot fights with - what it carries and what its build makes it pick up. The item groups come from the item data: the scepters live in the mace group, the Rage Fighter's gloves in the sword group, and sticks and books next to the staffs. The points go into something useful instead; nothing is held back.
|
Added self-healing for bots hit by the engine's attribute race, stopped master points going into unused weapon strengtheners, and documented the feature in docs/Bots.md. |
How it works, what the bots do, what they were validated with, the known limitations, and how to enable them on a server.
A master class at the maximum level only earns master experience from monsters of at least MinimumMonsterLevelForMasterExperience, and no regular experience at all - so below that line a kill pays it nothing. Bots kept hunting the strongest monsters they considered safe, which are weaker than that line, and never gained a single point of master experience. Mastered bots now pick their map and hunting ground among the monsters which pay them, and take the weakest ones above the line: master experience hardly grows with the monster's level, so the cheapest kill above it is the best one. Those monsters carry 40.000+ health, out of reach of the regular hit budget for a bot in the gear it collects from drops, so the budget is stretched for them - a long fight it survives beats a quick one worth nothing. Survivability is not stretched: a monster whose hits it cannot take is still refused.
Level-up points were only invested on a level-up, so a bot which was given points while it was not playing - a freshly generated one, or one whose level-up handler failed - carried them around unspent until its next level-up, fighting with the strength of a much weaker character in the meantime. A bot which holds points now spends them when it enters the world.
A bot spends its life hunting monsters, and it may not even attack a player unless it is attacked first - so an attack or defense rate against players does nothing for it. Those bonuses now get filled last, like the bonuses of a weapon type the bot does not carry.
|
Update: three fixes found by running the bots, not by reading them Three commits on top of the current PR head. All three were found on a lab server 1. A mastered bot earned no master experience at all (
|
A wrapped line started with a hyphen, which markdownlint reads as a list item without the surrounding blank lines.
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces a comprehensive server-side AI bot system for OpenMU. These bots act as persistent, autonomous entities that simulate player behavior, including hunting, progression, and social interaction. The feature is designed to populate servers dynamically while respecting player capacity limits and ensuring that bots never prevent real players from joining. The implementation focuses on high-fidelity simulation, allowing bots to make realistic decisions about gear, combat, and movement, while maintaining server stability and performance. Highlights
New Features🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces a comprehensive server-side AI bot system to populate the game server, featuring autonomous hunting, equipment progression, shopping, party coordination, and event participation. The review feedback highlights several key improvements: optimizing the BCrypt hashing work factor during bot generation to prevent startup CPU bottlenecks, using ConditionalWeakTable to avoid potential memory leaks of game contexts, utilizing DateTime.UtcNow.Hour for timezone consistency, and passing a CancellationToken to pathfinding pool waits to prevent hangs during server shutdown.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
A bot's password is a random Guid which is discarded at once and never used to log in - a bot is a connection-less OfflinePlayer, so no client authenticates against it. The default work factor made HashPassword the dominant cost of generating a large population (minutes for a thousand accounts). A minimal factor is safe here (a 128-bit random secret is infeasible to brute-force at any factor) and still stores a valid BCrypt hash; real accounts keep the default. Raised by the automated review of PR 820.
The daily activity curve models human presence, so it follows the players' wall clock (the host's local time), not UTC like the durations elsewhere in the class. Documented so the deviation does not read as an oversight; switching it to UtcNow would drift the evening peak away from the players' evening. Raised by the automated review of PR 820.
TravelTowardAsync queued for a shared path finder with an untokened WaitAsync, so a bot travelling while the navigator is disposed held the tick until a finder freed up. Thread the navigator's shutdown token (already captured once in Start, never touching the disposed source) down to the wait; the OperationCanceledException is expected and already handled in SafeEvaluateAsync. Raised by the automated review of PR 820.
|
@nolt man this is insane, good job very hard to review this I will try to review this feature tomorrow locally |
Review: Server-side AI Bots (PR #820)(by Claude Code 😉) I went through the full diff (58 files, ~14.8k insertions), focusing on the changes to existing code (highest regression risk), the concurrency/lifecycle design, and the persistence layer. This is an impressive, unusually well-documented contribution — Strengths worth calling out
Should fix before merge
Worth a second look
Smaller notes / questions
Overall a strong, careful implementation. The duplicate-summary warning is the only true blocker I found; the rest are refinements. 👍 |
A copy of the ServerState summary was left dangling above PersistConfigurationAsync, which raises CS1571 (duplicate XML comment tag) and can fail the build under doc generation or StyleCop. Delete the stray block; the method keeps its own summary.
The per-server startup state is a genuine three-state machine that previously needed a doc comment to explain that 0 = not started, 1 = in progress, 2 = done. Introduce a StartupPhase enum for it. The field stays an int at the CLR level because Interlocked has no overload for arbitrary enum types, so the phase constants are cast at the call sites; _maintenanceRunning stays the 0/1 interlocked flag it is.
The static cache was keyed on the MonsterDefinition instance, so a runtime configuration reload builds new instances and orphans the old keys forever. Key it by the monster number instead - the cached values are derived purely from the monster's stats - so a reload reuses the entries rather than leaking them.
The aggressor player and the aggression time were two plain fields written from the attack path and read from the AI tick. A DateTime write is not atomic, so a torn read was possible. Fold them into a single volatile Aggression record, matching the _revenge and _deathSiteToAvoid pattern already used for the same reason: the read is now a single reference load and cannot tear.
The remarks on NumberOfAccounts and ProofOfConceptAccounts read as if the generation step were still a future phase - it is implemented. Drop the obsolete note and document ProofOfConceptAccounts for what it is now: an optional extra hook to animate existing accounts alongside the generated, capacity-limited population.
The consecutive-failure guard fires on '== threshold', not '>=', on purpose: it arms the restart exactly once at the tick that crosses the threshold. Spell that out so the '==' no longer reads as a fencepost bug - further failures keep incrementing the counter without re-firing, and the maintenance pass consumes the flag and resets it.
|
Thanks for the thorough read — and for doing it at the level of the changes to Should fix before merge
Worth a second look
Smaller notes
All six edits build clean (0 errors) and the suite is green (582/582). Left the layering |
|
Thanks for this great PR, looks like our AI Agents came to a conclusion 😄 |
OpenMU Bots - Server-side AI Bots
Server-side bots are persistent, fully autonomous characters which populate the server like real
players: they hunt monsters with class-appropriate skills, level up, invest their stat points, keep
their buffs up, loot and equip better gear, go shopping in town, hunt in small parties, defend
themselves when attacked, and log in and out over the day. They are driven entirely by the server —
no game client is involved — by reusing the connection-less
OfflinePlayerand its MU Helper AI.How it works
Account.IsBot, each with realistic,randomly generated character names. They are generated once (levels, classes, stats, skills and
gear included) and reloaded on every server start; their progress is saved periodically like any
online player's. Each bot animates one character in its own persistence context, so several
characters of one account can play at the same time without cross-context conflicts.
pickup) plus a bot navigator (1 s tick: hunting grounds, travel, warps, shopping, party follow).
players do, so they must not fill it up:
BotCapacityPercent(60 by default) is the share of aserver's maximum player count its bots may occupy — the rest stays reserved for the players, who
are never denied a slot by a bot. The population is split over all configured game servers
proportionally to their capacity, and which accounts a server animates is a pure function of the
account index and the set of servers: every server computes the same answer without asking the
others (which also holds when each game server runs as its own process). Exactly one server
generates the population, so accounts and character names are never created twice. The split is
computed at startup: adding a game server takes a restart before the bots spread onto it —
deliberately, since moving a bot between two running servers would animate one account from two
contexts.
controls everything:
Enabled,NumberOfAccounts,MaxCharactersPerAccount,BotCapacityPercent,ResetBots(purge and regenerate once, self-clearing),PresenceRotationandMinOnlineSharePercent.What bots do
Hunting and movement
points — validated: 100 % of 250 bots gained experience in a 6-minute window.
bot can stop and fight on the way.
population spreads over 14+ maps (Lost Tower, Tarkan, Aida, Kanturu, Vulcanus, Karutan, …).
may legally warp to, exactly like a player using the warp window. A bot stranded on a map it
may not be on (e.g. after a reset) evacuates — to the best legal hunting map, or home.
bot's defense and health (an average hit must stay under a fraction of max HP) — a monster's
nominal level says nothing about its punch on high-end maps (Swamp of Calmness, LaCleon).
The estimate uses the game's own hit chance, so an agility build's dodge counts as the
defense it really is; without it the squishy-looking casters considered every high-end map
lethal and never left the starter maps. Better looted gear raises the bot's defense and
unlocks tougher maps, like for a real player.
Unreachable targets (across a wall or river) are briefly blacklisted instead of freezing the bot.
Combat and progression
range and drink mana potions.
level) — at generation and again on every level-up.
Increase Block, Soul Master Soul Barrier, …) and cast the class heal before drinking potions.
servers an agility/SD-tank meta (vitality left at a per-bot rolled 100–500 target), on
classic servers guide-style builds — selected automatically by the presence of the reset
feature. Classes with two viable archetypes (warrior/mage Magic Gladiator, pure/energy Blade
Knight, …) roll one per bot. Per-stat maximum values of fun servers are respected; capped
stats overflow into the rest of the build.
the reset configuration, seeded populations start with 0..ResetLimit resets, and hunting
decisions use the reset-effective level.
Dark Wizard → Soul Master, Fairy Elf → Muse Elf, Summoner → Bloody Summoner) — the same
assignment the class-change quest performs; bots generated beyond 200 start out evolved.
quests: evolve into the master class (Blade Master, Grand Master, High Elf, Dimension Master —
and directly Duel Master/Lord Emperor/Fist Master for MG/DL/RF), relog like a player (required
for the master stats to mount), then earn master levels from hunting. Every master point goes
into the master skill tree through the regular action (rank gates and skill requirements
enforced), preferring stat passives and strengtheners of skills the bot actually uses. Iron
rule on reset servers: a bot only becomes a master once its reset limit is exhausted; with no
reset limit configured it never does (resetting forever is the endgame there). No master
levels are ever seeded — every master on the server earned it in play.
Items and economy
(jewels, excellent/ancient items) are collected.
swap is planned before it starts (which slot, which pieces have to come off — a two-handed
weapon also frees the other hand, which the engine demands), and if the engine refuses the
equip after all, the old gear is put straight back on. Weapons must match the class's
fighting style (bow for agility builds, staff for casters, melee otherwise), a weapon only
goes into the main hand and a shield into the off-hand, and ammunition is never displaced.
The replaced piece stays in the backpack and is sold on the next shopping trip instead of
being dropped — the bots' hand-me-downs would otherwise litter the hunting grounds and be
picked right back up. Four inventory extensions provide buffer space.
buy potion refills with it (keeping a Zen reserve), while the shop dialog visibly pauses the
bot. Conjuring supplies out of thin air remains only as an emergency fallback.
the regular consume action (same success rates and failure penalties as for a player):
Bless pushes the weakest piece towards +6, Soul is only risked with a spare in stock — on
plain items only at +6 (a failure from +7 on would reset the item to +0), up to +9 only on
items with luck — and Life sparingly on already upgraded gear; at most two jewels per trip.
straight into the wing slot): the first pair at level 180 (+0, luck, +12 option), the second
at 280 (+9, luck, +16), the third at 400 (+15, luck, +16). Which classes wear which pair
comes from the item data — third-tier wings (master classes only) are not offered before the
master evolution, the Dark Lord/Rage Fighter capes are re-granted as a fresh +9 cape at the
second milestone (the Cape of Lord lives in group 13, unlike all other wings), and a
Magic Gladiator picks the pair matching its fighting style, as does
the wing option (wizardry/curse for casters, physical otherwise). The outgrown pair is
destroyed, never dropped; after a reset a bot keeps its better wings while re-levelling.
Mini game events
player who leads a party with bots enters with their own ticket, the party's bots enter
too (no tickets of their own - the leader's entry legitimizes the visit; bots never enter
events on their own). Each bot is checked against the same entry restrictions a player
faces (level bracket, master class requirement, player killer rule); a bot which does not
qualify leaves the party and goes back to its own hunting life. Inside, the open-world
routine (shopping, map changes, party boredom, invitations, revenge) is suspended: the bot
fights what the event throws at it, keeps up with the leader through the course and closes
in on the action. In Chaos Castle - the free-for-all - the other participants join the
bot's target pool (area skills splash them like anyone else's), while event fights leave
no grudge outside. A dead bot respawns at the safezone like a real player, which removes
it from the event; the event warps the survivors out when it ends.
Social behavior and realism
whole group can hunt its maps) leads, the others follow across maps; the elf heals the party,
buffs are shared, the party experience bonus applies.
while being killed — but only within the game's own PvP legality: a bot strikes a player
solely inside the active self-defense window or against an already-flagged player killer, so
a bot can never escalate into an outlaw that players could farm for free. The grudge memory
keeps the aggressor prioritized; a killed bot still marches back to its killer, waiting for a
legal opening.
AutoAcceptAnyoneMU Helper flag): after ahuman-like 2–5 s pause, guarded by an effective-level gap, and refused while shopping or on a
revenge march. A living player takes precedence over the bot's own company: a bot which hunts
in a bot party leaves it for the inviter (and breaks that party up if it was leading it), so a
player never has to guess which bot happens to be free. In a party the bot follows the leader,
heals as an elf, defers a due reset, and leaves politely — when the leader enters a map the bot
may not access, before its presence-rotation logout, or when it gets bored (10–20 min).
changing by at most one bot per minute; rotated-out bots leave their party cleanly and log
out like a regular player. Parties re-form hourly.
approach instead of zig-zagging, randomized AI tick phases, target choice randomized among
the nearest candidates to avoid dogpiling.
concurrently-played account would otherwise corrupt — see known issues).
Validated at scale
persistence conflicts.
persistence conflicts; the population hunts across 9+ maps and keeps up its full progression
(measured ~9.4 billion raw experience per 3 minutes at x300 rates). The two known engine
races (see below) scale linearly (~3 caught exceptions/min) and stay harmless. Generation of
1050 new bots took ~14 minutes (BCrypt per account), spawn of all 1100 about 13 seconds.
All figures from live runs on a 12-core host. A 15-minute run of the 1100-bot population after the
last round of fixes: 696 equips spread over 369 bots (at most 9 per bot — all of them genuine loot
progression), no rejected equip, no persistence conflict, no error in the AI tick.
The split over the game servers was validated on two deployments: 220 accounts on two servers
(2000 players each) → 110 accounts / 550 bots per server, and 30 accounts on the three servers of the
default seed → 10 accounts / 50 bots per server, with the servers which started before the population
existed picking up their share on the following maintenance pass. No account was ever animated twice.
The whole event feature was also played through from a game client: Blood Castle (entry behind the
party leader, the full course, exit at the end), Chaos Castle (party dissolved on entry, death and a
clean return to the open world) and Devil Square (waves, and the loot sold on the next shopping trip).
Known issues and limitations
MagicEffectsList(unsynchronizedSortedList): the effect-expiry timer vs. applying a buffMagicEffectsList) — maintainer territory; also affects heavily-buffing human playersComposableAttribute(the attribute system is not thread-safe)Enabling on a server
NumberOfAccounts(each account carries up toMaxCharactersPerAccountcharacters; 50 × 5 = 250 bots).BotCapacityPercentof itsmaximum player count (60 % of 1000 players = 600 bot characters per server). What does not fit
stays offline, and the plugin says so in the log — raise the player limit, the capacity share, or
add a game server; the population spreads over the servers by itself.
ResetBots— it purges, regenerates and clearsitself.