Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
66cb485
feat(bots): let bots talk with configurable lines
nolt Nov 4, 2026
82ae662
feat(bots): wait for players after announcing an event
nolt Nov 4, 2026
3e2e94f
feat(bots): answer whispers by their keywords
nolt Nov 4, 2026
4dfdbd9
feat(bots): greet players entering the game
nolt Nov 4, 2026
3ffb6a3
feat(bots): talk in the party chat of a player
nolt Nov 4, 2026
d88039b
feat(bots): comment on being killed by a player
nolt Nov 4, 2026
60f4982
feat(bots): answer when mentioned in the normal chat
nolt Nov 4, 2026
bf614d5
feat(bots): make small talk in town
nolt Nov 4, 2026
ae328ff
feat(bots): ship the default chat lines in Polish too
nolt Nov 4, 2026
a93b94b
refactor(bots): give every chat situation its own list
nolt Nov 4, 2026
7c4927f
feat(bots): tell where the bot stands with {location}
nolt Nov 4, 2026
984cb07
feat(bots): answer a player's party search posted with /post
nolt Nov 4, 2026
bbe343c
feat(bots): let two bots talk to each other near a player
nolt Nov 4, 2026
49c8894
feat(bots): turn the lone small talk into merchant adverts
nolt Nov 4, 2026
79e19cf
feat(bots): congratulate players on level-ups and resets
nolt Nov 4, 2026
98a885e
feat(bots): take the time to type an answer
nolt Nov 4, 2026
a7782c5
feat(bots): don't give a player the same answer twice in a row
nolt Nov 4, 2026
db490d5
feat(bots): fit lines and conversations to the time of day
nolt Nov 4, 2026
f01e91c
feat(bots): say why a trade or duel is turned down
nolt Nov 4, 2026
874f487
feat(bots): keep quiet while the players around are talking
nolt Nov 4, 2026
d29b033
feat(admin-panel): edit translations in the server's content languages
nolt Nov 4, 2026
b1547b0
feat(admin-panel): start the lists of the bot chat collapsed
nolt Nov 4, 2026
5ae6797
feat(bots): answer in the party chat
nolt Nov 4, 2026
8beb357
feat(bots): understand more ways of saying goodbye
nolt Nov 4, 2026
a76a674
feat(bots): give the bots a richer default vocabulary
nolt Nov 4, 2026
34dcebb
feat(bots): answer in the language of the player's message
nolt Nov 4, 2026
3c494e4
feat(bots): make gestures with the lines
nolt Nov 4, 2026
980f212
feat(bots): let a conversation have a third line and players chime in
nolt Nov 4, 2026
fc1b1f2
feat(bots): remind of an opened event in town
nolt Nov 4, 2026
097f3bc
feat(bots): react to rare drops and notable monsters nearby
nolt Nov 4, 2026
c9f6b18
feat(bots): remember the players a bot was in a party with
nolt Nov 4, 2026
e0a7f37
feat(bots): let a game master make a bot talk with /botsay
nolt Nov 4, 2026
657e1d7
feat(bots): comment on wings
nolt Nov 5, 2026
af9bce8
fix(bots): come to a meeting place through the warp which leads there
nolt Nov 7, 2026
af570d8
feat(bots): understand an event in a party search
nolt Nov 8, 2026
1fe7b43
docs(bots): wrap the party search paragraph
nolt Nov 8, 2026
9c9b218
fix(bots): forget bot conversations nobody chimed in to
nolt Nov 13, 2026
78b4a29
fix(bots): forget expired chat occasions
nolt Nov 13, 2026
abab816
fix(bots): reserve a bot for one party search at a time
nolt Nov 13, 2026
b15ff0a
fix(bots): keep the party offer when the meeting place is out of reach
nolt Nov 13, 2026
bd42958
fix(bots): walk to a meeting place on the map the bot stands on
nolt Nov 13, 2026
8aaf769
fix(bots): say a delayed party line only to the party it was meant for
nolt Nov 13, 2026
729ad62
fix(bots): drop delayed bot chat once the plugin is deactivated
nolt Nov 13, 2026
08e50d0
docs(bots): describe naming another meeting place and walking on the …
nolt Nov 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 160 additions & 0 deletions docs-website/docs/server-features/bots.md
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,166 @@ Over the day, the presence rotation logs bots in and out: fewest in the early
morning, most in the evening, and never more than one at a time, so the
population ebbs and flows instead of appearing and vanishing in blocks.

