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!

[Release] Nexon's TwoStateTemporaryStat Buffs

Custom Title Activated
Loyal Member
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:
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
 
Junior Spellweaver
Joined
Nov 16, 2010
Messages
144
Reaction score
72
wow. thank you Eric . I'm looking/analysing IBD file and still stuck at *mass amounts of randomized ints, writeInt(0)s, and skips*(like you said above) until i see this post <3 you already did it. You saved my time <3 Thank you for very useful release :eek:tt:
 
Newbie Spellweaver
Joined
Jun 15, 2013
Messages
31
Reaction score
0
Anyone know what skill 'Undead' refers to?
 
Junior Spellweaver
Joined
Sep 16, 2017
Messages
156
Reaction score
36
@Eric , really sorry for bumping an almost-year-old thread, though the topic is still the same, so I imagined it would make more sense than creating a new one and linking from here.

I was adapting the code you provided (thank you so much for it!) to my server's Odin-based source, but at the same time I was confronting that with the client v83 functions.

When reaching the virtual function call, if I traced it correctly, this should be what's called for Monster Riding:

Code:
int __thiscall TwoStateTemporaryStat_long_not_equal_long_0__NoExpire_Nothing_long__Nothing_long___::DecodeForClient(int this, int a2)
{
  int v2; // edi@1
  int v3; // esi@1
  int result; // eax@1
  bool v5; // zf@2

  v2 = this;
  v3 = this + 24;
  ZFatalSection::Lock((this + 24));
  result = TemporaryStatBase_long_::DecodeForClient(v2, a2);
  if ( v3 )
  {
    v5 = (*(v3 + 4))-- == 1;
    if ( v5 )
      *v3 = 0;
  }
  return result;
}

Inside DecodeForClient:

Code:
int __thiscall TemporaryStatBase_long_::DecodeForClient(int this, int a2)
{
  int v2; // esi@1
  int v3; // edi@1
  int result; // eax@1
  bool v5; // zf@2

  v2 = this;
  v3 = this + 24;
  ZFatalSection::Lock((this + 24));
  CInPacket::DecodeBuffer(a2, (v2 + 12), 4u);
  CInPacket::DecodeBuffer(a2, (v2 + 16), 4u);
  result = anonymous_namespace_::DecodeTime(a2);
  *(v2 + 20) = result;
  if ( v3 )
  {
    v5 = (*(v3 + 4))-- == 1;
    if ( v5 )
      *v3 = 0;
  }
  return result;
}

Finally, inside DecodeTime:

Code:
int __cdecl anonymous_namespace_::DecodeTime(int a1)
{
  int v1; // esi@1
  int v2; // edi@1
  int v3; // eax@1
  int result; // eax@2

  v1 = (dword_BF060C)();
  v2 = CInPacket::Decode1(a1);
  v3 = CInPacket::Decode4(a1);
  if ( v2 )
    result = v1 - v3;
  else
    result = v1 + v3;
  return result;
}


According to this, we should have 4 + 4 + 1 + 4 packet bytes to decode.

While the first two 4-byte buffers should be the skill ID and the mount ID, I'm not sure about that extra byte before what should be the time.
It basically decides whether the update time should be summed or subtracted from another function I'm not aware the use of.

Mind please enlightening me on whether I'm looking at the right thing or not, and if yes, on what the result of this DecodeTime function should be?

Thanks. c:
 
Custom Title Activated
Loyal Member
Joined
Jan 18, 2010
Messages
3,109
Reaction score
1,139
@Pipotron Correct - the int you usually refer to as time should be the mountID iirc. As for the EncodeTime function, you do have the correct one right there. I didn't realize that I never explained how/what that function was (woops, my bad), so here's my EncodeTime that I used during this release:

PHP:
public void EncodeTime(Long tTime) {
        long tCur = System.currentTimeMillis();
        
        boolean bInterval = false;
        if (tTime >= tCur) {
            tTime -= tCur;
        } else {
            bInterval = true;
            tTime = tCur - tTime;
        }
        tTime /= 1000;//I believe Nexon uses seconds here.
        
        Encode1(bInterval);
        Encode4(tTime.intValue());
    }

I believe I ended up changing what the boolean was used for by manually making it a parameter in future revisions of this, but it all works the same anyways.
 
Junior Spellweaver
Joined
Sep 16, 2017
Messages
156
Reaction score
36
Ohhh, I understand!
So that BF060C call is supposed to be a timeGetTime-like function call, I guess this also explains why it's so widely used.

Thank you c: can't wait to be finally riding ships and hogs without having to summon magic numbers all over the place.
 
Back
Top