Welcome!

Join our community of MMO enthusiasts and game developers! By registering, you'll gain access to discussions on the latest developments in MMO server files and collaborate with like-minded individuals. Join us today and unlock the potential of MMO server development!

Join Today!

[Guide] Understanding OnTemporaryStatSet

Skilled Illusionist
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.
Code:
Buffmask (Always a multiple of 4 bytes)
Buff values, written in the order they appear in the buffmask in binary
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:
PHP:
value = CInPacket::Decode2(iPacket);
buffSourceId = CInPacket::Decode4(iPacket);
buffDuration = CInPacket::Decode4(iPacket);
Each entry in the buffmask has the above set of bytes:
  1. value, which is the amount of buff to give (e.g. +20 WATK)
  2. buffSourceId, the ID of the buff (either an item ID or a skill ID)
  3. buffDuration, which is simply the duration in milliseconds (often 0 if permanent)
There are several buffs that defy this convention -- in many cases, they need a bigger value than what can be shown in a short, so they use an int for value instead of a short. You can find the exact list in IDA, as it varies by version.

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
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:

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
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.
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
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:
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)
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:
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::DecodeForLocal 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:
Initiate Mage
Joined
Sep 11, 2016
Messages
59
Reaction score
81
Re: [Guide] OnTemporaryStatSet (v15x)

The majority of this post is incorrect and that code is entirely incorrect, especially when trying to writing a full and correct buff packet (your words).

You even have IDA comments but don't actually follow IDA. Logic?

PHP:
mplew.writeZeroBytes(12); //Padding. This isn't technically part of the buff packet, but placed here as insurance against possible Error 38s.

This made my day. Thats how you truly know you've fucked up.
 
Last edited:
Skilled Illusionist
Joined
Jul 17, 2010
Messages
333
Reaction score
165
Re: [Guide] OnTemporaryStatSet (v15x)

Here's how 18-19 bytes used in the packet.
Code:
[B][SIZE=4]//SecondaryStat (normal temporary stat) part[/SIZE][/B]

[SIZE=2]//a few buffs here?[/SIZE]

[SIZE=4]mplew.writeShort(0);//mBuffedForSpecMap[/SIZE]

[SIZE=2]//a few buffs here?[/SIZE]

[SIZE=4]mplew.write(0);//nDefenseAtt
mplew.write(0);//nDefenseState
mplew.write(0);//nPVPDamage[/SIZE]

[SIZE=4]//some values for a certain buffs ([U]mostly not contained skillID and duration[/U])[/SIZE]

[SIZE=4]mplew.writeInt(0);//nViperEnergyCharge_[/SIZE]

[SIZE=2]//a few buffs here?[/SIZE]

[SIZE=4][B]//TwoStateTemporaryStat part (e.g. RideVehicle, PartyBooster, etc...)[/B]

[B]//IndieTempStat part (e.g. IndiePad, IndieMad, etc...)[/B][/SIZE]

[SIZE=2]//a few buffs here?  [/SIZE]

[SIZE=4]mplew.writeShort(0);//tDelay *effect delay, mostly used in debuffs
mplew.write(0);//unk
mplew.write(0);//bJustBuffCheck
mplew.write(0);//bFirstSet
if (SecondaryStat.isMovementAffectingStat(stats)) {
    mplew.write(1);//bSN (StatChangedPoint) *buff count
}
mplew.writeInt(0);//unk[/SIZE]
 
Skilled Illusionist
Joined
Jul 16, 2010
Messages
318
Reaction score
116
Re: [Guide] OnTemporaryStatSet (v15x)

The majority of this post is incorrect and that code is entirely incorrect, especially when trying to writing a full and correct buff packet (your words).

You even have IDA comments but don't actually follow IDA. Logic?

PHP:
mplew.writeZeroBytes(12); //Padding. This isn't technically part of the buff packet, but placed here as insurance against possible Error 38s.

This made my day. Thats how you truly know you've fucked up.

Anything actually useful? It's strange that you came just to tell people it was wrong without providing anything correct yourself.
If you can correct me, do so. This isn't a challenge, I'm honestly asking you: Please tell me what is wrong. A guide is no use if it is not correct. My version of the packet was built entirely from packet sniffing, well before the public KMST leak was a thing. I can guess at a few things I might have missed (I think there is an extra byte for one of Dawn Warrior's stances).

I'm also aware the client uses a vastly different structure than the above internally in SecondaryStat::DecodeForLocal, but identical code structure does not preclude a correct packet.

I would like to point out again the topic of this thread, which clearly states: GMS v15x, which is by now nearly 2 years out of date. However, unless you're willing to post your no-doubt updated source public, that is the version a good portion of the community is still below.

I'll grant you the correction to "full and complete", which has since been removed.
 
Last edited:
(O_o(o_O(O_O)o_O)O_o)
Member
Joined
Apr 9, 2009
Messages
1,088
Reaction score
322
Re: [Guide] OnTemporaryStatSet (v15x)

Feelsbadman when your code is wrong.

And you could've just peeked in an old release to see more or less how the encodeForLocal should be (as in, you're missing the actual ordered encode, which is pretty long) by checking the remote version.
http://forum.ragezone.com/f427/leak-v179-packetbakerys-encodeforremote-nexon-1122674/