### Talking

With the separate *Bot chat* plugin (off by default), bots say a line now and
then. The lines come from the plugin's configuration, one list per situation
(event lines, greetings, whisper replies, …); there is no language model. A bot
only talks about what it could actually do:

| Situation | Where | Who says it |
|---|---|---|
| A Blood Castle, Devil Square or Chaos Castle entrance opened | `/post`, then it waits for players | a bot which meets that event's entry requirements — the same check that lets it follow a leader in |
| A Blood Castle, Devil Square or Chaos Castle entrance opened | normal chat in town, no `/post` ("bc open, anyone going?") | the nearest bot which meets the entry requirements, once per town where a player stands - 50 % chance |
| An invasion (White Wizard, Golden, Red Dragon) started | `/post` | a bot which may warp to the invaded map |
| A level milestone (every 50 levels by default) | normal chat, players nearby | the bot which reached it |
| A player entered the game (not after a warp) | normal chat, `{player}` | the nearest bot which sees the player — once per player in 30 minutes |
| A player leveled up or made a reset | normal chat, "gz", `{player}`, `{level}`, `{resets}` | the nearest bot which sees the player — 30 % chance, once per player in 10 minutes |
| A player's kill dropped an excellent or ancient item | normal chat ("nice drop!"), `{item}` | the nearest bot - 50 % chance |
| The bot put on the new wings it earned at a level milestone | normal chat ("finally, my wings!"), `{wings}` | the bot, while a player sees it - 50 % chance; at a level milestone, instead of the level comment |
| A player put on wings | normal chat ("nice wings!"), `{player}`, `{wings}` | the nearest bot which sees the player - 50 % chance, once per player and pair until the server restarts |
| A golden monster, the Red Dragon or the White Wizard appeared near a bot | normal chat ("Golden Goblin here!"), `{monster}` | the nearest bot, while a player sees it - once per map and monster in two minutes |
| A player the bot was in a party with before enters the game or invites it again | normal / party chat ("good to see you again"), `{player}` | the bot - it remembers its party companions until the server restarts, and a companion nearby greets first |
| The bot accepted a player's party invitation | party chat, `{player}` | the bot |
| An event the bot went through with a player's party ended | party chat, `{event}` | one bot per party |
| A player killed the bot (outside of an event) | normal chat, `{player}`, before it respawns — a different line when the same player kills it again | the bot |
| A player mentioned the bot's name in the normal chat next to it | normal chat, the same lines as whisper replies | the bot |
| A player wrote in the party chat (`~`) | party chat, the same lines as whisper replies | the bot whose name is in the message; without a name, one bot of the party - only when a keyword matches |
| Now and then, like a merchant advertising (off by default — meant for a future merchant bot) | normal chat, `{location}` | a bot standing in a safezone which a player sees |
| A player stands near two bots (every 5 minutes per player) | normal chat: one bot says something, the other answers | two bots close together, both in a town or both out hunting |
| A player asked the bot for a trade or challenged it to a duel | whisper, 70 % chance, `{player}` — the bot turns it down in any case (no trade window, no duels) | the bot |
| A player whispered to the bot | whisper back, after a few seconds — answering the whisper's keywords (see below) | the bot |

A bot which posts about an event does not stay out hunting where nobody
could find it. It goes to the meeting point and waits there until the entrance
closes: next to the Messenger of Archangel for Blood Castle, next to Charon for
Devil Square, and — 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 it decline party invitations; being invited is the point. A player who
invites it and enters the event takes it along like any party bot. If nobody
invites it, it goes back to hunting when the entrance closes. A bot does not
announce an event whose entrance closes in less than two minutes, and a bot
already in a party with a player does not look for company.

