Français


Fonctionnement de la magie débilitante / neutralisante

Fonctionnement de la magie débilitante et neutralisante


/!\Attention ! Gros spoilers ! Si vous préférez garder le secret, ne continuez pas ce post ...

Coucou, l'autre jour je me suis plongé dans le code de Ryzom sur la neutra (à la base pour essayer de résoudre le "bug" folie q15), je me suis dit que ça pourrait être intéressant de partager.

Généralités


Il faut savoir que dans Ryzom, quand on effectue une action, un test de succès est fait, et il y a 4 résultats possibles (exprimé sous la forme d'un nombre décimal, Succès)
* (Succès > 1) Réussite critique (à ma connaissance, c'est uniquement les critiques à mélée / distance)
* (Succès == 1) Réussite normale (à peu près tout)
* (Succès == 0) Échec (quand on rate un sort, on rate une attaque, on rate complètement un craft ...)
* (Succès € ]0, 1[) Réussite partielle (quand on fait qu'une partie des dégâts en off, quand on soigne que 60% de la valeur prévue, quand on fait un degrade en craft...)


Cela s'applique donc évidemment aux actions de liens (débi et neutra, ainsi que les dégâts au fil du temps en élémentaire). La seule différence avec les autres actions, c'est qu'on a deux types d'actions différentes : quand on crée le lien, et quand le lien se met à jour ; chacune de ses actions réagit de manière un peu différentes aux différents types de réussite / échec.

Quand on crée le lien :

* Une réussite normale (ou critique) indique que le lien est bien créée
* Un échec signifie que le lien ne passe pas du tout
* Une réussite partielle indique que le lien est bien créé, mais immédiatement détruit. On bénéficie de l'effet du sort que pendant une certaine période, qui du type de sort, et du Succès (c'est simplement Succès * Durée pour le type de sort). On ne peut pas calculer la valeur exacte du Succès, car ça demande d'avoir accès aux données côté serveur (plus précisément, le MaxPartialSuccessFactor et le MinPartialSuccessFactor). Du coup, on peut juste dire que l'effet restera actif au plus la valeur de base indiquée après (les valeurs sont en tick, il faut diviser par 10 pour avoir une valeur en secondes) :

NoLinkTimeFear = 10; // Peur
NoLinkTimeSleep = 30; // Endormissement
NoLinkTimeStun = 15; // Étourdissement
NoLinkTimeRoot = 30; // Enracinement
NoLinkTimeSnare = 30; // Mouvement ralenti
NoLinkTimeSlow = 30; // Attaque ralentie
NoLinkTimeBlind = 20; // Aveuglement
NoLinkTimeMadness = 35; // Folie
NoLinkTimeDot = 20; // lien d'élémentaire

Quand on met à jour le lien :

* Une réussite normale (ou critique) maintient le lien
* Une réussite partielle maintient aussi le lien
* Un échec casse le lien, et l'effet cesse immédiatement

Note : le lien se met à jour toutes les 4 secondes quelque soit le type d'effet (les valeurs sont configurables pour chaque type d'effet, mais actuellement elles valent toutes 4 secondes).
Note 2 : Chaque réussite (normale, critique ou partielle) va aussi augmenter la résistance de la cible à ce type d'effet en particulier (d'une certaine valeur, dépendant du type de sort, et d'un facteur entre 0 et 1 que j'ai pas entièrement creusé). Les valeurs de base pour les différents effets sont :

ResistIncreaseFear = 6;
ResistIncreaseSleep = 4;
ResistIncreaseStun = 8;
ResistIncreaseRoot = 4;
ResistIncreaseSnare = 3;
ResistIncreaseSlow = 4;
ResistIncreaseBlind = 7;
ResistIncreaseMadness = 5;

Test de résistance


Le calcul est tout simple, il nous faut la puissance de l'attaquant, et la résistance de la cible. La puissance de l'attaquant est tout simplement le minimum entre son niveau de magie et le niveau de la brique (level 250 et une brique q15 -> puissance = 15). Pour la résistance de la cible, c'est simplement ce qui est affiché dans l'onglet "résistance" de l'identité, donc résistance naturelle (plus haut niveau en magie / combat - 26 + 10 de la race - 10 de la zone) + la résistance des bijoux.

