Skip to content

feat(bots): let the bots talk – template chat, party search, bot-to-bot conversations - #1438

Merged
sven-n merged 44 commits into
MUnique:masterfrom
nolt:bot-learn-speaking
Nov 14, 2026
Merged

sven-n merged 44 commits into
MUnique:masterfrom
nolt:bot-learn-speaking

Conversation

@nolt

@nolt nolt commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • A new Bot chat feature plugin (off by default) lets the server-side bots talk – with
    configurable lines, no language model: a few hundred bots must not cost more than a few string
    replacements. The lines are localized texts; every player reads them in their own language.
  • Bots announce events they could actually join with /post and then wait at the event's NPC
    to be invited, answer whispers by keywords, greet, congratulate ("gz"), comment on being killed,
    chat in the party, and talk to each other near a player like the townsfolk of an RPG.
  • A player can search party members among the bots the way players ask each other:
    /post party 300-350 bk dw – the bots whisper back, the player names the meeting place
    (devias 215 40, a warp like devias3 or losttower7, or here), and the bot comes there to be invited.
  • The admin panel gets two small, generic improvements: translation fields for content
    languages
    the panel itself isn't translated to (e.g. Polish), and lists which start
    collapsed
    on request.

How to talk to the bots

📖 Full description: docs › Server-side AI bots › Talking
(part of this PR).

Enable Bot chat under Feature plugins (next to Bots). Then, as a player:

You do The bot does
Whisper to a bot: hi, how are you?, what level are you?, where are you?, party?, bye whispers back after a few seconds of "typing", matching the keywords, in the language you wrote in
Say its name in the normal chat, or write in the party chat (~) while it's in your party answers in the same chat
/post party 300-350 bk dw (also 300+, min 300, pt, grupa, …) or an event: /post party bc7, pt ds 5 up to 5 fitting bots whisper an offer – for an event only bots which may enter it; the same post again calls 5 others
Whisper back to one of them: devias 215 40, devias3 227 227, losttower7, lorencia, or here it comes to that place through the warp closest to it and waits 5 minutes for your invitation
Stand near two bots in town or while hunting they talk to each other now and then; write something within 30 s and one answers
Level up / reset next to a bot, or drop an excellent item "gz" / "nice drop!"
Put on wings next to a bot, or whisper it what wings do you have? "nice wings!" / it names its wings ("Wings of Soul +9")
Ask a bot for a trade or challenge it to a duel it turns it down, with a reason
GM: /botsay <bot> <text> the bot says the text

Polish players: set the account's Language Iso Code to pl; to edit the Polish texts in the
panel, set AdminPanel__ContentLanguages__0=pl (see Defaults).

Commits

  1. feat(bots): let bots talk with configurable lines – the plugin, posts about events/invasions, level milestones
  2. feat(bots): wait for players after announcing an event
  3. feat(bots): answer whispers by their keywords
  4. feat(bots): greet players entering the game
  5. feat(bots): talk in the party chat of a player
  6. feat(bots): comment on being killed by a player
  7. feat(bots): answer when mentioned in the normal chat
  8. feat(bots): make small talk in town
  9. feat(bots): ship the default chat lines in Polish too
  10. refactor(bots): give every chat situation its own list
  11. feat(bots): tell where the bot stands with {location}
  12. feat(bots): answer a player's party search posted with /post
  13. feat(bots): let two bots talk to each other near a player
  14. feat(bots): turn the lone small talk into merchant adverts
  15. feat(bots): congratulate players on level-ups and resets
  16. feat(bots): take the time to type an answer
  17. feat(bots): don't give a player the same answer twice in a row
  18. feat(bots): fit lines and conversations to the time of day
  19. feat(bots): say why a trade or duel is turned down
  20. feat(bots): keep quiet while the players around are talking
  21. feat(admin-panel): edit translations in the server's content languages
  22. feat(admin-panel): start the lists of the bot chat collapsed
  23. feat(bots): answer in the party chat
  24. feat(bots): understand more ways of saying goodbye
  25. feat(bots): give the bots a richer default vocabulary
  26. feat(bots): answer in the language of the player's message
  27. feat(bots): make gestures with the lines
  28. feat(bots): let a conversation have a third line and players chime in
  29. feat(bots): remind of an opened event in town
  30. feat(bots): react to rare drops and notable monsters nearby
  31. feat(bots): remember the players a bot was in a party with
  32. feat(bots): let a game master make a bot talk with /botsay
  33. feat(bots): comment on wings
  34. fix(bots): come to a meeting place through the warp which leads there
  35. feat(bots): understand an event in a party search
  36. docs(bots): wrap the party search paragraph
  37. fix(bots): forget bot conversations nobody chimed in to
  38. fix(bots): forget expired chat occasions
  39. fix(bots): reserve a bot for one party search at a time
  40. fix(bots): keep the party offer when the meeting place is out of reach
  41. fix(bots): walk to a meeting place on the map the bot stands on
  42. fix(bots): say a delayed party line only to the party it was meant for
  43. fix(bots): drop delayed bot chat once the plugin is deactivated
  44. docs(bots): describe naming another meeting place and walking on the same map