Just read IDA for the encodeForLocal one (rather similar, just gotta sit though the long list), and then check the givebuff structure which is basically nothing. Super short.

In encodeForLocal you should see a uhh, section for the indie stats. That's where yours is doing stackables. AKA yours is wrong. (The bytes seem in order but "stackable" isn't a think. The flags have names, iirc anything below INDIE_STAT_COUNT 's value is an indie.) Other than that's iirc it's pretty similar to encodeForRemote.
 
Last edited:
Skilled Illusionist
Joined
Jul 16, 2010
Messages
318
Reaction score
116
Re: [Guide] OnTemporaryStatSet (v15x)

Feelsbadman when your code is wrong.

And you could've just peeked in an old release to see more or less how the encodeForLocal should be (as in, you're missing the actual ordered encode, which is pretty long) by checking the remote version.
http://forum.ragezone.com/f427/leak-v179-packetbakerys-encodeforremote-nexon-1122674/

Just read IDA for the encodeForLocal one (rather similar, just gotta sit though the long list), and then check the givebuff structure which is basically nothing. Super short.

In encodeForLocal you should see a uhh, section for the indie stats. That's where yours is doing stackables. AKA yours is wrong. (The bytes seem in order but "stackable" isn't a think. The flags have names, iirc anything below INDIE_STAT_COUNT 's value is an indie.) Other than that's iirc it's pretty similar to encodeForRemote.
Thanks for the feedback! One by one:

The encode is ordered. It is sorted before being written in the call to MapleBuffStat.sortBuffStats(), which handles getting the order of the stats to be written correctly.

I would argue that the serverside implementation of the "stackable" flag doesn't matter. Come runtime, it does not matter if it is set beforehand statically or dynamically calculated based on the position.

My (albeit somewhat limited) understanding of EncodeForRemote was that it was substantially different, too different to be worth trying to shoehorn into EncodeForLocal. For one thing, there isn't a set of default masks in EncodeForLocal

I mean...I could amend this whole thread to just be a paste of the contents of the KMST leak's OnTemporaryStatSet and SecondaryStat::DecodeForLocal, but that wouldn't really help anybody. The point of the guide is to walk beginners through the process of "reading" a buff packet without delving into the enormous block of if-else-if that is SecondaryStat::DecodeForLocal in IDA.

Edited.
 
(O_o(o_O(O_O)o_O)O_o)
Member
Joined
Apr 9, 2009
Messages
1,088
Reaction score
322
Re: [Guide] OnTemporaryStatSet (v15x)

Thanks for the feedback! One by one:

The encode is ordered. It is sorted before being written in the call to MapleBuffStat.sortBuffStats(), which handles getting the order of the stats to be written correctly.

I would argue that the serverside implementation of the "stackable" flag doesn't matter. Come runtime, it does not matter if it is set beforehand statically or dynamically calculated based on the position.

My (albeit somewhat limited) understanding of EncodeForRemote was that it was substantially different, too different to be worth trying to shoehorn into EncodeForLocal. For one thing, there isn't a set of default masks in EncodeForLocal

I mean...I could amend this whole thread to just be a paste of the contents of the KMST leak's OnTemporaryStatSet and SecondaryStat::DecodeForLocal, but that wouldn't really help anybody. The point of the guide is to walk beginners through the process of "reading" a buff packet without delving into the enormous block of if-else-if that is SecondaryStat::DecodeForLocal in IDA.

Edited.

Fair enough. It is pretty similar though. You still have the twostates tsindex in local just as in remote (the specific defaults). Didnt notice the ordering. So that's good to know you do. With the default encode sorted, your nviperenergycharge bytes and stuff all in order, and all the extra flag specific encodes properly added after that (don't think you have those cuz of how short your packet seemed) it should actually be pretty solid. But yeah, don't forget dem twostates.
 
Initiate Mage
Joined
Sep 11, 2016
Messages
59
Reaction score
81
From what I can tell, you may or may not do some sorting of flags. According to your post, you say you do so lets just assume its 100% correct.
Now if you travel further down your code block, immediately after the IsIndie check, you will have SoulMP and SoulFullMP checks with their xOption & rOption.

After that, there is a map with Integer, Boolean as the parameters but you aren't using it.
After that, you have nDefenseAtt, nDefenseState, and nPVPDamage.
(The 2 above things you seem to call a separator, not sure why)

After those 3 bytes, you start with flag conditionals. The order in this matters completely and in your code block, you checked very few and hardcoded the option values. Each option value differs based on skill and level and the large block of conditionals go through many different option values. (b, c, m, n, u, v, w, x, possibly a few more)

When you finish that long block of conditionals, you encode nViperCharge then another block of conditionals but only 3 CTS (BladeStance, DarkSight, Stigma).

Then you do TwoStates, IndieTempStat, and Scouter (Scouter is not in your v15x release).

After that you have the remaining bytes for delay, buffchecks, firstset, movementaffecting, and a few other remaining items.

It's similar to Remote yes, but I wouldn't really judge Local off of Remote.
 

Sen

Initiate Mage
Joined
Dec 5, 2015
Messages
52
Reaction score
5
great tutorial :D
 
Last edited:
Back
Top