- Joined
- Jul 16, 2010
- Messages
- 318
- Reaction score
- 116
Disclaimer: I have not worked with GMS since about version 162. It's entirely possible they tore out all the buff code since then and replaced it with something sane...but I doubt it. Nexon sure loves to cling to their ten-year-plus crapsack code, and buffs should be no different.
This guide is about writing a workable buff packet (workable being the operating term). GIVE_BUFF for those of you that use Odin-based sources; OnTemporaryStatSet for those of you that are fanatics about Nexon names.
There are two distinct formats of buff packet you'll find, depending on which version of the game you're programming for.
Buff packets from versions before the indie_ family of WZ properties was introduced follow a fairly simple format.
We'll start with the buffmask.
Take the buffmask (X amount of bytes) and expand it to binary form. Each binary digit in that buffmask is a 0 or 1, representing a buff effect being either on or off. For ease of manipulation, Odin-based sources divide the buffmask into portions of 32 bits (4 bytes): A position, which is a multiple of 32 bits, and a value, which represents an index into that 32 bits.
I think the client also reads the buffmask only 32 bits at a time as well, but I am not sure. It doesn't make much difference either way; it is only an implementation detail that doesn't change the underlying logic.
Anyway,
Notice I explicitly specified buff effect and not buff skill. The buffmask deals strictly with buff effects, not with individual skills. The difference can be shown as follows:
Suppose I have two commonly known potions: Warrior Potion and Warrior Elixir. Both of these increase Weapon Attack for a short duration. They share the same buff effect (Increase character's weapon attack) but have different buff IDs.
This is an important distinction because the buff mask deals strictly with effects only. Two buffs that give the same buff effect will use the exact same buff mask. In this example, that buff mask is WATK -- or, for those of you into Nexon names, pad.
A character cannot have more than one of any given buff effect at any time. If two buffs both have the WATK effect, one of them will cancel the other. Which one is cancelled and which one stays is dependent on your source, but most sources simply discard the older buff and use the new one only.
But how can a character, then, have multiple different attack-increasing buffs? Surely there can't be just one single source of weapon attack at all times!
Nexon's answer to this is both simple and frustrating: They just used multiple different buff effects. There's WATK (pad), but also ENHANCED_ATK (epad), INDIE_ATK (indiePad), padX, and a whole slew of other skill-specific ones. So in old versions of GMS, whenever Nexon wanted two buffs with the same effect to play nicely with each other, they'd just make a new buff effect that did the exact same thing as the original one and call it a job well done.
That covers the buffmask, which tells us what effects are being applied. The rest of the buff packet deals with everything else needed to construct a buff -- the duration, value, skillid, etc.
Which brings us to the next part of the buff packet: Buff values.
In earlier versions of GMS this is a pretty straightforward thing. For every buff effect in the buffmask, we write the following data after:
Each entry in the buffmask has the above set of bytes:
There are also several buffs that use radically different structures than above. Monster Riding is a good example -- it must encode special information about the mount, and so thus needs a deviation from the standard format. However, a buff packet will always have a valid buff mask, no questions asked. It's only the buff values that can get wonky.
Let's break this down into an example.
Let's say I fire up Wireshark, log onto GMS v149, and use a Warrior Elixir. I get the following buff packet:
This is from version 149, so I know the length of the buff mask: 52 bytes. Expanding the buffmask into Odin format and separating it from the buff values, I get this:
Looking at the buff mask, we have a single buff effect: 0x2000, 12. If my bit math hasn't failed me, that is bit 370, out of 416.
We don't know what the buff mask is yet. However, given I just used a Warrior Elixir, it's a fair bet to say this buff mask increases Weapon Attack -- likely, pad. I check the Item.wz for the Warrior Elixir and confirm the item properties -- yep, it's pad.
Now we know the buffmask. Let's take the rest of the packet, and split it up into its values. We only have a single buffmask -- pad -- so there will only be one set of values:
And suddenly, everything makes sense! We have, in order: The buff value of 12, for a boost of +12 Weapon Attack; a Buff ID of -2002017, which is the buff ID*, and a Buff Duration of 480,000 milliseconds, which works out to 8 minutes.
* Special note: The client uses a negative ID to tell the difference between an Item buff and a Skill buff. A negative ID is item, a positive one is skill. That's why it shows the item ID as a negative number.
The rest of the packet, you'll notice, is all zero bytes. This is where special data would go -- things like Monster Riding's extra mount info. Unfortunately, even I am not familiar with all the things that go in there, so you're on your own if you want to crack that part of the packet. I would love to hear your findings if you do.
Now, to nail in the point, let's apply our new understanding of the buff packet to a more complex buff: Hex of Beholder. In later GMS versions, it is called Hex of Evil Eye.
Notice how there are multiple buff value segments -- one for each buff effect. This is how we write a composite buff consisting of multiple different effects. Combine the buffmask, and then write a block of buff values for each mask.
At this point, stop, take a moment, and let it sink in. If you're working on a version of GMS before professions, you can stop here -- you're done, and the above is pretty much all you need to know. If not, then continue on...
Because I'm about to tell you that everything I just said is incorrect. Or rather, it's incomplete. The above understanding is sufficient for virtually every buff you will find -- only if you are on a GMS version before Professions.
With the introduction of Professions in GMS came the ubiqitous Angelic Blessing Ring. This ring used a type of buffmask never seen before: indiePad. This buff mask, and all the future indie_ buffs that would come after it, does not use the standard structure of "value, id, duration"; it has its whole other thing in place.
The special distinction about this indie_ category of buffs was that they violated one of the previous assumptions: A single buff effect can have only one source, or it will be overwritten. The introduction of indie_ buffs required a rehauling of most of the source's buff handling code, since now a single buff effect could have multiple independent sources in tandem.
Unfortunately, years after the release, modern publicly released sources still don't have the correct structure for this family of buffs. If you try to get two indiePad buffs to play nice with each other in most public servers, it won't work. That is because nobody has really been willing (though I'm sure there are private sources with this fully operational) to tear out so much of the old buff code and rewrite it.
We'll start with the changes to the buffmask.
Each buff mask now has a special flag to indicate if it is from the indie_ family of buffmasks and so requires special treatment in the buff packet. In Odin sources, this is called "stacked". This does not change how the buffmask is written in the buff packet -- it is still a multiple of 4 bytes, written out the same way as before. The list of indie_ buffs is calculated based on the position, and you should match this in your source (or just hardcode it, if that's your thing).
Our old buff packet looked like this:
With the introduction of the indie_ family of buffs, that structure has drastically changed. Loosely, it looks like this today:
Those of you familiar with the nitty-gritty of the OnTemporaryStatSet packet will know more than I do, given that I'm quite outdated by now.
In order to get stackable buffs to work, you first have to start with your source. Change every place that assumes a buffmask can only have one source. For example, MapleCharacter.getStatForBuff() should return a list of found effects, not a single effect. Any EnumMap<> that uses MapleBuffStat values as its key will need to be changed.
Unfortunately, I don't have a complete packet structure for this packet myself. You can look at the KMST leak to figure out exactly how the packet is structured in later versions -- but here's a brief peek:
OnTemporaryStatSet:
In this, SecondaryStat:ecodeForLocal handles most of the logic. It's an enormous block of if statements -- too large to be pasted here. If you're interested in updating this packet for later versions, I would refer you to PacketBakery's EncodeForRemote as a reference, though I should note that there are several changes from EncodeForRemote versus EncodeForLocal.
This guide is about writing a workable buff packet (workable being the operating term). GIVE_BUFF for those of you that use Odin-based sources; OnTemporaryStatSet for those of you that are fanatics about Nexon names.
There are two distinct formats of buff packet you'll find, depending on which version of the game you're programming for.
Buff packets from versions before the indie_ family of WZ properties was introduced follow a fairly simple format.
Code:
Buffmask (Always a multiple of 4 bytes)
Buff values, written in the order they appear in the buffmask in binary
Take the buffmask (X amount of bytes) and expand it to binary form. Each binary digit in that buffmask is a 0 or 1, representing a buff effect being either on or off. For ease of manipulation, Odin-based sources divide the buffmask into portions of 32 bits (4 bytes): A position, which is a multiple of 32 bits, and a value, which represents an index into that 32 bits.
I think the client also reads the buffmask only 32 bits at a time as well, but I am not sure. It doesn't make much difference either way; it is only an implementation detail that doesn't change the underlying logic.
Anyway,
Notice I explicitly specified buff effect and not buff skill. The buffmask deals strictly with buff effects, not with individual skills. The difference can be shown as follows:
Suppose I have two commonly known potions: Warrior Potion and Warrior Elixir. Both of these increase Weapon Attack for a short duration. They share the same buff effect (Increase character's weapon attack) but have different buff IDs.
This is an important distinction because the buff mask deals strictly with effects only. Two buffs that give the same buff effect will use the exact same buff mask. In this example, that buff mask is WATK -- or, for those of you into Nexon names, pad.
A character cannot have more than one of any given buff effect at any time. If two buffs both have the WATK effect, one of them will cancel the other. Which one is cancelled and which one stays is dependent on your source, but most sources simply discard the older buff and use the new one only.
But how can a character, then, have multiple different attack-increasing buffs? Surely there can't be just one single source of weapon attack at all times!
Nexon's answer to this is both simple and frustrating: They just used multiple different buff effects. There's WATK (pad), but also ENHANCED_ATK (epad), INDIE_ATK (indiePad), padX, and a whole slew of other skill-specific ones. So in old versions of GMS, whenever Nexon wanted two buffs with the same effect to play nicely with each other, they'd just make a new buff effect that did the exact same thing as the original one and call it a job well done.
That covers the buffmask, which tells us what effects are being applied. The rest of the buff packet deals with everything else needed to construct a buff -- the duration, value, skillid, etc.
Which brings us to the next part of the buff packet: Buff values.
In earlier versions of GMS this is a pretty straightforward thing. For every buff effect in the buffmask, we write the following data after:
PHP:
value = CInPacket::Decode2(iPacket);
buffSourceId = CInPacket::Decode4(iPacket);
buffDuration = CInPacket::Decode4(iPacket);
- value, which is the amount of buff to give (e.g. +20 WATK)
- buffSourceId, the ID of the buff (either an item ID or a skill ID)
- buffDuration, which is simply the duration in milliseconds (often 0 if permanent)
There are also several buffs that use radically different structures than above. Monster Riding is a good example -- it must encode special information about the mount, and so thus needs a deviation from the standard format. However, a buff packet will always have a valid buff mask, no questions asked. It's only the buff values that can get wonky.
Let's break this down into an example.
Let's say I fire up Wireshark, log onto GMS v149, and use a Warrior Elixir. I get the following buff packet:
Code:
00 00 00 00 00 20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0C 00 9F 73 E1 FF 00 53 07 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
This is from version 149, so I know the length of the buff mask: 52 bytes. Expanding the buffmask into Odin format and separating it from the buff values, I get this:
PHP:
//Buffmask: 13 ints * 4 bytes = 52 bytes (Odin format)
//In Odin format, the last int is position 1, and they count backwards from there.
00 00 00 00
00 20 00 00 //(0x2000, 12)
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
//Rest of the packet.
0C 00 9F 73 E1 FF 00 53 07 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
We don't know what the buff mask is yet. However, given I just used a Warrior Elixir, it's a fair bet to say this buff mask increases Weapon Attack -- likely, pad. I check the Item.wz for the Warrior Elixir and confirm the item properties -- yep, it's pad.
Now we know the buffmask. Let's take the rest of the packet, and split it up into its values. We only have a single buffmask -- pad -- so there will only be one set of values:
PHP:
//Buffmask
00 00 00 00
00 20 00 00 //pad(0x2000, 12)
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
//Buff values
0C 00 //Two bytes: value (12)
9F 73 E1 FF //Four bytes: Buff Id (-2002017)
00 53 07 00 //Four bytes: Duration (480000)
//Rest of buff packet.
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
* Special note: The client uses a negative ID to tell the difference between an Item buff and a Skill buff. A negative ID is item, a positive one is skill. That's why it shows the item ID as a negative number.
The rest of the packet, you'll notice, is all zero bytes. This is where special data would go -- things like Monster Riding's extra mount info. Unfortunately, even I am not familiar with all the things that go in there, so you're on your own if you want to crack that part of the packet. I would love to hear your findings if you do.
Now, to nail in the point, let's apply our new understanding of the buff packet to a more complex buff: Hex of Beholder. In later GMS versions, it is called Hex of Evil Eye.
PHP:
//Buffmask
00 00 00 00
00 03 00 00 //(0x300, 12), which is (0x100, 12) plus (0x200, 12)
00 00 00 00
00 00 00 00
00 00 0B 00 //(0xB, 9), which is (0x8, 9) plus (0x2, 9) plus (0x1, 9)
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
//Buff values
2C 01 //value (300)
13 25 E1 FF //buff id (-2022125)
A0 86 01 00 //duration (100000)
2C 01 //value (300)
13 25 E1 FF //buff id (-2022125)
A0 86 01 00 //duration (100000)
28 00 //value (40)
13 25 E1 FF //buff id (-2022125)
A0 86 01 00 //duration (100000)
F4 01 //value (500)
13 25 E1 FF //buff id (-2022125)
A0 86 01 00 //duration (100000)
F4 01 //value (500)
13 25 E1 FF //buff id (-2022125)
A0 86 01 00 //duration (100000)
//Rest of packet
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
At this point, stop, take a moment, and let it sink in. If you're working on a version of GMS before professions, you can stop here -- you're done, and the above is pretty much all you need to know. If not, then continue on...
Because I'm about to tell you that everything I just said is incorrect. Or rather, it's incomplete. The above understanding is sufficient for virtually every buff you will find -- only if you are on a GMS version before Professions.
With the introduction of Professions in GMS came the ubiqitous Angelic Blessing Ring. This ring used a type of buffmask never seen before: indiePad. This buff mask, and all the future indie_ buffs that would come after it, does not use the standard structure of "value, id, duration"; it has its whole other thing in place.
The special distinction about this indie_ category of buffs was that they violated one of the previous assumptions: A single buff effect can have only one source, or it will be overwritten. The introduction of indie_ buffs required a rehauling of most of the source's buff handling code, since now a single buff effect could have multiple independent sources in tandem.
Unfortunately, years after the release, modern publicly released sources still don't have the correct structure for this family of buffs. If you try to get two indiePad buffs to play nice with each other in most public servers, it won't work. That is because nobody has really been willing (though I'm sure there are private sources with this fully operational) to tear out so much of the old buff code and rewrite it.
We'll start with the changes to the buffmask.
Each buff mask now has a special flag to indicate if it is from the indie_ family of buffmasks and so requires special treatment in the buff packet. In Odin sources, this is called "stacked". This does not change how the buffmask is written in the buff packet -- it is still a multiple of 4 bytes, written out the same way as before. The list of indie_ buffs is calculated based on the position, and you should match this in your source (or just hardcode it, if that's your thing).
Our old buff packet looked like this:
Code:
Buffmask
Buff values
Miscellaneous data
With the introduction of the indie_ family of buffs, that structure has drastically changed. Loosely, it looks like this today:
Code:
Buffmask
Buff values (nonstackable)
Some miscellaneous data (EnergyCharge, TwoStateTemporaryStat, and a few other things)
Stackable buff data (indie_ family of buffmasks)
In order to get stackable buffs to work, you first have to start with your source. Change every place that assumes a buffmask can only have one source. For example, MapleCharacter.getStatForBuff() should return a list of found effects, not a single effect. Any EnumMap<> that uses MapleBuffStat values as its key will need to be changed.
Unfortunately, I don't have a complete packet structure for this packet myself. You can look at the KMST leak to figure out exactly how the packet is structured in later versions -- but here's a brief peek:
OnTemporaryStatSet:
Code:
void __thiscall CWvsContext::OnTemporaryStatSet(CWvsContext *this, CInPacket *iPacket)
{
v2 = this;
v3 = &this->m_secondaryStat;
v195 = &this->m_secondaryStat;
v201 = SecondaryStat::GetVechicleID(&this->m_secondaryStat);
nOldFireBomb = SecondaryStat::_ZtlSecureGet_nFireBomb_(v3);
mElem.vfptr = &ZMap<long,ZRef<SecondaryStat::VIEWELEM>,long>::`vftable';
mElem._m_apTable = 0;
mElem._m_uTableSize = 31;
mElem._m_uCount = 0;
mElem._m_uAutoGrowEvery128 = 100;
mElem._m_uAutoGrowLimit = 24;
v212 = 0;
v4 = SecondaryStat::DecodeForLocal(v3, &result, iPacket, &mElem);
CFlag<480>::CFlag<480>(&uFlagTemp, v4, 0x1E0u);
v198 = CInPacket::DecodeShort(iPacket);
v204 = CInPacket::DecodeByte(iPacket);
bJustBuffCheck = CInPacket::DecodeByte(iPacket);
bFirstSet = CInPacket::DecodeByte(iPacket);
//Rest of function omitted for length
}
In this, SecondaryStat:ecodeForLocal handles most of the logic. It's an enormous block of if statements -- too large to be pasted here. If you're interested in updating this packet for later versions, I would refer you to PacketBakery's EncodeForRemote as a reference, though I should note that there are several changes from EncodeForRemote versus EncodeForLocal.
Last edited: