Skip to content

Module Mercenaires

SylvaniaCore deploy edited this page Aug 31, 2026 · 1 revision

Module Mercenaires

État : actif en production (pbotmerc = 1)

Un joueur solo loue des compagnons — des playerbots — contre de l'or, auprès d'un PNJ dédié. Ils rejoignent son groupe, obéissent à ses ordres, et peuvent dialoguer via un modèle de langage payé par la clé API du joueur.


Le PNJ

Portail d'Invocation de Mercenairecreature_template 1000010 :

Champ Valeur
displayid 74465
rank 3 (boss)
faction 35 (amical avec tous)
unit_flags 770
ScriptName npc_mercenary_portal

Le PNJ n'est pas spawné par SQL : les portails sont posés à la main en jeu avec .npc add 1000010 (60 spawns en production).

Le gossip enseigne aussi le système d'ordres depuis le commit 9e18ce4c (sous-menu « Comment leur donner des ordres ? »).


Règles du contrat

Règle Valeur / motif
Coût 100 po (pbotmerc_cost)
Plafond 4 mercenaires (pbotmerc_max)
Niveau minimum de l'employeur 10 (pbotmerc_minlevel)
Rôle acheté protecteur / guérisseur / combattant
Niveau du mercenaire celui de l'employeur

Pourquoi 4 et pas 5 — un groupe WoW compte cinq places, employeur inclus. Cinq mercenaires imposeraient un raid, ce qui interdirait les donjons. MERCENARY_HARD_CAP = 4.

Le contrat est jetable, par choix de conception. Déconnexion du joueur, départ ou dissolution du groupe, expulsion d'un mercenaire → le bot quitte le groupe et se déconnecte. Toute nouvelle invocation se repaie. Aucun remboursement, sauf échec d'invocation (aucun bot présenté en 60 s).

Classes utilisables

Les 10 classes couvertes par AI/PlayerAI/BotGroupAI/ : guerrier, paladin, chasseur, voleur, prêtre, chevalier de la mort, chaman, mage, démoniste, druide.

Pas de moine ni de chasseur de démons — ils resteraient plantés.


Architecture

src/server/game/Mercenary/
├── MercenaryMgr.{h,cpp}    # 763 lignes — contrats, invocation, rupture
└── MercenaryChat.{h,cpp}   # 967 lignes — volet dialogue LLM
src/server/scripts/Mercenary/

Aucun fichier du core n'est modifié. Les ruptures de contrat passent par les hooks existants — GroupScript::OnRemoveMember, GroupScript::OnDisband, PlayerScript::OnLogout. Seul ajout externe : CapitalSiegeMgr::IsAccountEngaged(), pour ne pas débaucher un bot engagé dans l'assaut quotidien.

⚠️ Ne jamais modifier un groupe depuis un hook de groupe

La leçon la plus coûteuse du module (corruption de tas du 16/08/2026, commit 0dbbb73a) :

  • Group::Disband() appelle sScriptMgr->OnGroupDisband(this) en première instruction, puis finit par delete this ;
  • Group::RemoveMember appelle Disband() dès que le groupe repasse sous deux membres.

Congédier un mercenaire depuis ces hooks réentre dans Disband() → double libération du Group → tas corrompu et crash différé dans un malloc sans aucun rapport.

Solution : les hooks se contentent de marquer pendingRelease ; Update() libère au tick suivant. Le garde-fou m_releasing ne protège que nos propres hooks, pas la réentrance du core.

Le diagnostic complet de cette corruption est détaillé dans Diagnostic des crashs — c'est un bon cas d'école.

Autre piège corrigé dans le même commit

PlayerBotMgr::UpdateIdleBotLogout exemptait le siège des capitales mais pas les mercenaires : un bot loué resté immobile était déconnecté après pbotbg_idlelogout (300 s). Exemption via IsAccountHired().

Texte de gossip

En 7.3.5, le texte d'un npc_text vit dans le DB2 BroadcastText (table dc_hotfixes.broadcast_text). Conséquence : pas de texte de gossip custom sans hotfix. Le boniment du portail passe donc par creature->Whisper(std::string, LANG_UNIVERSAL, player).


Volet dialogue LLM

Livré le 18/08/2026 (commit 77c5c68e). Un mercenaire peut converser, en français, dans un registre médiéval-fantastique, sans jamais rompre l'illusion.

Principe : la clé du joueur, jamais stockée