A player can also search party members among the bots with a post:
`/post party 300-350 bk dw`. A post with one of the party words (`party`, `pt`,
`grupa`, …) and a level — `300-350`, `300+`, `min 300` or just `300` — calls up
to five free bots of the game server whose level fits; class words (`dk`, `bk`,
`bm`, `dw`, `elf`, `mg`, `dl`, `sum`, `rf`, … — each meaning its whole class
line) narrow it down. Instead of a level the post may name an event with its
level — `/post party bc7`, `pt ds 5`, `party chaos castle 3` — and then only
bots which may enter that event answer: the event's own level bracket (the
lower one of the Magic Gladiator, Dark Lord, Rage Fighter and Summoner
included) and the master class requirement of the Blood Castle 8, as
configured for the event. Each of them whispers to the player, a few seconds
apart.
The same search posted again calls five others. The player whispers back to a
bot where to meet — `devias 215 40`, a warp of the warp list like
`devias3 227 227` or `losttower7`, a map name alone, or `here` for where the
player stands — and the bot warps there, walks up and waits five minutes for
the invitation. It enters through the warp from which the place is the
shortest walk, like a player would: the floors of the Lost Tower or the
Dungeon can't be walked between at all, and the walk from the town of Devias
to Devias3 takes long. A bot already standing on the map walks to the named
coordinates, even on a map without an entry in the warp list. A place it may
not go to (its level is too low for any warp leading there) it declines, and
the player may name another one while the offer lasts; a whisper without a
place gets asked again.
Map and warp names are recognized in every translation. The words, the number
of bots and the times are part of the configuration.

The conversations make a town feel lived in, like the townsfolk of a
role-playing game: "my potions are running low" — "the shop here sells them
cheap". Only when a human player stands near two bots which stand close
together do they talk — nobody else would hear it. Each conversation has a
place: `Town` (both bots in a safezone), `Hunting` (both outside of it, usually
fighting) or `Anywhere`. They are drawn without repetition until all of a place
were used, and each player hears about one every five minutes. The default
configuration comes with some 190 of them. A conversation may have a third
line - the first bot's riposte. 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 "right?",
"haha, exactly" and the like.

A whisper reply can carry `Keywords`, a comma-separated list of words or
phrases it answers (`how are you, how r u`). The bot picks among the replies
whose keyword matches the whisper — whole words, ignoring case and punctuation,
the longest match winning, so "hi, how are you?" gets an answer to the question
rather than a greeting. When nothing matches, it uses a reply without keywords.
The default lines cover greetings, "how are you", the level and class, where the
bot is, where to hunt, its wings, party, help, trade, thanks and goodbye.

Placeholders in a line are filled in when it is said: `{event}`, `{map}`,
`{level}`, `{class}`, `{player}` (whisper replies), `{partner}` (the other bot of
a conversation), `{wings}` (see below), and `{location}` — the map with the
bot's coordinates the way players write them, e.g. "Devias 215, 40", so a player
can find it with the mini map. The default "where are you?" replies use it. In
an event post, `{location}` is the meeting point; in an invasion post, just the
invaded map. Each line is a localized text, so
every player sees the translation of their own language, or the English text
when it has none. The default lines come with a Polish translation, and the default
keywords understand Polish whispers too.

`{wings}` names the bot's wings with their level, e.g. "Wings of Soul +9"; in a
comment on a player's wings, the player's. A bot without wings doesn't answer
"what wings do you have?" with an empty name: a whisper reply with `{wings}` is
swapped for one of the *answers without wings* ("no wings yet, saving up").

A bot answers a message in the language it is written in: a Polish whisper
gets a Polish answer, an English one an English answer, whatever the player's
account language. The *Message languages* list holds the typical words and
letters by which it tells them apart; a message which doesn't tell ("lol") is
answered in the player's own language. Everything else a bot says - greetings,
posts, conversations - follows the account language:

For the Polish lines to reach a Polish player, three things are needed:

