Skip to content

[Feature] OpenMU Bots#820

Merged
sven-n merged 62 commits into
MUnique:masterfrom
nolt:feature-bots
Jul 16, 2026
Merged

[Feature] OpenMU Bots#820
sven-n merged 62 commits into
MUnique:masterfrom
nolt:feature-bots

Conversation

@nolt

@nolt nolt commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

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 OfflinePlayer and its MU Helper AI.

How it works

  • Persistence. Bots are regular accounts flagged with 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.
  • Brains. Every bot runs the offline MU Helper AI (500 ms tick: combat, healing, buffs, item
    pickup) plus a bot navigator (1 s tick: hunting grounds, travel, warps, shopping, party follow).
  • Spread over the game servers. Bots count towards the player count of their server exactly like
    players do, so they must not fill it up: BotCapacityPercent (60 by default) is the share of a
    server'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.
  • Configuration. The Bots feature plugin ("Feature Plugins" section in the admin panel)
    controls everything: Enabled, NumberOfAccounts, MaxCharactersPerAccount, BotCapacityPercent,
    ResetBots (purge and regenerate once, self-clearing), PresenceRotation and
    MinOnlineSharePercent.

What bots do

Hunting and movement

  • Home in on actual live monsters (area-of-interest scan) instead of wandering to random spawn
    points — validated: 100 % of 250 bots gained experience in a 6-minute window.
  • Travel with a cached long-distance route (whole-map A*), consumed a few steps per hop so the
    bot can stop and fight on the way.
  • Warp to maps that match their level, with priority once they outgrow the current map — the
    population spreads over 14+ maps (Lost Tower, Tarkan, Aida, Kanturu, Vulcanus, Karutan, …).
  • Map access obeys the game's warp list: a bot only enters maps its plain (post-reset) level
    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.
  • The reached map is persisted, so a restarted bot wakes up where it stopped.
  • Only engage monsters they can safely handle, judged by the monster's REAL damage against the
    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.
  • Watchdog against getting stuck; blocked routes trigger picking a new hunting ground.