Ensuite, on fait puissance - résistance, et le résultat est le relativeLevel, qui vaut entre -50 et 50 (si c'est en dessous ou au dessus, c'est ramené à +-50). On utilise ce résultat pour trouver dans la table de success le pourcentage de chance de réussir notre action (normale et partielle).

Table de succès


(en spoiler, car c'est un peu gros)
pour lire, c'est très simple : RelativeLevel est la valeur qu'on a calculé juste avant, SuccessProbability donne la chance de faire une réussite normale, PartialSuccessProbability celle de faire une réussite partielle.
Note : le pourcentage de chance de tirer une réussite normale est compris dans le pourcentage de chance de tirer une réussite partielle : si par exemple on a 10% de normale et 25% de partielle, on a en fait 10% de chance de tirer une normale, 15%(25-10) de tirer une partielle, et 75% de chance d'échec

Valeurs importantes : à +26 (par exemple un mage 250 contre 224 de domaine, une parrure sans résistance), on a 85% de chance de réussite normale, 90% de réussite partielle. à -50 (un sort q15 contre n'importe quel homin, par exemple), on a 10% de chance de réussite normale, 25% de réussite partielle.


Les amplificateurs


La vitesse d'un amplificateur change juste ... la vitesse à laquelle votre sort initial va partir, et c'est tout.
La puissance va changer le coût en sève de votre lien (puis la puissance est élevé, moins le coût en sève sera élevé), et c'est tout (ça ne change pas le %age de chance de passer votre sort).

Cas pratique : enchantement folie q15


Si on prend un guerrier qui spam folie q15, contre un 250 lambda, on a donc puissance = 15, domaine = 224, le relativeLevel est donc -50. La table nous dit que le SuccessProbability = 10, et le PartialSuccessProbability = 25. On tire un nombre au hasard entre 0 et 100, puis :
* S'il vaut entre 0 et 10(SuccessProbability) inclus, on a une réussite normale : un lien est créé et maintenu pour au moins 4 secondes. Si le guerrier attaque immédiatement après, ça casse le lien, mais l'effet reste actif (Succès * NoLinkMadness) ticks. Vu que Succès = 1 (on a fait une réussite normale), l'effet reste actif (NoLinkMadness) ticks, soit, 3.5 secondes
* S'il vaut entre 11 (inclus) et 25 (exclus), on a une réussite partielle : un lien est créé et cassé immédiatement, l'effet reste actif (Succès * NoLinkMadness) ticks. Ne connaissant pas la valeur exacte de Succès, ça peut être entre 1 et 35 ticks (des tests empiriques ont montré que c'était pas en dessous de 5 ticks, mais ça reste à confirmer)
* S'il vaut 25 ou plus, c'est un échec, aucun lien n'est créé.

Correctif possible


C'est, à mon avis, un fonctionnement un peu aberrant que le spam d'un enchantement q15 (qui ne coûte donc rien) permette 1 fois sur 4 de quand même avoir un effet qui va affecter le combat (en plus, si vous êtes chanceux, vous risquez d'avoir un effet qui dure pendant 2 coups de l'adversaire, sur une arme rapide type lance / épée 1M / dague). La solution la plus simple à mon goût serait de modifier la SuccessTable pour qu'il soit (quasiment) impossible de réussir un lien quand on RelativeLevel = -50 (donc quand on utilise quelque chose de très inférieur au niveau de l'ennemi)

Source & méthodologie


Évidemment, je ne sors pas cette information de nul part ; tout vient du code de RyzomCore. Il est évidemment possible que le code du serveur utilisé par Winchgate soit différent, mais cela me semble peu probable (la majorité du code se comporte de manière identique entre les deux, et des tests, même si empiriques, me montrent que c'est très certainement probable que cela marche ainsi sur le serveur de Winchgate).

J'ai commencé par chercher où trouver le fonctionnement de la magie de lien ; je savais déjà que c'était géré par le phrase_manager de l'EGS car j'avais déjà exploré le code pour fixer le bug de vitesse des attaques, mais supposons que je n'y connaisse rien et que je parte de 0.
Je vais trouver ce fonctionnement de manière évidente côté serveur (et non pas client), donc on va par là, et tout ce qui concerne les entités est géré par l'EGS (entities game service), on continue donc par là. Un petit find (ou équivalent) montre qu'il y a des fichiers nommés *magic* dans le dossier phrase_manager (c'est en fait là que sont gérés toutes les actions des joueurs).

En particulier, on tombe sur un fichier magic_action_negative_effect.cpp, qui contient une classe, CMagicActionNegativeEffect (qui hérite de IMagicAction), avec 4 fonctions : addBrick (qui a l'air de rajouter une brique à une action, pas ce qui nous intéresse), validate (qui permet de savoir si une action est valide, à priori, pas intéressant non plus), launch() et apply(), deux grosses fonctions avec un comportement un peu complexe. On ne sait pas quand elles sont appelées, on peut néanmoins regarder dans la déclaration de IMagicAction (qui se trouve dans magic_action.h, tout bon IDE vous y amenera directement) et trouver les commentaires suivants :
/// launch the action
virtual void launch(
/// apply the action
virtual void apply(

(Note : ici on pourrait simplement supposer que les deux fonctions sont appelées, d'abord launch() puis apply(), et simplement regarder le contenu des fonctions à chaque fois)

Cela nous aide pas énormément ... On va donc trouver où elles sont appelées, pour voir le contexte ! une rapide recherche de symbole nous indique que c'est utilisé dans des fichiers magic_action_*.cpp/.h, afin de définir les fonctions suivant les différentes actions magiques (soins, dégâts, lien ... qui auront donc évidemment tous des effets différents).
Du coup, on ruse un peu, et on essaie de trouver les utilisations de la classe CMagicActionNegativeEffect. Malheureusement, rien de direct, donc cherche du côté de IMagicAction. Là, on trouve des utilisations dans les magic_action*.cpp/.h (afin de définir l'héritage), et surtout deux utilisations dans magic_phrase.cpp, qui ont l'air plus intéressantes :

(ligne 116, dans CMagicPhrase::initPhraseFromAiAction) IMagicAction *spellAction = IMagicAiActionFactory::buildActionFromAiAction(aiAction, this);
(ligne 587, dans CMagicPhrase::build) IMagicAction * action = IMagicActionFactory::buildAction(actorRowId,bricks,i,buildParams, this);

Le premier a l'air de concerner la magie des IA (donc les monstres ET PNJ), le deuxième d'être plus générique, donc sans doute ce que les joueurs utilisent. Dans les deux cas, si l'action est bien créée, on le rajoute dans le vector _Actions.
Du coup, on cherche les utilisations de ce vector _Actions, et on trouve rapidement deux utilisations intéressantes. La première, ligne 1526, dans la fonction CMagicPhrase::launch() :
_Actions[i]->launch(this,deltaLvl,skillValue, successFactor,behav,_ApplyParams.TargetPowerFactor,affectedTarget s, invulnerabilityOffensive,invulnerabilityAll,isMad,resists,report) ;
Et la deuxième, ligne 1695, dans la fonctionCMagicPhrase::apply() :
_Actions[i]->apply(this,deltaLvl,skillValue, successFactor,behav,targetPowerFactor,affectedTargets, invulnerabilityOffensive,invulnerabilityAll,isMad,resists,report, _Vampirise,_VampiriseRatio, gainXp);

On cherche donc les utilisations de CMagicPhrase::launch() et on trouve rapidement que c'est utilisé dans le fichier phrase_manager.cpp, ligne 534, dans la fonction CPhraseManager::updateEntityCurrentAction. On voit que l'exécution de cette fonction dépend fortement du résultat de la fonction state() appelée sur un CSPhrasePtr (un typedef pour un CSmartPtr qui poitne vers CSPhrase), fonction qui renvoie une variable privée _State de type TPhraseState, dont la définition est dans la classe CSPhrase :

enum TPhraseState
{
New = 0,
Evaluated,
Validated,
ExecutionInProgress,
SecondValidated,
Latent,
LatencyEnded,
UnknownState,
};

De plus, on voit dans le constructeur de CSPhrase que _State est initialisé à New (ce qui paraît plutôt logique ...). Revenons donc dans notre updateEntityCurrentAction :
Au début, notre état est New, on rentre donc dans le else ligne 588 ; à priori, le test phrase->idle() est faux (on vérifie qu'idle renvoie simplement un booléen privé _Idle, qui est initialisé à false dans le constructeur de CSPhrase, ce qui est logique aussi), on ignore cela. On voit ensuite qu'on appelle d'abord phrase->evaluate(), puis (s'il n'y a pas d'erreur) phrase->validate(), puis, s'il n'y a pas d'erreur, phrase->execute(), ce qui met la phrase dans l'état ExecutionInProgress. Enfin, on a du code de nettoyage s'il faut supprimer la phrase, on peut l'ignorer, et la fonction se termine.

Manifestement cette fonction updateEntityCurrentAction est appelée plusieurs fois (on voit qu'elle est appelée dans CPhraseManager::updatePhrases() qui est appelée dans CPlayerService::egsUpdate(), qui a manifestement l'air d'être une fonction régulièrement appelée), on reteste donc une exécution (avec notre nouvel état). Cette fois-ci, on rentre dans le if ligne 501.
On commence par tester si le executionEndDate() a été atteint, et que nous sommes dans l'état ExecutionInProgress (chic ! c'est nous). On vérifie et on voit que executionEndDate() est juste un getter pour la variable privée _ExecutionEndDate, qui est modifié 1 fois seulement dans le fichier magic_phrase.cpp, et c'est lors de la fonction execute() (qu'on a appelé un peu plus tôt ):
_ExecutionEndDate = time + castingTime;
Du coup, à priori executionEndDate() correspond à la fin du temps de cast de notre sort. S'il n'est pas atteint, on rentre dans le else ligne 518, qui appelle CMagicPhrase::update(), une fonction qui renvoie ... true en permanence. On arrive donc au switch sur l'état ligne 529, et on ne trouve pas notre état, donc la fonction se termine sans rien faire d'autres.
En revanche, si executionEndDate() est atteint, alors on revalide la phrase, et on la met dans l'état SecondValidated. Ensuite, on rentre dans le switch ligne 529, et dans le case ligne 531 : on appelle phrase->launch() (youpii !! une des deux fonctions trouvées), puis on passe dans l'état CSPhrase::Latent. Ensuite, on a un petit commentaire qui nous indique qu'on ne sort pas toujours du switch si on devient Latent (ce qu'on vient de devenir). Faisons lui confiance et passons dans le case ligne 548 (et même si le commentaire est faux, on rentrerait dans ce case à la prochaine exécution de la fonction).
On voit un test pour vérifier que notre phrase n'a pas encore été apply, et ensuite on appelle phrase->apply(). Victoire !!

Pour résumer, notre phrase va être créé (on ne sait pas exactement où, mais ce n'est pas très important), puis on va appeler launch(), et ensuite appeler apply(). On peut donc revenir dans le contenu de notre fonction launch() et apply() !
La fonction launch() fait un certain nombre de checks (cible pas trop loin, cible attaquable ...) et récupère la valeur de puissance de l'attaquant, et de résistance de la cible (ainsi que les éventuels malus/bonus temporaires). On voit en particulier une ligne :
// test resistance
On approche ! Et en effet, un peu plus loin, on fait ceci :
const uint8 roll = (uint8)RandomGenerator.rand( 99 );
resistFactor = CStaticSuccessTable::getSuccessFactor(SUCCESS_TABLE_TYPE::MagicRe sistLink, localSkillValue - resistValue, roll);
On regarde rapidement, et on voit qu'on arrive dans la fonction float CStaticSuccessTable::getSuccessFactor( sint32 relativeLevel, uint8 tirage, bool fadeClip ), avec fadeClip = false (valeur par défaut). Elle récupère dans la success table la valeur SuccessProbability, PartialSuccessMaxDraw, et ensuite fait du petit traitement du nombre aléatoire : elle calcule le type de succès (succès critique, succès normal, succès partiel, échec). Les valeurs de la StaticSuccessTable ont l'air contenue dans une data côté serveur, on ne pourra à priori pas avoir les valeurs exactes :(

Ici, resistFactor est un pointeur vers targetInfos.resistFactor, qu'on stocke à la fin de launch() dans le vector _ApplyTargets. On note que si le succesFactor est > 0 (donc autre qu'un échec), on augmente la résistance de la cible à ce type d'effet.
La fonction launch() se termine ensuite, regardons la fonction apply():
On commence par quelques tests basiques (lanceur de l'action existe toujours, pas d'invulnérabilité ...), et ensuite on fait des tests sur le resistFactor : s'il est <= 0, on envoie des messages de résistances aux sorts. Et surtout, s'il vaut plus de 0, on crée un CSLinkEffectOffensive, le nom est intéressant ... Ensuite, on teste phrase->breakNewLink() (on voit rapidement que cela vaut true uniquement si notre action va casser un lien déjà existant, car on ne peut avoir qu'un lien en même temps), et si c'est faux, on rajoute le lien à l'acteur, et l'effet à la cible. De plus, si on a une résistance partielle, le lien est cassé immédiatement en appelant CCharacter::stopAllLinks, qui appelle CEntityBase::stopAllLinks, qui va appeler sur chaque lien
effect->breakLink(factorOnSurvivalTime);

L'effect ici est celui qu'on a créé un peu plus tôt, on va donc regarder de plus près cette classe. La fonction breakLink est très simple, elle met un timer de fin à factorOnSurvivalTime (qui vaut successFactor, donc la réussite partielle) * (_NoLinkSurvivalTime + CSLinkEffect::getNoLinkDurationTime(_Family)). Une recherche rapide de ses utilisations montre que _NoLinkSurvivalTime vaut toujours 0, on peut l'ignorer. Par contre, on voit que getNoLinkDurationTime nous renvoie une CVariable, qui sont des variables définies avec une valeur par défaut dans le code, valeur qui peut être écrasée si définie dans un fichier de configuration. Ça tombe bien, on a accès au fichier de configuration utilisé par Winchgate (sans doute un oubli, mais cette version est très cohérente avec les valeurs que j'ai pu voir IG sur de très nombreux points, donc onva supposer que c'est le même) pourl'EGS dans code/server/patchman_cfg/default/entities_game_service.cfg ; un grep sur NoLink donne les valeurs que j'ai montré plus haut.

On peut donc déjà conclure que si on fait une réussite partielle, notre lien sera cassé mais l'effet durera entre 0 et NoLinkTime{Effet} ticks, enfonction du SuccessFactor (qui est calculé, dans le cas d'un succès partiel, avec _MaxPartialSuccessFactor et _MinPartialSuccessFactor, des valeurs qui viennent des données serveur, donc auquel on n'a à priori pas accès :( cela reste néanmoins une information importante !)

Si on regarde la réussite normale maintenant, elle rajoute simplement un lien et un effet. Plongeons donc dans la classe CSLinkEffectOffensive, qui hérite de CSLinkEffect, et a deux fonctions : update(), une virtual (qui doit sûrement être appelé dans egsUpdate() ou similaire, en tout cas c'est à priori une fonction récurrente) qui appelle simplement updateOffensive(). Elle a un fonctionnement similaire à launch(), en appelant CSLinkEffect::update() qui effectue plusieurs checks peu intéressant ; on peut cependant remarquer à la ligne 197 :
_UpdateTimer.setRemaining( CSLinkEffect::getUpdatePeriod(_Family), event );
On voit rapidement que getUpdatePeriod renvoie une CVariable aussi, et on trouve dans le egs.cfg qu'il faut 40 ticks pour tous les effets ; donc à priori, notre update() est appelé toutes les 4 secondes, bon à savoir !
Une différence notable : si la variable _FirstResist est vrai, on s'arrête ici (on met juste _FirstResist à false). On voit que dans le constructeur _FirstResist vaut vrai, donc à priori lors du premier appel de update() on ne fait pas de test de résistance (ce qui paraît logique : on a déjà réussi un test de résistance pour launch() l'action correctement, et c'était dans le même tick ...).
Si _FirstResist vaut false, on relance un test de résistance ; si on a encore une réussite (normale ou partielle, ça n'importe plus), on augmente la résistance de la cible, et on envoie un report pour l'XP (action réussie, le joueur a le droit de toucher de l'XP si jamais c'est un monstre en face). Par contre, si on a un échec, la variable end vaudra true, et donc à la fin de updateOffensive(), on va mettre le EndTimer à 1 tick de maintenant (c'est à dire qu'on arrête tout de suite le lien)

On sait donc maintenant qu'en cas de réussite normale, le lien est créé, et on ne fait pas de test de résistance la première fois ; ce qui veut dire qu'il durera au moins jusqu'au prochain update(), qui est 40 ticks plus tard. S'il y aun échec, cette fois-ci l'effet est immédiatement annulé.


On a donc globalement compris le fonctionnement du système, il nous manque cependant deux informations cruciales : les valeurs du StaticSuccessTable, présents dans une .packed_sheet, et les valeurs de _MaxPartialSuccessFactor et _MinPartialSuccessFactor (certes moins importantes, mais tout de même). À priori, c'est terminé, mais il ne faut pas oublier qu'un certain nombres de données sont partagées entre le client et le serveur. On va donc dans le dossier data de notre client, et on regarde les fichiers *.packed_sheets présents. On voit, par chance, qu'il y a un fichier succes_chances_table.packed_sheets (on remarquera la belle faute sur succes !). Une petite recherche dans le code de notre fichier avec "succes_chances_table" nous montre que c'est bien utilisé dans l'initialisation de l'egs staticSuccess (c'est la bonne !), etcôté client dans src/interface_v3/sphrase_manager.cpp::loadSuccessTable, qui les emts dans une variable _SuccessTableSheet.

Pour les voir, on s'embête pas: on met un std::raise(SIGINT); à la fin de cette fonction loadSuccessTable, on compile avec les symboles de debug (-DWITH_SYMBOLS=ON), on exécute notre client avec gdb, et quand on arrive à notre breakpoint, on se met dans la frame CSPhraseManager::loadSuccessTable, et on les affiche comme ceci :

p _SuccessTableSheet[6]->SuccessTable

On a nos valeurs, on n'est cependant pas certains que c'est les mêmes utilisées côté serveur. Pour s'en convaincre, j'ai
1) Pris le .packed_sheets généré par sheets_packer à partir des données RyzomCore, qui utilise des valeurs débiles pour la SuccessTable, et j'ai mis le .packed_sheets dans mon user et relancé le client : j'avais bien les données débile, donc il semblerait bien que le .packed_sheets que j'ai est généré avec sheets_packer qu'on a lancé sur les données winchgate : c'est sans doute les bonnes
2) Pris mes amplis, demandé à gentil guildoux de faire un duel avec moi, et j'ai envoyé des sorts d'enracinement q15 (pour avoir un RelativeLevel = -50) en boucle, en notant combien j'en faisais et combien de résisté. Je me suis arrêté au bout de 500 sorts au total, et 132 sorts passés. Ça fait 26.4% de réussite, on est plutôt proche des 25% de réussite partielle annoncée par les résultats trouvé juste avant (je n'ai pas compté les liens qui ne cassaient pas immédiatement, donc les réussites normales, par flemme, mais je pense qu'on a les mêmes résultats).

Pour les valeurs exactes de _MaxPartialSuccessFactor et _MinPartialSuccessFactor, je n'ai pas encore trouvé de manière de les trouver ; je pense qu'ils sont aussi dans le succes_chances_table.packed_sheets, mais je connais trop mal le format des packed_sheets pour en dire plus. Si jamais quelqu'un a la réponse, je prends !

---

Show topic
Last visit Thursday, 28 March 18:00:28 UTC
P_:

powered by ryzom-api