Each commit message explains its change; the sections below give the overview.

The plugin

BotChatPlugIn is a feature plugin next to the Bots plugin, disabled by default
(IDisabledByDefault). Its configuration (BotChatConfiguration) has the chances, cooldowns and
intervals at the top and one list per situation below: event lines, invasion lines, level
milestones, whisper replies (with keywords), greetings, party lines, congratulations, refusals,
party search whispers, class words, death comments, merchant adverts and conversations.
A list without lines keeps the bots silent in that situation.

Lines are LocalizedStrings, rendered per receiver in their culture (like the server
announcements), with placeholders: {event}, {map}, {location} (map + coordinates, e.g.
"Devias 215, 40"), {level}, {class}, {player}, {resets}, {partner}, {wings} (e.g.
"Wings of Soul +9"; a bot without wings answers such a reply with "no wings yet, saving up"). Unknown placeholders
stay visible, so a typo in a configured line shows in the game. Bot messages bypass the
ChatMessageAction: they reach the human receivers only, never Discord or the chat bridge.

What the bots say, and when

Situation Channel Who
BC/DS/CC entrance opened /post, then waits at the entry NPC (CC: in the home town's safezone) until the entrance closes a bot which meets the entry requirements (BotMiniGameHandler.IsEligible); not when less than 2 minutes remain
Invasion started /post a bot which may warp to the invaded map
Player's party search (/post party 300+ bk) whisper; then meeting at the named place up to 5 free bots whose level and class fit; the same post again calls others
Whisper / name mentioned in the normal chat / party chat (~) whisper / normal chat / party chat, in the language of the message the bot; best keyword match, fallback without keywords (party chat without a name: keyword matches only)
Player entered the game normal chat the nearest bot which sees the player
Player leveled up or reset normal chat ("gz") the nearest bot
Bot joined a player's party / event finished party chat the bot / one bot per party
Killed by a player (again) normal chat the bot
Trade request / duel challenge whisper; the duel is refused like a click on "no" the bot
Two bots near a player normal chat: opener + reply (+ riposte) two bots close together, both in town or both hunting; a player who chimes in within 30 s gets an answer
Event entrance opened normal chat in town ("bc open, anyone going?") an eligible bot, once per town where a player stands
Rare drop / notable monster nearby normal chat ("nice drop!", "Golden Goblin here!") the nearest bot
Bot earned its wings at a level milestone normal chat ("finally, my wings!") the bot, while a player sees it
Player put on wings normal chat ("nice wings!") the nearest bot; once per player and pair until restart
Old party companion enters / invites again normal / party chat ("together again?") the bot which remembers them
/botsay <bot> <text> normal chat a GM makes a bot talk
Merchant adverts normal chat off by default – used by the Bot merchants plugin (PR 1455): there, only bots behind their open store advertise, on by default

Lines can come with a gesture – the client's emote animations (wave, clap, cheer, cry, ...),
performed facing the player; the defaults have fitting ones.

Keeping it believable and quiet

  • Server-wide gap between posts, each event/invasion announced once, per-bot cooldowns.
  • Conversations at most every 5 minutes per player, and only when a human hears them; drawn
    without repetition; filtered by place (town/hunting/anywhere) and time of day.
  • Answers take typing time (reaction + per character, capped at 12 s).
  • A player doesn't get the same answer twice in a row.
  • Bots say nothing unasked while the players around them are busy talking.
  • Both post chances are 30 % by default, so not every opening gets a post; bots post only while /post is enabled.
  • Bots never announce or meet on maps forbidden for bots (PR 1419), and a bot on an errand
    (shop, buff, chaos machine trip, revenge) or in a player's party doesn't volunteer.

New plugin points and chat command

  • /botsay <bot> <text> (GM) – a bot says the text as typed; only bots.
  • IBotPartyJoinedPlugIn – a bot accepted a player's party invitation.
  • IBotWingsEarnedPlugIn – a bot put on the wings it earned at a level milestone (BotWingHandler).
  • IPostMessageSentPlugIn – a player posted with /post. Posts go out as gens chat, so the
    existing chat-sent plugin point can't tell them from the gens chat.
  • ITradeRequestedPlugIn – before a trade request is checked: a request to a bot can't be
    delivered (no trade window), so it's reported early.
  • IDuelRequestedPlugIn – after a duel challenge was handed to the challenged player. A bot never
    answered one, which left the challenger waiting; the bot chat now refuses it.

Admin panel

  • Content languages. The editor of a LocalizedString showed fields only for the languages of
    the panel's own UI (en, de, zh-CN). It now also shows every language a text already has a
    translation in, plus the languages of the new AdminPanel:ContentLanguages setting
    (e.g. AdminPanel__ContentLanguages__0=pl), so a server for Polish players can edit Polish texts.
  • Collapsed lists. [CollapsedByDefault] makes a collection start collapsed whatever its size
    (otherwise only lists of more than ten items do). The bot chat lists use it.
  • The Docker images (Startup, GameServer.Host, AdminPanel.Host) install icu-data-full:
    with the English-only culture data of Alpine's icu-libs, the server doesn't know a culture like
    pl, and a player whose account language is pl gets the English texts.

Defaults

English lines with a Polish translation (the keywords understand Polish with and without
diacritics); 194 conversations, 24 of them with a riposte. For a Polish player to see them in Polish: the account's
Language Iso Code is pl, the image has the full ICU data, and – to edit them – the content
language setting.

Testing

  • Unit tests for the composer (keywords, placeholders, places, party search parsing, time of day,
    typing time, repetition), the throttle (posts, occasions, cooldowns, conversation interval and
    draw, reset detection, busy chat, party volunteers), the meeting planner and the greeter.
  • Full solution test run on bot-behavior-update1 (PR 1419) + these commits: all green
    (MUnique.OpenMU.Tests 2822 passed, Persistence.Initialization 808, Web 572, the rest green), no new
    analyzer warnings. After the rebase onto the current master (00f7373): all green –
    MUnique.OpenMU.Tests 2957 passed / 2 skipped, Persistence.Initialization 838 / 12 skipped, Web 661,
    the rest green.
    After the rebase onto master 3738d38: MUnique.OpenMU.Tests 2986 passed / 2 skipped,
    Persistence.Initialization 851 / 12 skipped, Web 666, the rest green.
  • In game, on a lab server with 100 bots: a bot posted about a forced Blood Castle and waited at the
    Messenger of Archangel; whisper replies by keywords ("how are you?" → "fine, thanks for asking").
  • In game, on the same lab with 200+ bots (levels 10–296) spread over three game servers, with an
    account whose language is pl: Polish lines and Polish fields in the panel; party search with
    /post party 300+ bk (whispered offers, the next post calls other bots, the bot came to the named
    place and waited); "gz" on a level-up; trade and duel refusals; silence while players were
    chatting; party chat answers and goodbyes; replies in the message's language; gestures;
    conversations with a riposte and a player chiming in; the event reminder in town; rare drop and
    golden monster reactions; greeting of an old party companion; /botsay.
  • Wings (last commit): unit tests for the item name, the once-per-pair memory and the default
    replies; bot tests 338 passed, 9 failed - the Chaos Machine tests which fail on master too
    (fixed on master since). In game, on a live server: a bot next to the player
    congratulated on the wings the player put on.
  • Meeting place through a warp (commit 34): unit tests for reading devias3 227 227 / losttower7,
    choosing the floor's warp of the Lost Tower, the level of that warp, a town gate with blocked
    tiles, and the warp to another floor of the same map; bot tests 348 passed, 9 failed - the Chaos
    Machine tests (fixed on master since). A check on the Season 6 data showed which maps have parts that can't be
    walked between: the 7 floors of the Lost Tower, the 3 parts of the Dungeon, and Kanturu Ruins 3.
    In game, on a live server: whispering a warp as the meeting place (e.g. devias3 227 227) brought
    the bot there through that warp.
  • Event in a party search (commit 35): unit tests for bc7, bc 8, blood castle 7, ds5, cc3 350+,
    an unknown event level (bc9) and a word which only ends in an event word (abc7). Full run on D:
    MUnique.OpenMU.Tests 2967 passed / 2 skipped, Persistence.Initialization 838 / 12 skipped, Web 661;
    no new analyzer warnings. Not tested in game.
  • Review fixes (commits 37–44): unit tests for pruning conversations nobody chimed in to and expired
    occasions, one reservation of a bot by 8 concurrent party searches, keeping the offer after an
    unreachable place (and ending it with a reachable one), walking to coordinates on a map without a
    warp entry, and a delayed party line which doesn't reach another party. Full run: MUnique.OpenMU.Tests 2992 passed / 2 skipped,
    Persistence.Initialization 851 / 12 skipped, Web 666, the rest green; no new analyzer warnings.
    Not tested in game.
  • Not tested: Chaos Castle meeting and invasions in game; the Dapr images.

@codacy-production

codacy-production Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 825 complexity · 12 duplication

Metric Results
Complexity 825
Duplication 12

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@nolt
nolt force-pushed the bot-learn-speaking branch 3 times, most recently from 8b262dc to aa7c923 Compare November 8, 2026 11:57
@nolt
nolt marked this pull request as ready for review November 8, 2026 12:04
nolt added 24 commits November 13, 2026 08:39
Adds the "Bot chat" plugin (disabled by default). Bots announce opened
Blood Castle, Devil Square and Chaos Castle entrances and started
invasions with /post, comment on level milestones in the normal chat
and answer whispers of players - with lines from the plugin
configuration, no language model.

A bot only posts about what it could do: an event only when it meets
the entry requirements (BotMiniGameHandler.IsEligible), an invasion
only when it may warp to the invaded map. A server-wide gap between
posts, one announcement per event opening and per-bot cooldowns keep a
large population from flooding the chat. Both post chances default to
0, and bots post only while the /post command is enabled.

Lines are localized strings rendered per receiver, like the server
announcements. The /post format is shared with PostChatCommandPlugIn.
A bot which posts "Blood Castle open, anyone going?" and keeps hunting
in another corner of the world can't be found. After its post it now
goes to a meeting point and waits there until the entrance closes:
next to the Messenger of Archangel (Blood Castle) or Charon (Devil
Square), found in the game data, or - since Chaos Castle is entered
with the ticket alone - in the safezone of its home town, which the
post names ({map}).

Waiting does not make the bot decline party invitations; a player who
invites it and enters takes it along like any party bot. The meeting
ends when the entrance closes, when a player takes the bot into a party
or when there is no route to the NPC.

A bot does not announce an event whose entrance closes in less than two
minutes, and a bot on an errand, on a revenge or in a party with a
player does not look for company.
A whisper reply line can now carry keywords - comma-separated words or
phrases it answers, e.g. "how are you, how r u". The bot answers with a
line whose keyword matches the whisper (whole words, ignoring case and
punctuation, the longest match winning), and falls back to the replies
without keywords when nothing matches.

The default lines cover greetings, "how are you", the bot's level and
map, party, thanks and goodbye. The bare "?" reply is replaced by
"hi, what's up?".
When a player enters the game from the character selection (not after
a warp), the nearest bot which sees the player greets them in the
normal chat, e.g. "hi {player}, have a nice game". Without a bot in
sight nobody greets.

A player is greeted at most once per GreetingCooldown (30 minutes), so
logging out and in again doesn't start a round of greetings; the
chance is GreetingChancePercent (50 %), and the bot's own chat
cooldown applies.
A bot which accepts a player's party invitation thanks for it in the
party chat ("thx for invite"), and after an event the bot went through
with a player's party, one bot of that party says "gg". The chance is
PartyChatChancePercent (70 %); the bot's chat cooldown applies.

The accepted invitation is reported through the new
IBotPartyJoinedPlugIn plugin point, called by BotPartyHandler.
A bot killed by a player outside of an event says something about it in
the normal chat while it still lies there ("why??", "wtf {player}").
When the same player kills it again within the revenge memory - the
point at which the bot gives up its revenge - it says a resigned line
instead ("you again...").

The chance is KilledByPlayerChancePercent (70 %); the bot's chat
cooldown applies. OfflinePlayer exposes the recent death count per
killer for this.
When a player writes the name of a bot which sees them in the normal
chat ("Serael, how are you?"), the bot answers in the normal chat with
the same lines and keywords as a whisper reply. The name must be a
whole word; the whisper reply chance and cooldown apply.
Every SmallTalkInterval (10 minutes) per game server, with
SmallTalkChancePercent (50 %), a bot standing in a safezone says
something in the normal chat ("lf party {map}", "lag?"). Only a bot
which a player sees talks - talking to nobody would be wasted.

The lines are kept neutral on purpose: bots don't trade with players,
so they don't offer anything for sale. A forced start of the plugin
from the admin panel makes the small talk due at once.
Every default bot chat line now carries a Polish translation, so Polish
players see the bots talk in Polish without the server owner writing
the translations first. Event, map and item names stay English, as the
game data names them. The default reply keywords also understand
Polish whispers, with and without diacritics ("jak się masz",
"jak sie masz").
The bot chat configuration had one long "Lines" list in which every line named
its situation. With some fifty lines, the admin panel showed one endless page,
and a line could end up in the wrong situation or carry keywords which only
whisper replies use.

Every situation now has its own list (event lines, greetings, whisper replies,
...), each collapsible on its own in the plugin's configuration dialog. The
lines only hold their text; the whisper replies keep their keywords. The
situation enum is gone - the list a line is in says when it is said.
{map} names the map only, which is little help on a big map: a player asking
a bot "where are you?" still has to search for it. The new {location}
placeholder adds the bot's coordinates the way players write them, e.g.
"Devias 215, 40", so the mini map leads the way. The default "where are you"
replies use it.

In an event post, {location} is the meeting point next to the entry NPC; in an
invasion post, the invaded map alone.
A player looking for party members can now ask the bots the way players ask
each other: "/post party 300-350 bk dw". A post with a party word and a level
(a range, "300+", "min 300" or a single number) calls up to five free bots of
the game server whose level fits; class words narrow it down, each meaning its
whole class line (bk = Dark Knight, Blade Knight, Blade Master). The bots
whisper to the player a few seconds apart, and the same search posted again
calls five others.

The player whispers back to a bot where to meet - "devias 215 40", a map name
alone, or "here" - and the bot warps there, walks up and waits for the
invitation, using the meeting of the event announcements. A map the bot may
not go to it declines; a whisper without a place gets asked again. Map names
are recognized in every translation.

The /post command had no hook, and its messages go out as gens chat, so they
can't be told apart from the gens chat in the chat-sent plugin point. A new
plugin point reports the posts alone.
Up to now a bot only said a line on its own. Now two bots standing close
together talk like the townsfolk of a role-playing game: one says the opener
("my potions are running low"), the other answers a few seconds later ("the
shop here sells them cheap").

They only talk when a human player stands near them - nobody else would hear
it - and each player overhears about one conversation every five minutes; the
interval counts per player, so a quiet corner doesn't wait for a busy town.
Each conversation has a place: in a town (both bots in a safezone), out
hunting (both outside of it, usually fighting) or anywhere. Conversations are
drawn without repetition until all of a place were used.

The default configuration comes with seventy-three conversations, each with a
Polish translation; they live in a file of their own.
With the conversations, a bot no longer needs to talk to itself in town: "lf
party", "lag?" and the like are now said by two bots to each other. The lone
line of a bot standing in a safezone gets a new purpose instead - adverts like
those of a merchant ("selling potions, cheap! Lorencia 130, 125"), as the
groundwork for a merchant bot.

The adverts are off by default (chance 0) until such a bot exists; the
settings and the list are renamed after them.
Few things make an online world feel as alive as a stranger's "gz". When a
player levels up or makes a reset, the nearest bot which sees them now and then
congratulates them in the normal chat ("gz", "nice, 400!", "reset 12, nice
Hero").

The chance is 30 % and each player gets at most one congratulation per ten
minutes for each kind - with a high experience rate a player levels up every
minute. A reset logs the player out or not, depending on the reset
configuration, so it's noticed by the grown reset count when the bots look over
the players every few seconds.
A bot answered every whisper after the same random few seconds, whether it
said "np" or a whole sentence - a pattern a player notices. Now the answer
takes the time a player would need: a moment to read, then a little time per
character, capped at twelve seconds. This applies to whisper and mention
replies, the party chat, the party search whispers and the second line of a
conversation.
A player who whispers "how are you?" twice and gets the very same "fine,
thanks for asking" twice knows they talk to a machine. The bots now remember,
per player and list, the line they said last and pick another one when the
list has another to offer. This covers the lines said to one player: whisper
and mention replies, greetings, congratulations, the comments on being killed
and the party search questions.
A "good morning" at midnight gives a bot away. Every line and conversation now
has a "When": anytime (the default), morning, day, evening or night, by the
local time of the server, and a bot picks only among those which fit the hour.
The defaults add a few: "good morning", "late night grind?", "still up?" -
"the monsters don't sleep, neither do i".

The hours are fixed; they only pick fitting lines, so configurable boundaries
wouldn't be worth the extra settings.
A bot has no trade window and doesn't duel. A trade request to it went nowhere
without a word, and a duel challenge was never answered at all, which left the
challenger waiting. Now the bot refuses the duel like a player clicking "no",
and in both cases it often whispers why ("sry, no trade", "not now, maybe
later").

Two new plugin points report the requests: a trade request before it's
checked (a request to a bot can't be delivered), a duel challenge after it was
handed to the challenged player.
When a few players stand together and chat, a bot chiming in with a
conversation or a "gz" drowns them out. Now a bot says nothing unasked -
conversations, greetings, congratulations, merchant adverts, level and death
comments - while the players near it are busy talking: three normal chat
messages within 30 seconds by default, both configurable. Answers to a player
who talks to the bot stay unaffected.
The editor of a translatable text showed a field for each language of the
admin panel's own user interface only - English, German and Chinese. A server
for Polish players could neither see nor edit the Polish translations: the
Polish default lines of the bot chat were there, but invisible.

The editor now also shows every language a text already has a translation in,
and the additional content languages of the new AdminPanel:ContentLanguages
setting (e.g. AdminPanel__ContentLanguages__0=pl), so a new text gets a field
for them too. The admin panel itself isn't translated by this.

The Docker images install icu-data-full as well: with the English culture
data alone, which icu-libs brings on Alpine, the server doesn't know a culture
like "pl", and a Polish player gets the English texts. This is required for
the bots to answer Polish players in Polish, together with the player's
account language set to "pl".
A list in a configuration form starts collapsed only when it holds more than
ten items. The bot chat configuration has some twenty short lists, so most of
them showed all their cards at once - one endless page instead of a list of
sections to open one by one.

A collection property can now be marked with [CollapsedByDefault] to start
collapsed whatever its size; empty lists still show, so a first item can be
created. The lists of the bot chat configuration use it.
A bot in a player's party ignored the party chat entirely: "~Karanwyn what
level are you?" got no answer, although the same question as a whisper or in
the normal chat did.

The bot whose name a party message mentions now answers it in the party
chat, with the whisper replies. Without a name, one bot of the party answers -
but only a message which matches a keyword, so "ok" or "let's go" don't get a
reply each time. A bot inside an event with the party answers as well.
"muszę kończyć" (I have to go) was answered with the fallback "sorry, busy
hunting": the default goodbye keywords knew only "bye", "nara", "pa" and the
like. They now cover the common English and Polish ways ("gtg", "gotta go",
"spadam", "muszę kończyć" with and without diacritics), with two more fitting
answers.
nolt added 12 commits November 13, 2026 08:39
With three or four lines per situation and some eighty conversations, a
player who spends an evening next to the bots hears them repeat themselves.
The default configuration now has six to eight lines in most lists, 167
conversations (more for the town and for hunting, and more for the morning,
day, evening and night), and the Polish translations read like a players'
chat rather than a translation.

The whisper replies understand more topics: the class, where to hunt, help,
trade, and being asked whether it's a bot - which it answers with a wink, not
a lie.
A bot answered in the language of the player's account, which is English
unless an admin set it: a Polish player who whispered "jak się masz?" got
"fine, thanks for asking". Now an answer to a message - whisper, mention,
party chat, the meeting place of a party search - is in the language the
message is written in, recognized by its typical words and letters. A
message which doesn't tell ("lol") is answered in the player's own language,
as before.

The words and letters per language are a list of the configuration; the
defaults know Polish and English.
A player who greets waves, a player who says "gz" claps - the game client has
these emotes. A line or reply can now come with a gesture: the bot turns to
the player it talks to and performs the client's emote animation, seen by
the players around it. The default lines have fitting ones: a wave with a
greeting and a goodbye, a clap with a congratulation, a cheer after an
event, a victory pose at a level milestone, a raised fist when a player
killed the bot and tears when it happens again.

The gesture values are the client's animation numbers, so they're an enum
rather than free numbers.
A conversation of two bots stopped after the reply, and a player standing
next to it who said something got no reaction - the bots carried on as if
nobody was there.

A conversation may now have a riposte, the first bot's last word ("guess who
failed +10 again" - "you?" - "me. again."); twenty-four such conversations join
the defaults, 194 in all. And a player who writes in the normal chat within
30 seconds after a conversation next to them gets an answer from one of the
two bots: a whisper reply when the message matches a keyword, otherwise one
of the new "player chimes in" lines ("right?", "haha, exactly").
The /post announcement of an event reaches the whole server, so it's rare
(off by default). Now and then, a bot in a town where a player stands also
reminds of an opened Blood Castle, Devil Square or Chaos Castle in the normal
chat - "bc open, anyone going?" - heard only by the players around it. Like
the post, only a bot which meets the event's entry requirements says it, and
each town hears it once per opening; the chance is 50 % by default.
Players around a lucky drop or a golden monster say something - bots didn't.
Now the nearest bot sometimes comments on an excellent or ancient item a
player's kill dropped ("whoa, Excellent Dragon Armor!"), and a bot calls out
a notable monster which appeared near it while a player sees it ("Golden
Goblin here!"). The notable monsters are a configurable list of monster
numbers, by default the golden monsters, the Red Dragon and the White Wizard
of the invasions; an invasion spawning many of them gets one call-out per map
and monster in two minutes.
A bot greeted a player it had spent an hour with in a party like a
stranger. Now the bots remember their party companions (until the server
restarts): when such a player enters the game, an old companion nearby
greets first, with a line of its own ("hey Hero, together again?"), and the
same lines welcome the player who invites the bot again.
For staged scenes of an event - a bot telling of a dragon sighting before
the invasion, a merchant bot advertising a GM's lottery - a game master can
now make a bot say something: "/botsay <bot> <text>". The text is said as
typed in the normal chat, to the players who see the bot. Only bots can be
made to talk, never a player's character.
Wings are the milestone of a MU character, yet the bots let them pass in
silence - their own new pair as well as a player's.

- A bot which put on the wings it earned at a level milestone shows them off
  ("finally, my wings!") while a player sees it. The new plugin point
  IBotWingsEarnedPlugIn reports the grant of BotWingHandler. At a level
  milestone which brings wings, the bot talks about the wings, not the level.
- The nearest bot comments on the wings a player put on ("nice wings!") -
  once per player and pair until the server restarts, since the wings go on
  again after every change of equipment.
- The new placeholder {wings} names the bot's wings with their level ("Wings
  of Soul +9"); the default whisper replies answer "what wings do you have?"
  with it. A bot without wings answers with one of the new answers without
  wings instead ("no wings yet, saving up").

One chance setting (50 %) for both comments; English and Polish default lines.
A player who invited a bot from Devias3 227 227 had it walk all the way from
the town of Devias, and "devias3" alone wasn't understood at all: the bot
only knew the names of the maps, and always entered a map through its spawn
gate. The floors of the Lost Tower and the Dungeon, and Kanturu Ruins 3,
aren't even connected by walkable ground to the spawn gate, so a bot which
was called there gave up on the way.

- The whisper may name a warp of the warp list ("devias3 227 227",
  "losttower7"), in any of its translations. A map and a warp of the same
  name ("Devias") still mean the map.
- With coordinates, the bot enters through the legal warp from which they
  are the shortest walk - the same search the party followers use to reach
  a leader on another floor, now shared. Blocked landing tiles are left out
  there, as the town gates of Lorencia and Devias have some, and a player
  who lands on one is moved to the spawn gate anyway.
- A warp named without coordinates: the bot waits at its gate.
- A bot already on the map, but farther away than the meeting's gate, warps
  through the gate (at most twice) instead of walking.
- The level is checked for the warp which leads there, not for the map: a
  level 60 bot doesn't promise to come to Lost Tower 7.
A party search like "/post party bc7" called no bot: the parser only knew
levels, and "bc7" holds no number of its own. Worse, "party bc 7" was read
as "level 7 and above".

A post may now name a Blood Castle, Devil Square or Chaos Castle with its
level ("bc7", "ds 5", "chaos castle 3"). Only bots which may enter that
event answer - checked by the same rules a bot follows its leader in with:
the event's level bracket (the lower one of the special characters
included) and the master class requirement, as configured for the event.
An event level which doesn't exist calls no bot, and the event's digit is
never taken for a character level. The words for the events are part of
the Bot chat configuration.
The paragraph about events in a party search ran past the line length of the docs (markdownlint MD013).
@nolt
nolt force-pushed the bot-learn-speaking branch from 66f8b0f to 1fe7b43 Compare November 13, 2026 08:14

@sven-n sven-n left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Code review (merge conflicts intentionally excluded; static analysis, tests not independently run).

Medium — Expired conversation entries retain bots indefinitely. src/GameLogic/Bots/BotChatThrottle.cs:401-433: RememberConversation appends to _recentConversations, but expiration cleanup occurs only when a player calls TakeRecentConversation. On quiet servers the list grows and holds strong references. Prune on insertion or periodically; test long-running conversations with no human replies.

Medium — Same-map meetings can be rejected. src/GameLogic/Bots/BotChatPlugIn.cs:727-755: FindMeetingEntry requires legal destination warps before checking whether the bot is already on the map and can walk to the requested location. Handle same-map reachable targets without requiring a warp; test maps with no warp entries.

Medium — Unreachable location discards the pending offer. BotChatPlugIn.cs:1092-1150: RemovePartyOffer(bot) runs before FindMeetingEntry succeeds. A player who supplies an unreachable place cannot correct it in the next whisper. Retain the offer until a valid meeting is accepted or it expires.

Medium — Competing party searches can reserve the same bot. BotChatPlugIn.cs:626-670 and BotChatThrottle.cs:176-221: candidate filtering and SetPartyOffer are not atomic across different requesters. Both players may receive an offer, but only the last is remembered. Introduce an atomic conditional reservation and concurrency test.

Low/medium — Delayed party reply can go to another party. BotChatPlugIn.cs:1155-1184 and BotChatSpeaker.cs:77-94: delayed response resolves bot.Party at send time. Capture and validate the original party/recipient before delivery.

Also consider expiring _occasions entries and cancellation of fire-and-forget RunLater actions on shutdown/config disable. The template-driven design and existing GM command permission check are positives.

@nolt

nolt commented Nov 13, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review! All points are addressed in separate commits, each with a unit test:

  • Expired conversations – RememberConversation now prunes expired entries on insertion as well (9c9b218).
  • Same-map meetings – a bot already standing on the map walks to the named coordinates without a warp, also on maps without a warp list entry. A map forbidden for bots stays refused (bd42958).
  • Unreachable location – the offer is kept until a reachable place is accepted or the offer expires, so the player can name another place (b15ff0a).
  • Competing party searches – the reservation is now an atomic TryReservePartyOffer under a lock. A bot taken meanwhile is skipped. Covered by a concurrency test with 8 requesters (abab816).
  • Delayed party reply – the party is captured when the bot decides to answer, and the line is dropped if the bot isn't in that party anymore (8aaf769).
  • _occasions – expired entries are removed every 10 minutes (78b4a29).
  • RunLater – a delayed action is dropped once the plugin is deactivated (729ad62). I didn't add full cancellation on shutdown: the actions are a few seconds long, check the bot's state before speaking, and log failures.

Full test run is green.

@sven-n sven-n left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM. The bot chat is a feature plugin which is off by default (IDisabledByDefault), so existing servers are unaffected. The hooks into the core logic (trade and duel requests, /post, party join, wings) are only new plugin points. The texts the players see are localized resources, and the delayed lines are dropped as soon as the plugin is deactivated. The throttle state prunes expired occasions and conversations.

I built it and ran the tests locally on top of current master: everything passes, including all new bot chat tests. The only failure was ControllerCreatedAfterDisposalIsDisposedAsync, which is flaky on master as well.

Notes for later, none of them blocking:

  • This is a large PR with several concerns: bot chat, content languages in the admin panel, CollapsedByDefault and the ICU data in the Dockerfiles. Smaller PRs would review faster.
  • TradeRequestAction calls ITradeRequestedPlugIn before the view plugin check, so a bot may reply to a trade request which then isn't sent. That's harmless, but worth moving after the check.
  • BotChatPlugIn.cs has ~1600 lines. Splitting it by situation (whispers, party search, conversations) would help maintenance.

Generated by Claude Code

@sven-n
sven-n merged commit d2504ab into MUnique:master Nov 14, 2026
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.

2 participants