Bonjour à tous, aujourd’hui je vous propose un sujet assez différent : la création d’un addon pour World of Warcraft.
Introduction
World of Warcraft, pour ceux qui l’ignorent, est un MMORPG à abonnement sorti en 2004 et développé par Blizzard Entertainment. Le jeu offre la possibilité de modifier l’interface du jeu, à l’aide d’addons, qui sont chargés et exécutés par le client.
Récemment, je me suis intéressé au fonctionnement d’un addon, par curiosité, et je me suis mis en tête d’en créer un petit. Je vous propose donc un petit tutoriel qui prendra pour exemple l’addon que j’ai réalisé : GoldSquish.
Cet addon est assez simple, puisqu’il se contente de modifier la façon dont la valeur de la monnaie du jeu (les pièces d’or) est calculée par l’interface. Dans la version de base (sans addon donc), une pièce d’or vaut 100 pièces d’argent et une pièce d’argent vaut 100 pièces de cuivre. Les constantes d’interface SILVER_PER_GOLD et COPPER_PER_SILVER peuvent cependant être modifiées pour augmenter ou réduire la valeur général des objets du jeu.
Bien entendu, cette modification est locale au joueur et n’influe pas sur le jeu lui même. Par exemple, si un objet vaut normalement 1 po et qu’un joueur possède 100 po, fixer SILVER_PER_GOLD à 1 va faire qu’il se verra avec 10 000 pièces d’or et l’objet sera affiché comme valant 100 po.
L’objectif de l’addon est également de proposer une petite interface graphique pour les options pour permettre au joueur de régler la façon dont l’addon va modifier la valeur de la monnaie.
Architecture et fonctionnement d’un addon
Comme je l’ai indiqué précédemment, les addons sont chargés au démarrage puis sont interprétés par le jeu. Une certaine architecture minimale est requise pour qu’un addon puisse fonctionner.
Les addons sont programmés en langage LUA, un langage très utilisé pour le scripting (jeux vidéos, wireshark…) principalement en raison de sa rapidité, de sa faible empreinte mémoire et de sa faible courbe d’apprentissage. Je ne vais pas faire un cours sur le LUA dans cet article, mais n’ayez crainte, ça se prend en main très facilement.
La première chose indispensable pour un addon est le fichier TOC (pour Table of Content), qui est le premier fichier lu par le jeu au démarrage. Ce fichier contient les informations générales sur l’addon. Les fichiers d’un addon sont situés dans le dossier d’installation du jeu, à l’emplacement World of Warcraft/_retail_/Interface/addons/NomDeLAddon ( World of Warcraft/Interface/addons/NomDeLAddon avant le patch 8.1). Voici le fichier .toc final de GoldSquish :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
## Interface: 80100 ## Title: GoldSquish ## Author: Etienne Boespflug ## Version: 1.0 ## Notes : Allow you to set the value of golds in the game. ## SavedVariables: GoldSquishDB ## OptionalDeps: Ace3 ## X-Embeds: Ace3 includes.xml Locales\Locales.xml main.lua |
Ici, 80100 est l’interface number (IN), qui correspond à un identifiant permettant au jeu de savoir si l’addon est à jour par rapport à la version actuelle du client du jeu. D’une manière générale, celui-ci est directement lié à la version du jeu. Ici, il correspond donc à la version 8.1, la version 7.2 a pour IN 70200 par exemple (les numéros de version autres que majeurs et mineurs sont le plus souvent ignorés, la 7.2.5 avait donc également 70200 comme IN). Vous pouvez retrouver l’inteface number actuel avec la commande /run print((select(4, GetBuildInfo()))); en jeu.
Les attributs Title, Author et Version sont suffisamment explicites à mon avis. Le contenu de Notes sera affiché comme info-bulle dans le jeu pour offrir une description de l’addon dans le jeu.
Les attributs SavedVariables, OptionalDeps et X-Embeds seront détaillés par la suite. Vous trouverez des détails sur les attributs d’un fichier TOC ici.
Ensuite, le fichier TOC référence les fichiers qui sont utilisés par l’addon. Dans notre cas, includes.xml contient les références des fichiers des bibliothèques externes, Locales.xml référence les fichiers de traduction et main.lua correspond au fichier source principal de l’addon, en LUA.
Bibliothèque Ace3
Pour cet addon, j’ai choisi d’utiliser la bibliothèque Ace3 (qui n’a d’ailleurs rien à voir avec celle du même nom pour Arma 3), très largement utilisée pour les addons sur World of Warcraft : Elvui, DBM, Details, Recount…
En réalité, Ace3 est un ensemble de bibliothèques fournissant un grand nombre de fonctionnalités : AceAddon, AceEvent, AceDB, AceGUI, AceConsole, AceLocale, CallbackHandler… Nous n’allons bien évidemment pas tout utiliser, mais ça va très largement nous simplifier le développement de notre addon, notamment pour les traductions et le panneau de configuration GUI.
L’attribut du fichier TOC OptionalDeps permet d’indiquer que notre addon a une dépendance envers Ace3 et qu’il est possible d’utiliser une version globale d’Ace3 plutôt que notre version embarquée. X-Embeds sert quant à lui aux logiciels de mise à jour d’addon (tels que Twitch, anciennement Curse client), quelles sont les bibliothèques externes à mettre à jour si l’utilisateur veut utiliser une version globale.
Commençons donc par un addon tout simple, qui se contente d’afficher “Hello World!”. Ci-suit le fichier .toc, le fichier includes.xml et le fichier main.lua, qui sont à placer dans le dossier de l’addon ( World of Warcraft/_retail_/interface/addon/GoldSquish) :
1 2 3 4 5 6 7 8 9 10 |
## Interface: 80100 ## Title: GoldSquish ## Author: Etienne Boespflug ## Version: 1.0 ## Notes : Allow you to set the value of golds in the game. ## OptionalDeps: Ace3 ## X-Embeds: Ace3 includes.xml main.lua |
1 2 3 4 5 6 7 8 9 10 11 12 |
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\FrameXML\UI.xsd"> <Script file="Libs\LibStub\LibStub.lua"/> <Include file="Libs\CallbackHandler-1.0\CallbackHandler-1.0.xml"/> <Include file="Libs\AceAddon-3.0\AceAddon-3.0.xml"/> <Include file="Libs\AceEvent-3.0\AceEvent-3.0.xml"/> <Include file="Libs\AceDB-3.0\AceDB-3.0.xml"/> <Include file="Libs\AceLocale-3.0\AceLocale-3.0.xml"/> <Include file="Libs\AceConsole-3.0\AceConsole-3.0.xml"/> <Include file="Libs\AceGUI-3.0\AceGUI-3.0.xml"/> <Include file="Libs\AceConfig-3.0\AceConfig-3.0.xml"/> </Ui> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
-- main.lua - GoldSquish -- @author Etienne Boespflug -- @version 1.0 GoldSquish = LibStub("AceAddon-3.0"):NewAddon("GoldSquish") function GoldSquish:OnInitialize() self:Print("Hello World!") UIErrorsFrame:AddMessage("Hello World!", 1.0, 0.2, 0.2, 10.0, 5.0) end function GoldSquish:OnEnable() -- Called when the addon is enabled. end function GoldSquish:OnDisable() -- Called when the addon is disabled. end |
Il s’agit ici de la base d’un addon sous Ace3. LibStub permet de charger une bibliothèque d’Ace3, en l’occurence AceAddon.
Il y a ensuite trois fonctions principales d’un addon Ace3 :
- OnInitialize est appelé lors du chargement de l’addon (lorsque toutes les dépendances ont été chargées).
- OnEnable est appelé une fois que l’addon est activé.
- OnDisable est appelé lorsque l’addon est désactivé.
Pour le moment, nous nous contentons d’afficher “Hello World!” au joueur. self:Print affiche un message dans la fenêtre de discussion tandis que UIErrorsFrame:AddMessage est un message d’erreur. Pour ceux qui se le demandent, en plus du message, cette fonction prend en argument les paramètres rouge, vert et bleu pour la couleur, l’id du message (qui correspond au type de message, vous trouverez plus d’informations ici) ainsi que la durée d’affichage (en secondes).
Vous pouvez donc d’ores et déjà lancer l’addon depuis le jeu et vous assurer que tout marche bien. Notez qu’il est préférable de n’utiliser que cet addon pour la phase de développement :
Pour modifier la monnaie en jeu, il va nous suffire de modifier les deux variables globales prédéfinies SILVER_PER_GOLD et COPPER_PER_SILVER dans la fonction OnInitialize :
1 2 |
SILVER_PER_GOLD=1 COPPER_PER_SILVER=10 |
Traduction
Avant de passer à la suite, je vous propose de faire les modifications pour la traduction, ça nous évitera d’avoir à tout changer par la suite. La bibliothèque AceLocale permet de gérer la traduction. Nous allons donc créer deux fichiers, enEN.lua et frFR.lua, qui contiendront les chaînes traduites.
Dans chaque fichier, il faut créer l’objet de traduction, que nous appellerons L, à partir de LibStub. Ensuite, il suffit de remplir la table. Ici deux solutions s’offrent à nous :
- utiliser des clefs résumant la chaîne en question et leur donner une valeur pour chaque langue.
- utiliser la chaîne dans la langue principale comme clef (et dans ce cas vous pouvez utiliser la valeur true pour que la valeur soit celle de la clef).
J’ai pour ma part choisi la seconde solution dans un premier temps, même si ce n’est objectivement pas le plus pratique puisque ça implique une modification des clefs lors de la modification de la valeur de la langue principale :
1 2 3 4 5 6 7 8 |
local L = LibStub("AceLocale-3.0"):NewLocale("GoldSquish", "enUS", true) // true signifie ici que c'est la langue par défaut. L["Hello world !"] = true -- Mode L["Squish mode"] = true L["Select the squish mode used to transform the displayed golds."] = true |
1 2 3 4 5 6 7 |
local L = LibStub("AceLocale-3.0"):NewLocale("GoldSquish", "frFR") L["Hello world !"] = "Salut tout le monde !"; -- Mode L["Squish mode"] = "Mode" L["Select the squish mode used to transform the displayed golds."] = "Selectionne le niveau avec lequel la modification de la monnaie est percue." |
C’est dans ces fichiers que vous devez mettre toutes vos chaînes traduites. Vous pouvez les récupérer à nouveau dans main.lua (un nom de variable simple tel que L permet de garder en lisibilité) :
1 2 3 4 5 |
local L = LibStub("AceLocale-3.0"):GetLocale("GoldSquish") function GoldSquish:OnInitialize() self:Print(L["Hello world !"]) end |
Il nous faut également inclure nos fichiers de traduction dans le fichier TOC. Pour cela, je vous conseille de passer par un fichier XML qui référencera toutes vos locales. Placez donc enUS.lua et frFR.lua dans un dossier Locales accompagné du fichier Locales.xml :
1 2 3 4 5 6 7 |
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\FrameXML\UI.xsd"> <Script file="enUS.lua"/> <Script file="frFR.lua"/> </Ui> |
Il suffit alors d’ajouter la ligne suivante dans le fichier TOC :
1 2 3 4 5 6 7 |
## ... includes.xml Locales\Locales.xml main.lua |
Petit détail, il va parfois nous falloir gérer les cas où la chaîne contient une ou plusieurs variables. Dans ce cas, il suffit de passer une lambda prenant des paramètres plutôt qu’une chaîne :
1 2 3 4 5 6 7 8 |
-- enUS.lua L["squishMode_error"] = function(X) return "error: unrecognized squish value " .. X .. "."; end -- utilisation self:Print(L["squishMode_error"](value)) |
Voilà donc pour la traduction. Vous voyez qu’avec Ace3 c’est assez simple. Il nous suffira d’ajouter les chaînes à traduire dans les fichiers au fur et à mesure.
Les options
Une partie importante de l’addon est la fenêtre GUI permettant la modification simple des options. Celle-ci va contenir trois champs :
- La valeur de conversion de l’argent en or (SPG).
- La valeur de conversion du cuivre en argent (CPS).
- Le mode de conversion. Il s’agira d’une énumération de valeurs prédéfinies (x1, x10, x0.01) ainsi que du mode personnalisé (où les valeurs de conversions sont directement spécifiées par l’utilisateur.
Nous allons pour ce faire utiliser AceConfig, qui va simplement générer une fenêtre GUI d’addon en fonction de l’objet d’option qu’on lui passera. Nous allons avoir 5 éléments : les trois présentés ci-dessus ainsi qu’un bouton pour forcer la mise à jour de la monnaie et un second pour rétablir les valeurs par défaut :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
local squishTable = { ["x0.000000001"] = "x0.000000001", ["x0.000001"] = "x0.000001", ["x0.001"] = "x0.001", ["x0.01"] = "x0.01", ["x0.1"] = "x0.1", ["x1"] = "x1", ["x10"] = "x10", ["x100"] = "x100", ["x1000"] = "x1000", ["x1000000"] = "x1000000", ["x1000000000"] = "x1000000000", ["Custom"] = L["Custom"] } local options = { name = "GoldSquish", handler = GoldSquish, type = "group", args = { squishMode = { type = "select", name = L["Squish mode"], desc = L["Select the squish mode used to transform the displayed golds."], style = "dropdown", values = squishTable, get = function() return GoldSquish.db.profile.squishMode end, set = "SetMode", order = 10 }, spg = { type = "input", name = L["Silver per gold"], desc = L["The amount of silver contained in one gold."], usage = L["exemple_value"], set = "SetSPG", get = function(info) return GoldSquish.db.profile.spg end, order = 11 }, cps = { type = "input", name = L["Copper per silver"], desc = L["The amount of copper contained in one silver."], usage = L["exemple_value"], set = "SetCPS", get = function(info) return GoldSquish.db.profile.cps end, order = 12 }, forceUpdate = { type = "execute", name = L["Force update"], desc = L["Force GoldSquish to apply immediatly the changes"], func = function() GoldSquish:UpdateSquish() end, order = 100 }, resetToDefault = { type = "execute", name = L["Reset to default"], desc = L["Reset all GoldSquish's Config to default values."], func = "ResetToDefault", order = 101 } } } |
Vous voyez donc à quoi ressemble cette table, la documentation d’AceConfig vous donnera plus d’informations. Nous allons tout de même détailler un peu ce que ça contient.
Notre groupe contient un premier élément pour le mode. Il utilise le type select, qui correspond à un liste déroulante au niveau GUI. Les différentes valeurs possibles sont passées via la table squishTable. Les deux seconds, cps et spc, sont de type input, c’est-à-dire des simples chaînes de caractères.
Les attributs name, desc et usage sont suffisamment parlant. set et get correspondent respectivement au getter et au setter de l’attribut. Il s’agit soit de lambdas ( function() xxxxx end) soit de noms de fonctions à appeler et que nous allons définir tout de suite après. order est tout simplement l’ordre de chaque élément dans la fenêtre GUI.
Les deux derniers éléments sont des boutons, de type execute. Le paramètre func correspond à la fonction qui sera appelée lorsque le bouton sera déclenché.
Nous allons maintenant définir les différentes fonctions :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
function GoldSquish:SetMode(info, val) if val == "Custom" then -- do nothing elseif val == "x0.000000001" then self.spg = "10000000" self.cps = "1000000" elseif val == "x0.000001" then self.spg = "100000" self.cps = "100000" elseif val == "x0.001" then self.spg = "10000" self.cps = "1000" elseif val == "x0.01" then self.spg = "1000" self.cps = "1000" elseif val == "x0.1" then self.spg = "1000" self.cps = "100" elseif val == "x1" then self.spg = "100" self.cps = "100" elseif val == "x10" then self.spg = "100" self.cps = "10" elseif val == "x100" then self.spg = "10" self.cps = "10" elseif val == "x1000" then self.spg = "10" self.cps = "1" elseif val == "x1000000" then self.spg = "0.1" self.cps = "0.1" elseif val == "x1000000000" then self.db.profile.spg = "0.01" self.db.profile.cps = "0.001" else self:Print(L["squishMode_error"](val)) return end self.db.profile.squishMode = val self:UpdateSquish() end function GoldSquish:SetSPG(info, val) if tonumber(val) ~= nil or val < 0 then self:Print(L["SPG_typeError"](val)) return end self.db.profile.spg = val self.db.profile.squishMode = "Custom" self:UpdateSquish() end function GoldSquish:SetCPS(info, val) if tonumber(val) ~= nil or val < 0 then self:Print(L["CPS_typeError"](val)) return end self.db.profile.cps = val self.db.profile.squishMode = "Custom" self:UpdateSquish() end function GoldSquish:ResetToDefault() self.cps = 100 self.spg = 100 self.squishMode = "x1" self:UpdateSquish() end function GoldSquish:UpdateSquish() self:changeSquish(self.spg, self.cps) end function GoldSquish:changeSquish(spg, cps) SILVER_PER_GOLD=spg COPPER_PER_SILVER=cps end |
La fonction UpdateSquish est appelée dès qu’un attribut est modifié de manière à changer la conversion de la monnaie. Nous allons maintenant modifier la fonction OnInitialize pour initialiser ces variables et pour créer la fenêtre GUI. Nous allons en profiter pour mettre à jour la modification dans les fonctions OnEnable et OnDisable :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
function GoldSquish:OnInitialize() LibStub("AceConfig-3.0"):RegisterOptionsTable("GoldSquish", options) self.optionsFrame = LibStub("AceConfigDialog-3.0"):AddToBlizOptions("GoldSquish", "GoldSquish") self.cps = 100 self.spg = 100 self.squishMode = "x1" end function GoldSquish:OnEnable() self:UpdateSquish() end function GoldSquish:OnDisable() self:NoSquish() end function GoldSquish:NoSquish() SILVER_PER_GOLD=100 COPPER_PER_SILVER=100 end |
Et voilà ! Notre addon a maintenant sa propre GUI, et la modification des paramètres modifie la monnaie en jeu ! Vous pouvez vous amuser à tester en jeu, voici ce que j’obtiens avec l’interface du jeu de base et en langue française :
Sauvegarde entre les sessions
Pour le moment, les valeurs sont réinitialisées dès que l’utilisateur se déconnecte. Nous allons utiliser AceDB pour sauvegarder les paramètres entre les différentes sessions de jeu.
AceDB utilise les SavedVariables du jeu permettant de garder des données d’une session à l’autre. Dans un premier temps, il est nécessaire de mettre à jour le fichier TOC, pour indiquer les variables qui doivent être conservées au travers des sessions : ## SavedVariables: GoldSquishDB.
Nous allons créer une structure contenant les valeurs par défaut de nos variables et faire appel à AceDB pour récupérer notre base de données à l’initialisation de l’addon.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
local defaults = { profile = { cps = "100", spg = "100", squishMode = "x1" }, } function GoldSquish:OnInitialize() self.db = LibStub("AceDB-3.0"):New("GoldSquishDB", defaults, true) LibStub("AceConfig-3.0"):RegisterOptionsTable("GoldSquish", options) self.optionsFrame = LibStub("AceConfigDialog-3.0"):AddToBlizOptions("GoldSquish", "GoldSquish") end |
Vous constatez qu’on n’a plus besoin d’initialiser toutes nos variables puisqu’elles sont automatiquement mises à jour dans self.db. Il va cependant nous falloir mettre à jours toutes les fonctions où nos variables self.cps, self.spg et self.squishMode étaient référencées pour les remplacer par respectivement self.db.profile.cps, self.db.profile.spg et self.db.profile.squishMode.
Lancez l’addon à nouveau, vous devriez constater que les paramètres sont conservés d’une session à l’autre. Notez qu’ici, les données sont contenues dans le champs profile et les paramètres sont donc liés au profil utilisé. AceDB propose également les champs char (personnage), realm (royaume de jeu), local, global, race etc. La documentation d’AceDB liste les différents champs disponibles.
Commande de chat
Avant de nous quitter, je vous propose un dernier ajout à notre addon : la gestion des chat commands.
Ace3 propose d’ajouter simplement des commandes de chat de manière à pourvoir interagir avec l’addon en ligne de commande. Nous allons simplement déclarer nos commandes à l’initialisation et il va falloir redéfinir la fonction GoldSquish:ChatCommand de manière à gérer une commande utilisateur.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
function GoldSquish:OnInitialize() self.db = LibStub("AceDB-3.0"):New("GoldSquishDB", defaults, true) LibStub("AceConfig-3.0"):RegisterOptionsTable("GoldSquish", options) self.optionsFrame = LibStub("AceConfigDialog-3.0"):AddToBlizOptions("GoldSquish", "GoldSquish") self:RegisterChatCommand("goldsquish", "ChatCommand") self:RegisterChatCommand("gs", "ChatCommand") end function GoldSquish:ChatCommand(input) if not input or input:trim() == "" then InterfaceOptionsFrame_OpenToCategory(self.optionsFrame) else LibStub("AceConfigCmd-3.0"):HandleCommand("gs", "GoldSquish", input) end end |
La fonction de gestion de commande est relativement simple. Si la commande est appelée sans argument, on ouvre la fenêtre GUI d’options. Sinon, on transmet les paramètres à AceConfigCmd qui va se charger de la manipulation des paramètres de l’addon via les getter et setter. Plutôt simple non ?
Vous pouvez donc ouvrir la GUI avec la commande /gs et modifier directement les variables : /gs spg 0.001.
Et voilà donc ! Si vous souhaitez plus d’informations sur les addons, je vous conseille de vous tourner vers la documentation d’Ace3 ainsi que les différents sites référencés ci-dessous qui proposent des tutoriaux et des documentations de l’API de World of Warcraft.
Quant à moi, je vous remercie de m’avoir lu et je vous dis à une prochaine fois !
You must log in to post a comment.