1. **The player's language is set to `pl`** — the *Language Iso Code* of the
player's account in the admin panel. The game client doesn't tell the server
its language, and the `/language` command only offers the languages of the
server messages.
2. **The server has the full culture data.** The Docker images install
`icu-data-full`; with the English culture data alone (`icu-libs` on Alpine),
the server doesn't know the `pl` culture and every player gets the English
lines. A self-built image needs the package too.
3. **The admin panel shows a field for Polish**, to read and edit the
translations. It shows the languages of its own user interface, every
language a text already has a translation in, and the additional content
languages of the `AdminPanel:ContentLanguages` setting — e.g. the environment
variable `AdminPanel__ContentLanguages__0=pl`. Without the setting, the
Polish default lines are still shown, but a new line gets no Polish field.

Every line and conversation has a `When`: `Anytime` (the default), `Morning`
(5–11 o'clock), `Day` (11–18), `Evening` (18–23) or `Night` (23–5), by the local
time of the server. A greeting may be "good morning" in the morning and "late
night grind?" at night.

A line can come with a `Gesture` - the emotes of the game client, like a player
typing `/hello`: the bot turns to the player and waves with a greeting or a
goodbye, claps with a "gz", cheers after an event, raises its fist when a
player killed it. The default lines have fitting gestures.

A bot answers like a typing player: it takes a moment to read, then a little
time per character, so a quick "np" comes at once and a whole sentence takes a
few seconds (at most twelve).

The bots don't drown out the players: while the players around a bot are busy
talking (three normal chat messages within 30 seconds, by default), it says
nothing unasked — no conversation, greeting, congratulation or comment. It
still answers a player who talks to it.

A game master can make a bot talk with `/botsay <bot> <text>` - for staged
scenes of an event, or to spark a conversation. The text is said as typed in
the normal chat, to the players who see the bot; only bots can be made to talk.

The limits keep a large population from flooding the chat. All bots of a game
server share one gap between two posts (`Minimum time between posts`, 3 minutes),
each opened event type or started invasion is announced at most once, and each
bot has its own cooldown for the normal chat and for whisper replies. The two
post chances are 30 % by default, so not every opened event or invasion gets a
post. Bots post only while the `/post` command is enabled on the server, and
their messages look exactly like a player's post.

### Keeping itself alive

The engine's attribute system is not thread-safe, and a lost race can corrupt a
Expand Down
4 changes: 3 additions & 1 deletion src/Dapr/AdminPanel.Host/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ WORKDIR /app
# OpenMU uses localized texts, which require the culture data of ICU.
# The alpine images don't contain it and run in the globalization-invariant mode by default.
# tzdata provides the IANA time zones (e.g. Europe/Berlin), which the alpine images don't contain either.
RUN apk add --no-cache icu-libs tzdata
# icu-data-full: icu-libs alone brings the English culture data only, so a player's language like
# "pl" isn't found and every player gets the English texts.
RUN apk add --no-cache icu-libs icu-data-full tzdata
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
EXPOSE 8080

Expand Down
4 changes: 3 additions & 1 deletion src/Dapr/GameServer.Host/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ WORKDIR /app
# OpenMU uses localized texts, which require the culture data of ICU.
# The alpine images don't contain it and run in the globalization-invariant mode by default.
# tzdata provides the IANA time zones (e.g. Europe/Berlin), which the alpine images don't contain either.
RUN apk add --no-cache icu-libs tzdata
# icu-data-full: icu-libs alone brings the English culture data only, so a player's language like
# "pl" isn't found and every player gets the English texts.
RUN apk add --no-cache icu-libs icu-data-full tzdata
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
EXPOSE 8080

Expand Down
17 changes: 17 additions & 0 deletions src/DataModel/Composition/CollapsedByDefaultAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// <copyright file="CollapsedByDefaultAttribute.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

namespace MUnique.OpenMU.DataModel.Composition;

/// <summary>
/// Marks a collection property to be shown collapsed on the UI at first, whatever the number of its
/// items - for a configuration with many short lists, which would otherwise make one endless page.
/// </summary>
/// <remarks>
/// Without the attribute, a list starts collapsed only when it holds more than a few items.
/// </remarks>
[AttributeUsage(AttributeTargets.Property)]
public sealed class CollapsedByDefaultAttribute : Attribute
{
}
Loading
Loading