C'est l'exigence qui structure toute l'implémentation.

!api <fournisseur> <modèle> <clé> [adresse]
!api off

La commande est interceptée dès la première instruction de WorldSession::HandleChatMessage — avant tout journal et toute rediffusion. Sans cela, la clé s'afficherait dans le chat des autres joueurs et dans les fichiers de log du serveur.

La clé vit en mémoire seule, est écrasée caractère par caractère avant libération, et effacée à la rupture du dernier contrat, à la déconnexion, et à l'arrêt du serveur.

Fournisseurs

Trois adaptateurs natifs : OpenAI et compatibles, Anthropic, Gemini.

Fournisseur État depuis le VPS
Mistral (via l'adaptateur OpenAI) validé en jeu, canal retenu par le royaume — français natif, quota généreux, sans carte bancaire
OpenAI, Anthropic, OpenRouter ✅ joignables
Gemini en direct inutilisable depuis un centre de données

Commande qui fonctionne dans tous les cas :

!api openai mistral-small-latest <clé> https://api.mistral.ai/v1/chat/completions

⚠️ La forme courte !api mistral … n'existe que depuis l'ajout de MERC_LLM_MISTRAL. Avant, mistral était un alias d'OpenAI et la clé partait chez OpenAI — d'où un 401 trompeur.

Gemini. L'adaptateur fonctionne de bout en bout (payload system_instruction + contents + generationConfig), modèle validé : gemini-3.5-flash-lite. Mais Google renvoie 400 FAILED_PRECONDITION — "User location is not supported for the API use" pour tout appel venant d'une IP de centre de données, alors que la même clé fonctionne depuis une machine personnelle. Ce n'est pas le pays qui est refusé, c'est le type d'IP. Contournement : passer par OpenRouter en mode compatible OpenAI. Google retire vite ses anciens modèles (gemini-2.0-flash et gemini-2.5-flash-lite renvoient déjà 404) — pour lister ce qu'une clé permet :

curl -s "https://generativelanguage.googleapis.com/v1beta/models?key=CLE"

Règles de conversation

  • Le mercenaire répond dans le canal d'où vient le message.
  • Dans le canal de groupe, tout compagnon peut lui parler.
  • En chuchotement, seul l'employeur — c'est sa clé qui paie.
  • Anti-spam par délai (pbotmerc_chat_cooldown, 5 s).

⚠️ Les ordres ont changé de forme

Depuis l'ajout du dialogue, tous les ordres prennent le préfixe ! :

!follow      !stop      !@heal stop      !seduce

Sans préfixe, un mot comme « stop » au milieu d'une phrase figeait le mercenaire. Toute documentation ou annonce parlant des anciennes commandes est périmée.

Pièges de compilation

  • dep/rapidjson ne compile plus avec GCC 14 : document.h, GenericStringRef::operator= assigne un membre const. Le core ne l'utilise que par reader.h / writer.hn'incluez pas document.h ; écrivez un extracteur ou passez par le lecteur événementiel.
  • libcurl est lié directement dans src/server/game/CMakeLists.txt (find_package(CURL)), plutôt que d'activer WITH_CPR qui recompilerait curl et cpr en entier.
  • Jamais d'appel réseau dans le fil du monde : fil dédié + files d'entrée / sortie.

Leçon de diagnostic

Ne concluez jamais d'un code HTTP nu. Sur le premier échec Mistral : clé crue invalide (elle était bonne), puis payload cru défectueux (il était bon) — seul le message textuel du fournisseur disait la vérité. Le module le remonte désormais au joueur en jeu.


Configuration

pbotmerc               = 1
pbotmerc_cost          = 100   # pièces d'or
pbotmerc_max           = 4
pbotmerc_minlevel      = 10
pbotmerc_chat          = 1
pbotmerc_chat_cooldown = 5     # secondes
pbotmerc_chat_timeout  = 30    # secondes
pbotmerc_chat_maxtokens= 160
pbotmerc_chat_history  = 6
pbotmerc_chat_queue    = 32

Les valeurs ci-dessus sont celles de la production. Le worldserver.conf.dist versionné livre les défauts du code, module éteint (pbotmerc = 0, pbotmerc_chat = 0) — voir Configuration.

SQL associé

sql/sylvania/mercenary.sql (créature et gossip) · sql/sylvania/mercenary_title.sql (titre custom).

Documentation joueur

https://legendesylvania.com/mercenaires

Clone this wiki locally