- Joined
- Jan 18, 2010
- Messages
- 3,109
- Reaction score
- 1,139
Note: This is geared towards Nexon-styled sources and will have to be translated to work with OdinMS.
As of GMS Version 56, Nexon revamped the structure of OnUserEnterField (or spawnPlayerMapobject in Odin), and added forced/defaulted flags on buffs. This portion was added additionally to all TemporaryStat Encoding, which is the small portion of UserEnterField that you'll find fairly similar to OnTemporaryStatSet (or giveBuff in Odin). In order to activate these special defaulted buffs, they must be sent in a specific order, and after your initial buffs.
So, what does it look like in a standard OdinMS source? Well, the TwoStateTemporaryStat area in all public sources is the area with mass amounts of randomized ints, writeInt(0)s, and skips in the middle of your spawnPlayerMapobject packet.. It looks like so:
In lower versions before recent updates after GMS v170, there were only 7 known Two States. Here is the enum for all of them:
From above, you can see that Odin had managed to find one of them -- RideVehicle, or as most of you know it as "MonsterRiding". Most TwoStateTemporaryStat's will follow the basic structure, however we have a few that will differ: PartyBooster, which is Speed Infusion for Buccaneers, and Guided Bullet, which is Gaviota for Corsairs. As for the rest, they are TwoStateTemporaryStat's that either have Dynamic Terms or don't. Monster Riding, for example, is not a Dynamic Term, as it is a permanent buff on your character until your tamed monster gets too hungry and tired.
While we normally handle our buffs in SecondaryStat, we will additionally add a new List to add along with the regular buffs for our Two States. We must also always have the List initialized when SecondaryStat is, and only update the active values. Remember that these are defaults and will always have to be sent.
Alright, now we'll need to cover the classes. Our base class will always be TemporaryStatBase, which the only Two State that uses it will be Energy Charged. Here is TemporaryStatBase:
Next, we have TwoStateTemporaryStat, the one that will cover most of the stats:
Next up is GuidedBullet, that we'll see used for Gaviota.
Last, but not least, is the PartyBooster used for Speed Infusion:
In OnUserEnterField you will encode these buffs inside of a function called EncodeForRemote. In IDA, most of you have probably seen EncodeForRemote and EncodeForLocal. Ever seen or had gotten confused by this part?
If you pay close attention, you see the reference to aTemporaryStat[0] used.
During iteration, you see an inherited stdcall that has parameter using iPacket:
Even I myself had completely ignored it at first, because it never crashed without that extra data. Then I realized why. When we send a buff packet, those flags would actually have to be masked in order to send. The reason it causes issues in OnUserEnterField is because we must send all flags. It is an inherited call so you can't see any real functions pointing to it. So, all of this code block goes right below nDefenseAtt and nDefenseState, like we see here:
In our SecondaryStat's EncodeForRemote at the bottom, we do just that. We can simply iterate all TSIndexes and encode each index from the aTemporaryStat list. All of them have inherited EncodeForClient which will write the necessary packet for each TwoStateTemporaryStat buff.
When we encode in EncodeForLocal for giveBuff, if you loop your buffs, you have to skip all TSIndexes and encode them last.
Here are the common functions Nexon will use for TSIndexes, they're used when cancelling buffs and handled in EncodeForLocal etc.
..aand there we go. In simplified terms we have 7 default buff flags always sent with buff packets and when a user enters the map. They each have their own unique structure and have to be written in order. This is how Nexon does the process, and you can use this to help implement TwoState's into your sources!
Merry Christmas!
- Eric
As of GMS Version 56, Nexon revamped the structure of OnUserEnterField (or spawnPlayerMapobject in Odin), and added forced/defaulted flags on buffs. This portion was added additionally to all TemporaryStat Encoding, which is the small portion of UserEnterField that you'll find fairly similar to OnTemporaryStatSet (or giveBuff in Odin). In order to activate these special defaulted buffs, they must be sent in a specific order, and after your initial buffs.
So, what does it look like in a standard OdinMS source? Well, the TwoStateTemporaryStat area in all public sources is the area with mass amounts of randomized ints, writeInt(0)s, and skips in the middle of your spawnPlayerMapobject packet.. It looks like so:
Code:
int CHAR_MAGIC_SPAWN = Randomizer.nextInt();
mplew.skip(6);
mplew.writeInt(CHAR_MAGIC_SPAWN);
mplew.skip(11);
mplew.writeInt(CHAR_MAGIC_SPAWN);//v74
mplew.skip(11);
mplew.writeInt(CHAR_MAGIC_SPAWN);
mplew.writeShort(0);
mplew.write(0);
final Item mount = chr.getInventory(MapleInventoryType.EQUIPPED).getItem((byte) -18);
if (chr.getBuffedValue(MapleBuffStat.MONSTER_RIDING) != null && mount != null) {
mplew.writeInt(mount.getItemId());
mplew.writeInt(1004);
} else {
mplew.writeLong(0);
}
mplew.writeInt(CHAR_MAGIC_SPAWN);
mplew.skip(9);
mplew.writeInt(CHAR_MAGIC_SPAWN);
mplew.writeShort(0);
mplew.writeInt(0); // actually not 0, why is it 0 then?
mplew.skip(10);
mplew.writeInt(CHAR_MAGIC_SPAWN);
mplew.skip(13);
mplew.writeInt(CHAR_MAGIC_SPAWN);
mplew.writeShort(0);
mplew.write(0);
In lower versions before recent updates after GMS v170, there were only 7 known Two States. Here is the enum for all of them:
PHP:
/*
* This file is part of Development, a MapleStory Emulator Project.
* Copyright (C) 2015 Eric Smith <muffinman75013@yahoo.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
*/
package user.stat;
/**
* TSIndex
* Handles the TemporaryStat indexes for a TwoStateTemporaryStat.
*
* @author Eric
*/
public enum TSIndex {
EnergyCharged(0),
DashSpeed(1),
DashJump(2),
RideVehicle(3),
PartyBooster(4),
GuidedBullet(5),
Undead(6);
private final int index;
private TSIndex(int index) {
this.index = index;
}
public int getIndex() {
return index;
}
}
From above, you can see that Odin had managed to find one of them -- RideVehicle, or as most of you know it as "MonsterRiding". Most TwoStateTemporaryStat's will follow the basic structure, however we have a few that will differ: PartyBooster, which is Speed Infusion for Buccaneers, and Guided Bullet, which is Gaviota for Corsairs. As for the rest, they are TwoStateTemporaryStat's that either have Dynamic Terms or don't. Monster Riding, for example, is not a Dynamic Term, as it is a permanent buff on your character until your tamed monster gets too hungry and tired.
While we normally handle our buffs in SecondaryStat, we will additionally add a new List to add along with the regular buffs for our Two States. We must also always have the List initialized when SecondaryStat is, and only update the active values. Remember that these are defaults and will always have to be sent.
PHP:
public final List<TemporaryStatBase> aTemporaryStat = new ArrayList<>(7);
public SecondaryStat() {
for (TSIndex enIndex : TSIndex.values()) {
if (enIndex == TSIndex.PartyBooster) {
aTemporaryStat.add(new PartyBooster());
} else if (enIndex == TSIndex.GuidedBullet) {
aTemporaryStat.add(new GuidedBullet());
} else if (enIndex == TSIndex.EnergyCharged) {
aTemporaryStat.add(new TemporaryStatBase(true));
} else {
aTemporaryStat.add(new TwoStateTemporaryStat(enIndex != TSIndex.RideVehicle));
}
}
}
Alright, now we'll need to cover the classes. Our base class will always be TemporaryStatBase, which the only Two State that uses it will be Energy Charged. Here is TemporaryStatBase:
PHP:
/*
* This file is part of Development, a MapleStory Emulator Project.
* Copyright (C) 2015 Eric Smith <muffinman75013@yahoo.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
*/
package user.stat;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import network.packet.OutPacket;
/**
* TemporaryStatBase
*
* @author Eric
*/
public class TemporaryStatBase {
public int nOption;//m_value
public int rOption;//m_reason
public long tLastUpdated;
public int usExpireTerm;
public final Lock lock;
public final boolean bDynamicTermSet;
public TemporaryStatBase(boolean bDynamicTermSet) {
this.nOption = 0;
this.rOption = 0;
this.tLastUpdated = System.currentTimeMillis();
this.lock = new ReentrantLock();
this.bDynamicTermSet = bDynamicTermSet;
}
public void EncodeForClient(OutPacket oPacket) {
lock.lock();
try {
oPacket.Encode4(nOption);
oPacket.Encode4(rOption);
oPacket.EncodeTime(tLastUpdated);
if (bDynamicTermSet) {
oPacket.Encode2(usExpireTerm);
}
} finally {
lock.unlock();
}
}
public int GetExpireTerm() {
if (bDynamicTermSet) {
return 1000 * usExpireTerm;
}
return Integer.MAX_VALUE;
}
public int GetMaxValue() {
return 10000;
}
public boolean IsActivated() {
lock.lock();
try {
return nOption >= 10000;
} finally {
lock.unlock();
}
}
public boolean IsExpiredAt(long tCur) {
lock.lock();
try {
if (bDynamicTermSet) {
return GetExpireTerm() > tCur - tLastUpdated;
}
return false;
} finally {
lock.unlock();
}
}
public int GetReason() {
lock.lock();
try {
return rOption;
} finally {
lock.unlock();
}
}
public int GetValue() {
lock.lock();
try {
return nOption;
} finally {
lock.unlock();
}
}
public void Reset() {
nOption = 0;
rOption = 0;
tLastUpdated = System.currentTimeMillis();
}
}
Next, we have TwoStateTemporaryStat, the one that will cover most of the stats:
PHP:
/*
* This file is part of Development, a MapleStory Emulator Project.
* Copyright (C) 2015 Eric Smith <muffinman75013@yahoo.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
*/
package user.stat;
/**
* TwoStateTemporaryStat
*
* @author Eric
*/
public class TwoStateTemporaryStat extends TemporaryStatBase {
public TwoStateTemporaryStat(boolean bDynamicTermSet) {
super(bDynamicTermSet);
}
@Override
public int GetMaxValue() {
return 0;
}
@Override
public boolean IsActivated() {
lock.lock();
try {
return nOption != 0;
} finally {
lock.unlock();
}
}
}
Next up is GuidedBullet, that we'll see used for Gaviota.
PHP:
/*
* This file is part of Development, a MapleStory Emulator Project.
* Copyright (C) 2015 Eric Smith <muffinman75013@yahoo.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
*/
package user.stat;
import constants.SystemConstants;
import network.packet.OutPacket;
/**
* TemporaryStat_GuidedBullet
*
* @author Eric
*/
public class GuidedBullet extends TemporaryStatBase {
public int dwMobID;
public int dwUserID;
public GuidedBullet() {
super(false);
this.dwMobID = 0;
this.dwUserID = 0;
}
@Override
public void EncodeForClient(OutPacket oPacket) {
lock.lock();
try {
super.EncodeForClient(oPacket);
oPacket.Encode4(dwMobID);
if (SystemConstants.Client >= 170) {
oPacket.Encode4(dwUserID);
}
} finally {
lock.unlock();
}
}
public int GetMobID() {
lock.lock();
try {
return dwMobID;
} finally {
lock.unlock();
}
}
public int GetUserID() {
lock.lock();
try {
return dwUserID;
} finally {
lock.unlock();
}
}
@Override
public void Reset() {
super.Reset();
this.dwMobID = 0;
this.dwUserID = 0;
}
}
Last, but not least, is the PartyBooster used for Speed Infusion:
PHP:
/*
* This file is part of Development, a MapleStory Emulator Project.
* Copyright (C) 2015 Eric Smith <muffinman75013@yahoo.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
*/
package user.stat;
import network.packet.OutPacket;
/**
* TemporaryStat_PartyBooster
*
* @author Eric
*/
public class PartyBooster extends TwoStateTemporaryStat {
public int tCurrentTime;
public PartyBooster() {
super(false);
this.tCurrentTime = 0;
this.usExpireTerm = 0;
}
@Override
public void EncodeForClient(OutPacket oPacket) {
lock.lock();
try {
super.EncodeForClient(oPacket);
oPacket.EncodeTime(tCurrentTime);
oPacket.Encode2(usExpireTerm);
} finally {
lock.unlock();
}
}
@Override
public int GetExpireTerm() {
return 1000 * usExpireTerm;
}
@Override
public boolean IsExpiredAt(long tCur) {
lock.lock();
try {
return GetExpireTerm() < tCur - tCurrentTime;
} finally {
lock.unlock();
}
}
@Override
public void Reset() {
super.Reset();
tCurrentTime = 0;
}
}
In OnUserEnterField you will encode these buffs inside of a function called EncodeForRemote. In IDA, most of you have probably seen EncodeForRemote and EncodeForLocal. Ever seen or had gotten confused by this part?
PHP:
v89 = (TemporaryStatBase<long> *)&v3->aTemporaryStat[0].p;
do
{
UINT128::UINT128(&thisa, 1u);
v91 = UINT128::shiftLeft(v90, v88 + 122);
UINT128::UINT128(&value, v91, 0x80u);
v92 = UINT128::operator_(&uFlagTemp, (UINT128 *)&v97, &value);
if ( UINT128::operator bool(v92) )
(*(void (__stdcall **)(CInPacket *))((void (__stdcall **)(_DWORD))v89->baseclass_0.vfptr->__vecDelDtor + 6))(iPacket);
++v88;
v89 = (TemporaryStatBase<long> *)((char *)v89 + 8);
}
while ( v88 < 7 );
If you pay close attention, you see the reference to aTemporaryStat[0] used.
Code:
v89 = (TemporaryStatBase<long> *)&v3->aTemporaryStat[0].p;
During iteration, you see an inherited stdcall that has parameter using iPacket:
Code:
(*(void (__stdcall **)(CInPacket *))((void (__stdcall **)(_DWORD))v89->baseclass_0.vfptr->__vecDelDtor + 6))(iPacket);
Even I myself had completely ignored it at first, because it never crashed without that extra data. Then I realized why. When we send a buff packet, those flags would actually have to be masked in order to send. The reason it causes issues in OnUserEnterField is because we must send all flags. It is an inherited call so you can't see any real functions pointing to it. So, all of this code block goes right below nDefenseAtt and nDefenseState, like we see here:
Code:
v86 = CInPacket::Decode1(iPacket);
v3->_ZtlSecureTear_nDefenseAtt_CS = _ZtlSecureTear<char>(v86, v3->_ZtlSecureTear_nDefenseAtt);
v87 = CInPacket::Decode1(iPacket);
v3->_ZtlSecureTear_nDefenseState_CS = _ZtlSecureTear<char>(v87, v3->_ZtlSecureTear_nDefenseState);
v88 = 0;
v89 = (TemporaryStatBase<long> *)&v3->aTemporaryStat[0].p;
do
{
UINT128::UINT128(&thisa, 1u);
v91 = UINT128::shiftLeft(v90, v88 + 122);
UINT128::UINT128(&value, v91, 0x80u);
v92 = UINT128::operator_(&uFlagTemp, (UINT128 *)&v97, &value);
if ( UINT128::operator bool(v92) )
(*(void (__stdcall **)(CInPacket *))((void (__stdcall **)(_DWORD))v89->baseclass_0.vfptr->__vecDelDtor + 6))(iPacket);
++v88;
v89 = (TemporaryStatBase<long> *)((char *)v89 + 8);
}
while ( v88 < 7 );
In our SecondaryStat's EncodeForRemote at the bottom, we do just that. We can simply iterate all TSIndexes and encode each index from the aTemporaryStat list. All of them have inherited EncodeForClient which will write the necessary packet for each TwoStateTemporaryStat buff.
PHP:
oPacket.Encode1(nDefenseAtt);
oPacket.Encode1(nDefenseState);
for (TSIndex enIndex : TSIndex.values()) {
aTemporaryStat.get(enIndex.getIndex()).EncodeForClient(oPacket);
}
When we encode in EncodeForLocal for giveBuff, if you loop your buffs, you have to skip all TSIndexes and encode them last.
Here are the common functions Nexon will use for TSIndexes, they're used when cancelling buffs and handled in EncodeForLocal etc.
PHP:
public static CharacterTemporaryStat get_CTS_from_TSIndex(int nIdx) {
switch (nIdx) {
case 0:
return CharacterTemporaryStat.EnergyCharged;
case 1:
return CharacterTemporaryStat.DashSpeed;
case 2:
return CharacterTemporaryStat.DashJump;
case 3:
return CharacterTemporaryStat.RideVehicle;
case 4:
return CharacterTemporaryStat.PartyBooster;
case 5:
return CharacterTemporaryStat.GuidedBullet;
case 6:
return CharacterTemporaryStat.Undead;
default: {
return null;
}
}
}
public static int get_TSIndex_from_CTS(CharacterTemporaryStat uFlag) {
for (int i = 0; i < TSIndex.values().length; i++) {
if (get_CTS_from_TSIndex(i) == uFlag)
return i;
}
return -1;
}
public static boolean is_valid_TSIndex(CharacterTemporaryStat uFlag) {
for (int i = 0; i < TSIndex.values().length; i++) {
if (get_CTS_from_TSIndex(i) == uFlag)
return true;
}
return false;
}
..aand there we go. In simplified terms we have 7 default buff flags always sent with buff packets and when a user enters the map. They each have their own unique structure and have to be written in order. This is how Nexon does the process, and you can use this to help implement TwoState's into your sources!
Merry Christmas!
- Eric