Hi there!
Today I want to share how I managed to decode some kind of string logical template from an MMORPG spell's effects.
🤖 The Context
I'm developing a Discord Bot that retrieves data from a given Wakfu equipment. Fortunately, most of the required information is available thought some endpoints available in this forum post. One of them is the effect that an equipment can give according to its "action id".
The problem is this effect description comes with some variables inside the text that need to be decoded.
🐤 Example: easy
Equipment Royal Tofu Helmet from items.json
{
"definition": {
"item": {
"id": 9481,
"level": 18,
// ...
},
"equipEffects": [
{
"effect": {
"definition": {
"id": 184439,
"actionId": 1053,
"areaShape": 32767,
"areaSize": [],
"params": [22, 0]
}
}
},
// ...
]
},
// ...
}
And this is its action from actions.json:
{
"definition": {
"id": 1053,
"effect": "Gain : Maîtrise Distance"
},
"description": {
"fr": "[#1] Maîtrise Distance",
"en": "[#1] Distance Mastery",
"es": "[#1] dominio distancia",
"pt": "[#1] de Domínio de distância"
}
}
The [#1]
code tell us to use the first parameter from the equipment action's definition. However, parameters come in pairs. In this case, the first parameter is a fixed value plus a value that scales with level: params[0] + params[1] * level
⇒ 22 + 0*18
. So [#1]
= 22
.
So the description would be 22 Distance Mastery
Pretty simple so far.
🎃 Example: medium
Let's take a look on another example:
Equipment Gelano
"definition": {
"id": 127211,
"actionId": 1068,
"areaShape": 32767,
"areaSize": [],
"params": [
30,
0,
3,
0
]
}
{
"definition": {
"id": 1068,
"effect": "Gain : Maîtrise Élémentaire dans un nombre variable d'éléments"
},
"description": {
"fr": "{[~3]?[#1] Maîtrise [#3]:[#1] Maîtrise sur [#2] élément{[>2]?s:} aléatoire{[>2]?s:}}",
"en": "{[~3]?[#1] Mastery [#3]:[#1] Mastery of [#2] random{[=2]?:} element{[=2]?:s}}",
"es": "{[~3]?[#1] Dominio[#3]:[#1] Dominio de [#2] elemento{[>2]?s:} aleatorio{[>2]?s:}}",
"pt": "{[~3]?[#1] Domínio[#3]:[#1] Domínio sobre [#2] elemento{[>2]?s:} aleatório{[>2]?s:}}"
}
}
Now we not only have [#2]
and [#3]
, but a [~3]
and [>2]
as well.
By giving it a look, we can identify some condition expressions in the format {<condition>?<valueIfTrue>:<else>}
.
Clearly, the expression {[>2]?s:}
is there to give plural to words when something is higher than two.
Using the same logic, the entire expression checks for a condition to print [#1] Mastery [#3]
or [#1] Mastery of [#2] random{[=2]?:} element{[=2]?:s}
.
To understand what these symbols mean, we can check out the discoveries that 0M1N0U5 kindly shared in the game's forums.
Operators +, -, ~ to check the number of parameters.
<, >, = with two behaviours:If it comes with a parameter on the left [2=3], I use it as a constant compared to the parameter on the right using the operator that is naturally between both.
Every time I solve a calculated parameter, it is stacked so I can use it later.
If it comes without a number on the left, then I read the value from the top of the stack and use it as a value and in the same way as mentioned above.
We believe that [~3] checks if the number of arguments is at least three.
By checking equipment's params we can see that it has two arguments (four values), so it evaluates to its else
value.
Cool, now we have this {[=2]?:s}
which is likely to be a bug, since other languages uses {[>2]?s:}
.
The key here is that the [>2]
condition refers to the last evaluated parameter.
So in the expression {[>2]?s:}
we're checking if [#2]
is bigger than two (or equal if going with the English description).
Here's how the Spanish expression could be converted to some javascript code:
let stack = 0
const hasThreeOrMoreArguments = params.length >= 6 // [~3]
const firstParam = () => { // [#1]
const value = params[0] + params[1] * level
stack = value
return value
}
const secondParam = () => { // [#2]
const value = params[2] + params[3] * level
stack = value
return value
}
const thirdParam = () => { // [#3]
const value = params[4] + params[5] * level
stack = value
return value
}
const isLastStackValueGreatherThanTwo = () => stack > 2 // [>2]
const plural = () => isLastStackValueGreatherThanTwo() ? 's' : '' // [>2]?s:
// {[~3]?[#1] Dominio[#3]:[#1] Dominio de [#2] elemento{[>2]?s:} aleatorio{[>2]?s:}}
const description = `${hasThreeOrMoreArguments ?
`${firstParam()} Dominio${thirdParam()}`
:
`${firstParam()} Dominio de ${secondParam()} elemento${plural()}} aleatorio${plural()}`
}`
The description for this equipment would be 30 Dominio de 3 elementos aleatorios
What's odd in here is that equipment with three or more arguments would have something like 30 Dominio1
as description. However, there's no single equipment that satisfies this condition.
So far, so good.
🐲 Example: boss
Now we can check the boss example: "Gray Mage's Wand"
{
"definition": {
"item": {
"id": 23189,
"level": 109,
// ...
},
"useEffects": [
{
"effect": {
"definition": {
"id": 212575,
"actionId": 1084,
"areaShape": 32767,
"areaSize": 1,
"params": [
2.4,
0.201
]
}
}
},
// ...
]
},
// ...
}
{
"definition": {
"id": 1084,
"effect": "Soin : Lumière"
},
"description": {
"fr": "Soin [el6] : [#1]{[+3]?% des PV:}{[+3]?{[1=3]? max:{[2=3]? courants:{[3=3]? manquants:{[4=3]? max:{[5=3]? courants:{[6=3]? manquants:}}}}}}:}{[+3]?{[4<3]? du lanceur:{[7<3]? de la cible:}}:}{[-2]?{[0=2]? [ecnbi] [ecnbr]:}:}{[+2]?{[2=2]? [ecnbi]:}:}{[+2]?{[1=2]? [ecnbr]:}:}",
"en": "[el6] Heal: [#1]{[+3]?% of HP:}{[+3]?{[1=3]? max:{[2=3]? current:{[3=3]? lost:{[4=3]? max:{[5=3]? current:{[6=3]? lost:}}}}}}:}{[+3]?{[4<3]? of the caster:{[7<3]? of the target:}}:}{[-2]?{[0=2]? [ecnbi] [ecnbr]:}:}{[+2]?{[2=2]? [ecnbi]:}:}{[+2]?{[1=2]? [ecnbr]:}:}",
"es": "Cura [el6]: [#1]{[+3]?% de los PdV:}{[+3]?{[1=3]? máx.:{[2=3]? actuales:{[3=3]? faltantes:{[4=3]? máx.:{[5=3]? actuales:{[6=3]? faltantes:}}}}}}:}{[+3]?{[4<3]? del lanzador:{[7<3]? del objetivo:}}:}{[-2]?{[0=2]? [ecnbi] [ecnbr]:}:}{[+2]?{[2=2]? [ecnbi]:}:}{[+2]?{[1=2]? [ecnbr]:}:}",
"pt": "Cura [el6]: [#1]{[+3]?% dos PV:}{[+3]?{[1=3]? máx.:{[2=3]? atuais:{[3=3]? perdidos:{[4=3]? máx.:{[5=3]? atuais:{[6=3]? perdidos:}}}}}}:}{[+3]?{[4<3]? do lançador:{[7<3]? do alvo:}}:}{[-2]?{[0=2]?[ecnbi] [ecnbr]:}:}{[+2]?{[2=2]? [ecnbi]:}:}{[+2]?{[1=2]? [ecnbr]:}:}"
}
},
This might look crazy, but thanks to 0M1N0U5 we have all the information we need to solve it.
It basically reads as:
"Heals a given amount. If there are more than three arguments, it heals a percentage of HP. If the third argument is equal to one, it's about max HP. If equals to two, it's about current HP. If three, lost HP" and so on. The [el6]
tag means "Light Element" and [ecnbi]/[ecnbr]
some icon that I'm not sure what is.
And as the previous example, there's no equipment with three or more arguments, so the description ends up as [el6] Heal: 24
💻 The Code
Now that we've reached this far, we should be able to code some generic way to evaluate these expressions.
The strategy I've followed was to swap all conditions structures to javascript conditions ternary expressions inside string literals.
So {[>2]?s:}
becomes
`${ stack > 2 ? 's' : '' }`
for example.
In a similar way, I pre-calculate parameters values and swap [#1]
to
`${ stack = value }`
so the returned value becomes the value and stack value is updated.
I think it's easier to just show the code:
You can notice that some action ids require different calculation for its paremeters, even a hardcoding for Makabrafire equipment itself.
This gist is a replica from the parseEffect.js file from araknomecha-scrapper, a project that gathers wakfu data to build and provide information to Corvo Astral, the discord bot I've mentioned in the beginning of this article.
Here's the test file so you can check the results of this parsing and maybe tweak it yourself.
📜 The Conclusion
By checking all descriptions from actions.json, we could actually create a custom parser for each of them, specially if we don't take in account the cases where no equipment whatsoever falls into a given condition as mentioned earlier.
However, understanding the logic and implementing a parser for these codifications was challenging enough to be worth the time.
This post content is preeety specific and might help just a few number of people, but it's a cool knowledge to share ;D
A big thanks to 0M1N0U5 for sharing what they've discovered on the forums!