Combat and progression

  • Fight with the strongest affordable learned skill of their own class; casters attack from
    range and drink mana potions.
  • Learn skills gated by the game's real requirements (total energy, leadership, character
    level) — at generation and again on every level-up.
  • Keep their class buffs up automatically (elf Greater Defense/Greater Damage, Rage Fighter
    Increase Block, Soul Master Soul Barrier, …) and cast the class heal before drinking potions.
  • Invest level-up stat points according to per-class builds modeled on real players: on reset
    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.
  • On servers with the reset feature, bots reset like players: same level/point/stat rules from
    the reset configuration, seeded populations start with 0..ResetLimit resets, and hunting
    decisions use the reset-effective level.
  • Evolve into their second-generation class at level 200 (Dark Knight → Blade Knight,
    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.
  • Take the third-generation step at the game's maximum level like a player finishing the master
    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.
  • Generated levels range from 10 to 250 with a population-pyramid distribution.

Items and economy

  • Evaluate dropped gear before picking it up: only class-appropriate upgrades and valuables
    (jewels, excellent/ancient items) are collected.
  • Equip upgrades through the regular move-item action (all usual requirements enforced): the
    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.
  • Go shopping in town when the backpack fills up or potions run low: sell junk loot for Zen,
    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.
  • Keep healing/mana potions and arrows topped up.
  • Spend looted Jewels of Bless/Soul/Life on their own equipment after a shopping trip, through
    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.
  • Earn wings at the classic level milestones (wings don't drop, so they are created and put
    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

  • Follow a human party leader into Blood Castle, Devil Square and Chaos Castle: when a real
    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

  • Hunt in parties of 2–5 level-wise similar bots: the leader (the lowest-level member, so the
    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.
  • Fight back when a player attacks them (recent-aggressor memory), instead of placidly farming
    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.
  • Accept party invitations from players (the AutoAcceptAnyone MU Helper flag): after a
    human-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).
  • Presence rotation over the day: fewest bots online in the early morning, most in the evening,
    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.
  • Human rhythm: a randomized reaction delay before engaging a fresh target, straight-line
    approach instead of zig-zagging, randomized AI tick phases, target choice randomized among
    the nearest candidates to avoid dogpiling.
  • Exempt from the offline PC-Cafe Zen fee, and excluded from account class-unlocks (which a
    concurrently-played account would otherwise corrupt — see known issues).

Validated at scale

  • 250 concurrent bots: ~34 % of one CPU core, ~760 MB RAM, zero deaths in steady state, zero
    persistence conflicts.
  • 1100 concurrent bots (220 accounts × 5): ~1.7 CPU cores, ~1.2 GiB RAM, zero deaths, zero
    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

Issue Impact Fixable?
Race in MagicEffectsList (unsynchronized SortedList): the effect-expiry timer vs. applying a buff ~1 caught exception/min at 250 bots; the bot skips one buff tick Yes, but in the core engine (a lock in MagicEffectsList) — maintainer territory; also affects heavily-buffing human players
Race in ComposableAttribute (the attribute system is not thread-safe) mitigated on the bot side: every bot-initiated mutation (equipping, jewels, ammo refill, reset, master points) is queued into the AI tick, after a live incident proved the race can corrupt a bot's attribute graph permanently (error cascade until relog) Remaining surface (engine's effect-expiry timers) is core-engine territory; a bot relog heals an affected instance
Master skill picks include weapon strengtheners of unused weapon types once the useful picks are exhausted; skills whose learning costs 10 banked points (binary rank unlocks) are never learned, because points are always invested as they arrive cosmetic — a few late master points land in less useful passives Yes — match strengtheners to the equipped weapon group, and hold points back when only a banked unlock remains
Summoner enemy debuffs (Sleep/Weakness/Innovation) unused minor AI gap; deliberately excluded (the auto-buff rotation would cast Sleep on the bot itself) Yes — needs a cast-on-enemy path in the combat handler
No quests; bots never enter events on their own (only behind a human party leader with their own ticket); no player trading design scope Quests are bypassed where they matter (class change); self-organized event runs would need ticket farming and are deliberately out, as is trading with players (abuse risk)
Performance headroom items (seek radius, one shared scheduler instead of per-bot timers, warp-table cache) none — re-measured at 1100 concurrent bots: ~1.7 cores, ~1.2 GiB, no starvation Yes, easily — worth revisiting in the multi-thousand range
Adding a game server to a running deployment does not spread the bots onto it before a restart none in practice (an admin restarts after adding a server anyway) Deliberate: handing a bot over between two running servers would animate one account from two persistence contexts, which corrupts the character

Enabling on a server

  1. Enable the Bots feature plugin and set NumberOfAccounts (each account carries up to
    MaxCharactersPerAccount characters; 50 × 5 = 250 bots).
  2. Check that the population fits: the bots of a game server may occupy BotCapacityPercent of its
    maximum 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.
  3. Restart the server. The population is generated on first start and reloaded afterwards.
  4. To rebuild the population from scratch, set ResetBots — it purges, regenerates and clears
    itself.

nolt and others added 21 commits June 27, 2026 23:08
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.
nolt added 2 commits July 8, 2026 20:19
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.
@sven-n

sven-n commented Jul 8, 2026

Copy link
Copy Markdown
Member

That's crazy, I'll try it when I can 😅

nolt added 3 commits July 8, 2026 20:54
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.
@nolt

nolt commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

That's crazy, I'll try it when I can 😅

Yea 🙃
I've been working on this for over two weeks. But I think it's mature enough to share with everyone.

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.
Next on the list is enabling bots to use jewels – or at least the basic ones like Bless, Soul, and Life.

@nolt nolt changed the title [Feature[ OpenMU Bots [Feature] OpenMU Bots Jul 8, 2026
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.
@nolt

nolt commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

The Codacy findings

I went through all 19 of them. Two were real and are fixed (an unused local and an unused parameter,
both in tests). The other 17 do not survive a look at the code, so I left them alone rather than
degrade it:

  • "Remove this unnecessary using" (3×, System.Collections.Concurrent): the build fails without
    them — CS0246: The type or namespace name 'ConcurrentDictionary<,>' could not be found. I verified
    it by removing one and compiling.
  • "Remove this parameter, whose value is ignored" (2×, BotGenerator): level and resetMeta are
    both used — inside a local function, which the analyzer does not look into.
  • "Remove unassigned auto-property HeuristicEstimateMultiplier": the path finder assigns it right
    before the search (PathFinder.cs:125); that is exactly what the property is for, and every other
    IHeuristic implementation in the engine looks the same.
  • "Change this constant to a static read-only property": the constant is used as an argument of a
    [Range(1, …)] attribute, and attribute arguments must be compile-time constants.
  • "Consider making this method a property" (2×, GetEffectiveCharactersPerAccount and
    GetEffectiveBotCapacityPercent): they are methods on purpose — a get-only property would end up in
    the plugin's serialized configuration JSON. Both carry a comment saying so.
  • "Remove this commented out code" (5×): prose comments which happen to end in a semicolon or
    contain parentheses. Rewriting them to please the heuristic would only make them worse.
  • "Add the missing else clause" (3×): style only; the branches are exhaustive as they are.

So the gate stays red on those 17 no matter what I do — if you agree with the reasoning, they are
candidates for ignoring in Codacy.

nolt added 3 commits July 14, 2026 13:42
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.
@nolt

nolt commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

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.
nolt added 3 commits July 14, 2026 19:16
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.
@nolt

nolt commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

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
with 1000 bots spread over the default seed's three game servers, running at
5000x experience so a bot reaches the master class within minutes. The test suite
is at 582/582.

1. A mastered bot earned no master experience at all (48869d6c4)

The worst of the three, and invisible without a live run: a bot that reached level
400 and evolved into its master class gained zero master experience — for as
long as it played.

Master experience is only granted for monsters of at least
MinimumMonsterLevelForMasterExperience (95 in the default configuration), and a
character at the maximum level earns no regular experience either. A bot, however,
hunts the strongest monster it considers safe, and with the gear it collects
from drops that lands it around monster level 80-90 — just below the line. It
fought forever and was paid nothing.

Mastered bots now choose their map and hunting ground among the monsters which
actually pay them, and take the weakest ones above the line rather than the
strongest: master experience hardly grows with the monster's level, so the
cheapest kill above it is the best one. Monsters of level 95+ carry 40.000+
health, which is out of reach of the regular hit budget (100 net hits) for a bot
in modest gear, so that budget is tripled for those monsters only — a long fight
it survives beats a quick one worth nothing. Survivability is deliberately not
relaxed: a monster whose hits the bot cannot take is still refused.

Live, with one bot of every master class promoted to level 400: before the fix,
master experience stayed at 0 for an hour; after it, all seven reached master
level 6-17 within half an hour and spent their master points as they earned them.

2. Points a bot was owed sat unspent (be6f6f681)

Level-up points were only invested on a level-up event, so a bot which received
points while it was not playing — a freshly generated one, or one whose level-up
handler threw — carried them around until its next level-up and fought with the
strength of a much weaker character until then. A bot which holds points now
spends them when it enters the world.

Live: seven bots promoted with 750-1060 owed points each spent all of them
seconds after logging in, instead of idling with an empty budget.

3. Master points went into PvP-only bonuses (4bad38676)

Once the rank-1 root skill was full, bots reliably bought "PvP Defence Rate" —
a bonus that only ever applies against another player. A bot spends its life
hunting monsters and may not even attack a player unless attacked first (its PvP
rules see to that), so the points were wasted. These bonuses now get filled last,
exactly like the bonuses of a weapon type the bot does not carry.

Live: the same situation now yields "Attack Succ Rate Inc" instead.

Known limitation this exposed

Bots never buy equipment — they wear what drops for them, so their gear lags
behind their level and a bot at the maximum level is weaker than a player of the
same level would be. That is the reason a mastered bot needs the stretched hit
budget to reach the monsters which pay master experience at all. Letting bots
spend their money on gear would close that loop (they earn plenty of it); it is
noted in docs/Bots.md under the known limitations rather than fixed here.

A wrapped line started with a hyphen, which markdownlint reads as a list item
without the surrounding blank lines.
@sven-n
sven-n marked this pull request as ready for review July 14, 2026 18:33
@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, 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

  • Server-side AI Bots: Introduced persistent, autonomous AI bots that populate the server, hunt, level up, and manage their own gear and economy, functioning like real players.
  • Bot Intelligence and Navigation: Implemented a dual-tick system: the existing MU Helper AI for combat and a new bot navigator for intelligent hunting ground selection, warping, and shopping.
  • Economic and Progression Logic: Added logic for bots to evaluate gear upgrades, spend jewels on equipment, and perform resets, ensuring they progress autonomously without disrupting the player economy.
  • Social and Event Integration: Enabled bots to form parties, accept invitations from players, and follow human party leaders into mini-game events like Blood Castle and Chaos Castle.
  • Server-side Scaling: Implemented a partitioning system to distribute the bot population proportionally across game servers based on capacity, ensuring bots do not displace real players.
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 Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/GameLogic/Bots/BotGenerator.cs Outdated
Comment thread src/GameLogic/Bots/BotFeaturePlugIn.cs
Comment thread src/GameLogic/Bots/BotFeaturePlugIn.cs
Comment thread src/GameLogic/Bots/BotNavigator.cs Outdated
nolt added 3 commits July 15, 2026 07:38
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.
@jdaviderb

Copy link
Copy Markdown

@nolt man this is insane, good job very hard to review this I will try to review this feature tomorrow locally

@sven-n

sven-n commented Jul 15, 2026

Copy link
Copy Markdown
Member

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 — Bots.md and the inline "why" comments make the intent easy to follow, and almost all new behavior is cleanly gated so a plain human offline session is unaffected. Nice work.

Strengths worth calling out

  • Non-invasive gating. Every new behavior hangs off Account.IsBot or a new IMuHelperSettings flag that defaults to false (AutoSelectBestSkill, AutoSelectBuffs, UseManaPotion, OnlyHuntSafeMonsters, PickUpgradeItems), and MuHelperSettings implements them all as false. A player's offline session keeps its exact prior behavior.
  • Concurrency discipline. The Interlocked startup/maintenance latches in BotFeaturePlugIn.ServerState, the "only the maintenance pass may restart a bot" rule, and routing all bot-initiated character mutations through OfflinePlayer.PendingBotActions (drained at the top of the AI tick so nothing mutates the skill list while combat enumerates it) are all the right calls.
  • No cross-context sharing. BotManager loads each account in the bot's own persistence context and keys active bots by login/slot, sidestepping the /offlevel handover corruption class. RestartBotAsync's failure path re-adds the old instance rather than risk two contexts saving one character.
  • Refactor over copy. Extracting the two-handed/shield conflict check out of MoveItemAction into ItemExtensions.ConflictsWithEquippedHands is behavior-preserving (verified ShieldItemGroup == 6, matching the old literal) and adds a null-guard the original lacked.
  • Migration is correct (IsBot non-null, defaultValue: false), snapshot updated, and there's real test coverage.

Should fix before merge

  • Duplicate <summary> tag → CS1571. src/GameLogic/Bots/BotFeaturePlugIn.cs: PersistConfigurationAsync (~line 472/476) has two consecutive <summary> blocks — a copy of the ServerState summary is left dangling above the method's own. This raises CS1571 ("XML comment has a duplicate tag"), which with doc generation/StyleCop can fail the build. Delete the stray first block.

Worth a second look

  • ServerState.StartupState should be an enum instead of a raw int (0/1/2). _startupState is a genuine three-state machine — the code even needs a doc comment to explain that 0 = not run, 1 = in progress, 2 = done. An enum like StartupPhase { NotStarted, InProgress, Done } would make the transitions self-documenting and remove the magic numbers at the ExecuteTaskAsync call sites. Caveat: the field is mutated with Interlocked.CompareExchange/Exchange, which have no overload for arbitrary enum types, so the field must remain an int at the CLR level — keep an int backing field and cast the enum at the call sites ((int)StartupPhase.InProgress), or use Unsafe.As. _maintenanceRunning (0/1) is really a boolean interlocked lock flag rather than a multi-state value, so an enum there is more of a symmetry call than a clear win.
  • MonsterStatsCache never evicts and is keyed on entity instances. CombatHandler.MonsterStatsCache is static ConcurrentDictionary<MonsterDefinition, …>. Fine at steady state, but a runtime config reload produces new MonsterDefinition instances and the old keys are retained forever. Low-severity leak; consider keying by monster number or clearing on reload.
  • _lastAggressor / _lastAggressionUtc are unsynchronized. In OfflinePlayer, _revenge and _deathSiteToAvoid are correctly volatile immutable records, but the aggressor pair is a plain field written from the attack path and read from the AI tick. DateTime writes aren't atomic → a torn/stale read is possible. Blast radius is tiny (briefly-wrong self-defense window, every strike re-gated by BotPvpRules), but folding these into the same immutable-record pattern would be more consistent.

Smaller notes / questions

  • Stale "proof of concept" wording in BotConfiguration. The remarks on NumberOfAccounts and ProofOfConceptAccounts read as if generation isn't implemented yet — it is. Worth tidying.
  • OnAiTickFailed latches on == threshold (BotPlayer) — fine given it's a latch the maintenance pass consumes, just flagging the assumption.
  • Layering: EnterMiniGameAction, PartyRequestAction, and MuHelper.PartyRequestHandler now call into Bots.*. Same assembly and no-ops for non-bots, so not blocking — just noting the new coupling from generic action code toward the bot feature.
  • Scaling hotspot (informational): the per-tick work in CombatHandler is the thing to profile at 1000+ bots; you've already cached the expensive bits.

Overall a strong, careful implementation. The duplicate-summary warning is the only true blocker I found; the rest are refinements. 👍

nolt added 6 commits July 16, 2026 07:42
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.
@nolt

nolt commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough read — and for doing it at the level of the changes to
existing code and the lifecycle design, which is exactly where the regression risk
lives. Glad the gating and the "why" comments landed. I've pushed a small follow-up
round; point-by-point below.

Should fix before merge

  • Duplicate <summary> → CS1571. Good catch — that was a stray paste of the
    ServerState summary left dangling above PersistConfigurationAsync (the real one
    is on the class itself). The build stayed green because it's a warning in our
    current configuration rather than an error, but it shouldn't be there. Removed
    (2f3e0b016).

Worth a second look

  • StartupState as an enum. Done (3ffd86aee). Introduced
    StartupPhase { NotStarted, InProgress, Done }; the field stays an int at the CLR
    level and the constants are cast at the call sites, since — as you note — there's no
    Interlocked overload for arbitrary enums. Left _maintenanceRunning as the 0/1
    interlocked flag it is; an enum there would be symmetry for its own sake.

  • MonsterStatsCache eviction. Done (b33d80b20). Re-keyed by monster number
    instead of the MonsterDefinition instance; the cached values are derived purely
    from the monster's stats, so a config reload now reuses the entries rather than
    orphaning them.

  • _lastAggressor / _lastAggressionUtc unsynchronized. Done (520ce4d1f). Folded
    the pair into a single volatile Aggression? record (player + timestamp), matching
    the _revenge / _deathSiteToAvoid pattern — the read is now a single reference load
    and can't tear.

Smaller notes

  • Stale "proof of concept" wording in BotConfiguration. Tidied (c139cb48d).
    Dropped the obsolete "next phase" remark on NumberOfAccounts, and re-documented
    ProofOfConceptAccounts (display label now "Extra accounts to animate") for what it
    actually is now: an optional hook to animate existing accounts alongside the
    generated, capacity-limited population. Kept the property name to avoid churning the
    serialized config.

  • OnAiTickFailed latching on == threshold. Intended, and I've made it explicit
    in the comment (1686b529d): it arms the restart exactly once at the crossing tick;
    further failures keep incrementing the counter without re-firing, and the maintenance
    pass consumes the flag and resets the counter. Kept the ==.

  • Layering / coupling. This is the one I've left for your call. The generic action
    code (EnterMiniGameAction, PartyRequestAction, PartyRequestHandler) reaching
    into Bots.* is same-assembly and a no-op for non-bots today, so it's not breaking
    anything — but if you'd prefer the dependency inverted (a small bot-hook interface the
    feature registers, rather than the actions knowing about bots), I'm happy to do it in
    a follow-up. Just say which way you'd like the seam to face.

  • Scaling hotspot. Agreed the per-tick CombatHandler work is the thing to profile
    at 1000+; the target-scan and stats are cached, and that's where I'd look first if it
    ever shows up in a profile.

All six edits build clean (0 errors) and the suite is green (582/582). Left the layering
inversion out pending your preference on the seam.

@sven-n

sven-n commented Jul 16, 2026

Copy link
Copy Markdown
Member

Thanks for this great PR, looks like our AI Agents came to a conclusion 😄

@sven-n
sven-n merged commit b10de06 into MUnique:master Jul 16, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants