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!

Fixing Ninja Ambush Skill in MoopleDev

Newbie Spellweaver
Joined
Aug 27, 2016
Messages
91
Reaction score
1
Hi guys, its been a while. Finally got some free time and want to get back into MapleDev. A few years now? I was talking to ERic how the structure of Odin work and it helped a great deal however I still dont understand everything and still will like to learn.

So in MoopleDev the Ninja Ambush skill I believe doesnt work properly as:

1. It does 1 damage or it misses
2. It does do it periodically as its states.
I tracked it down being in the Special Move Handler with the packet:
Code:
5B 00 5A BF 03 00 AC E1 3E 00 1E 01 74 00 00 00 E8 03

After this I dont know what to do.

I tried using a buff that does work in the game to get more familiar with it but it left more questions then answers. For example using MapleWarrior. So here I think packets work in general in regards to Buffs.

Maple Warrior Packet:

Code:
5B 00 7E 02 06 00 A8 E1 3E 00 1E 52 01 4F 01 80 E8 03

1. The client invokes the method messageReceived(IOsession session, Object message)
Where it grabs the packets and parses it knowing which class / handler to invoke to.
Code:
Grabbing Packet: SeekableLittleEndianAccessor slea = newGenericSeekableLittleEndianAccessor(new ByteArrayByteStream(content));

Storing Packet Header: short packetId = slea.readShort();

Identifying which handler: final MaplePacketHandler packetHandler = processor.getHandler(packetId);

Invoking the respective handler: packetHandler.handlePacket(slea, client);

2. It goes into its respective handler class (SpecialMoveHandler):

Code:
Stores your maple Character: MapleCharacter chr = c.getPlayer();

No idea what it does but it reads 4 bytes from the packet?: chr.getAutobanManager().setTimestamp(4, slea.readInt());

Get the skill id from the packet: int skillid = slea.readInt();
Get skill level from Packet: int __skillLevel = slea.readByte();
Creates a Skill object: Skill skill = SkillFactory.getSkill(skillid);

Gets the effect: MapleStatEffect effect = skill.getEffect(skillLevel);

Then Apply the effect ?: skill.getEffect(skillLevel).applyTo(c.getPlayer(), pos);

What I am confused about is that it pops out of the messageRecieved method and then where does it go from there? Doesnt the server need to send the new stats to the client? Which class is that?

Also as you can see I wouldnt know where to start with ninja ambush as its a periodic and does damage buff?

Last thing, when you are reading bytes or shorts from the packet does it keep track for example, if I said slea.readByte() and my packet was lets say 5B 00 30 40 50 30 it would read 5B right? then if I did it again would it read 00 ? and if i did sea.readInt() would it read 0x30 50 40 30?
 
Last edited:
Junior Spellweaver
Joined
Sep 16, 2017
Messages
156
Reaction score
36
I'll try to guide you to where the issue potentially is; from there, with tests it should be simple to have a fix for it.

What I am confused about is that it pops out of the messageRecieved method and then where does it go from there? Doesnt the server need to send the new stats to the client? Which class is that?

Once the message from the client reaches messageReceived, it is first converted into an array of bytes, and the first two bytes are read: they correspond to the "opcode", aka a unique identifier for that specific type of action (life movement, skill usage, etc etc; the list is in RecvOpcode).

Each opcode is assigned to a handler. Handlers are stored in an array, where the index of each handler is equal to the opcode value; for example, SpecialMoveHandler will be stored at handlers[91], because the opcode for SPECIAL_MOVE is 0x5B = 91. See the function that takes care of storing all the handlers, and the opcode value for SPECIAL_MOVE.

Now, the messageReceived function takes care of selecting the proper handler from the array, and of invoking its handlePacket function. At this point, we're already in SpecialMoveHandler.java.

In there, depending on the skill that's being invoked, the behavior will differ, although most skills are handled in MapleStatEffect.java, as you already noted.

Also as you can see I wouldnt know where to start with ninja ambush as its a periodic and does damage buff?

Last thing, when you are reading bytes or shorts from the packet does it keep track for example, if I said slea.readByte() and my packet was lets say 5B 00 30 40 50 30 it would read 5B right? then if I did it again would it read 00 ? and if i did sea.readInt() would it read 0x30 50 40 30?
Calling read functions of that slea object takes care of both reading the requested amount of bytes, and of moving the current reading position to the first unread byte.
When you're reading an opcode, you always read it as a short (because the maximum value an opcode can reach exceeds 255), so you read "5B 00" together; being a LittleEndianAccessor, however, reverts that into "00 5B".

With that in mind, let's now follow the whole SpecialMoveHandler.java for Ninja Ambush. Note that I will refer to .
We will consider the packet you provided:
5B 00 5A BF 03 00 AC E1 3E 00 1E 01 74 00 00 00 E8 03

The opcode 0x5B has already been read as a short, in messageReceived:
[[strike]5B 00[/strike]] 5A BF 03 00 AC E1 3E 00 1E 01 74 00 00 00 E8 03

Now, the first thing we note is a readInt(), that is the timestamp of the skill usage, to be used in comparison with the previously-recorded timestamp, against potential skill spamming hacks and the likes. Won't be explaining this in-depth as it's not what we're looking for, currently.
[[strike]5B 00[/strike]] [[strike]5A BF 03 00[/strike]] AC E1 3E 00 1E 01 74 00 00 00 E8 03

Now, we have a readInt() for the skill ID, followed by a readByte() for the skill level:
readInt() = AC E1 3E 00 -> (following the little endian convention) 00 3E E1 AC.
If we convert 0x3EE1AC to decimal we obtain 4121004, which is the skill ID for Night Lord's Ninja Ambush.
readByte() = 1E , that converts to 30.
So we're dealing with a NL's Ninja Ambush at level 30.
[[strike]5B 00[/strike]] [[strike]5A BF 03 00[/strike]] [[strike]AC E1 3E 00[/strike]] [[strike]1E[/strike]] 01 74 00 00 00 E8 03

We reach "MapleStatEffect effect = skill.getEffect(skillLevel);", which gives us the effect object that we'll be needing from now on.

Ninja Ambush does not have a cooldown, so the cooldown check right after that is skipped completely. Same goes for the next skill checks (Monster Magnet, Time Leap...).

slea.available() returns the number of bytes still not read from the stream. In our case they're 7, so we don't get any position data.

The remaining bytes are not actually used, because the way Odin treats skill usage is not proper.
To give an idea, since the skill is a monster-affecting effect, the next data you have should be a byte for the number of monsters affected (1), followed by an int for the object id of the monster ("74 00 00 00" = 0x74 = 116; the object id is obviously not the monster id that you'd find in Mob.wz, but it's a unique identifier for a specific object in your map - in this case, the monster you hit with Ambush), and by a short for the skill delay (0x3E8 = 1000 milliseconds before a skill recast should be accepted).

We're finally arrived at the effect.applyTo function call: this is in .

calcMPChange takes care of registering the MP consumption of the skill. Starting at line 651 of the file, the player's MP is recalculated and then updated via packet (line 659).

All that's left now is dealing with the effect on the monster.
We can ignore all the subsequent checks (Shadow Claw, Summon), until we reach line 706: that's where the monster effect is handled.
We follow applyMonsterBuff to line 775, where the monster(s) affected are checked for and a status is applied to each. The function monster.applyStatus brings us to .
I highlighted the line of that function that we're interested in: that's where the damage for Ninja Ambush is calculated.

I'm not sure what's wrong with the skill yet as I haven't tested anything about it, but if something doesn't work right, then your best bet is to place some test output in there, and let us know what results you get.
 
Upvote 0
Newbie Spellweaver
Joined
Aug 27, 2016
Messages
91
Reaction score
1
Thank you so much for the in-depth post, looks like I wasn't far off. Ill for sure investigate in those regions. You really help to put things in perspective.

Couple of questions.

1. Using skills in the client sends to server correct? Then the damage is then sent back to the client from the server?
2. Are animations taken care of client side?
3. This might fix Ninja Ambush damage how about the periodic status it has, Ninja ambush keeps going until duration I guess?

Edit: So I did some digging and the damage is looking good, I am not sure where its entirely going wrong might be how the packet is being sent back to the client? But then again I still dont understand how odin works and what you have to code for essentially, like how are skills processed
 
Last edited:
Upvote 0
Newbie Spellweaver
Joined
Aug 27, 2016
Messages
91
Reaction score
1
Thank you so much for the in-depth post, looks like I wasn't far off. Ill for sure investigate in those regions. You really help to put things in perspective.

Couple of questions.

1. Using skills in the client sends to server correct? Then the damage is then sent back to the client from the server?
2. Are animations taken care of client side?
3. This might fix Ninja Ambush damage how about the periodic status it has, Ninja ambush keeps going until duration I guess?

Edit: So I did some digging and the damage is looking good, I am not sure where its entirely going wrong might be how the packet is being sent back to the client? But then again I still dont understand how odin works and what you have to code for essentially, like how are skills processed

Tried everything to my knowledge to change the damage but I wasnt successful still hitting 7's and missing.
 
Upvote 0
Junior Spellweaver
Joined
Sep 16, 2017
Messages
156
Reaction score
36
When you attack a monster with normal attacks (regular attack, physical or magic damaging skill), the damage is calculated in the client, and then sent to the server in order to be broadcasted to other players in the map, and to perform other operations such as dealing with the monster's hp loss and stuff like that.
Damaging effects, on the other hand, are applied only when the server sends back a packet with the respective monster status data.

To put it simply, this is how the system works:

CLIENT SIDE

  • You use a skill in the client.
  • The client checks what type of skill we're dealing with.
    • If it's an attack, the damage is calculated and shown in first person, then sent to the server.
    • If it's a damaging effect, then information about it (skill id, level, affected mobs, etc) is sent to the server, without any damage calculation performed beforehand.

SERVER SIDE
  • Attacks are handled in different packets depending on the type; for example, CloseRangeDamageHandler, MagicDamageHandler and RangedAttackHandler.
    • The damage is recalculated and broadcasted to the map.
  • Effects are instead handled in SpecialMoveHandler, as we already saw.
    • Damage-over-time effects are a type of monster "buff", along with stuff such as monster self-powerup or defenseup (the icons you see over the monster's head).
    • Information about these effects is loaded by the server at startup ( ). You can see an enum of all the available effects.
    • A packet with information about the status is then sent back to the client, in order to display the damage and the status animation.



Have you tried setting a chat message that would display the damage that the server passes to the client, just to see what number is getting passed?

I'm talking about something along the lines of this, to be added after the Ninja Ambush damage is calculated in MapleMonster.java:
PHP:
map.broadcastMessage(MaplePacketCreator.serverNotice(6, "Damage: " + damage));
, that will show you the damage that Ninja Ambush should be dealing.
This way, we can check whether the result of the calculation is actually 1 or not.
 
Upvote 0
Newbie Spellweaver
Joined
Aug 27, 2016
Messages
91
Reaction score
1
See, this is the things I should know about but where do you learn things like that?

Anyways I got updates for you.

1. Using your code I verified that the right damage is being sent back to the client, however isn't obvious it would be the same damage for example. Wouldnt it always return the right damage as its in the same scope?

2. The code in blue is the cause why Ninja ambush isn't working properly. Its creating a new task(thread) that ends the skill prematurely and rewrites the damage. When commented out the skill animation (the lingering affect) works and the damage now is proper, however if I set the damage higher than 32767(same as stats) it does some funky stuff for example it damages the enemy and the number appear is blue 600 like mana or something? The current damage output-ing back to the client is 19660200.

The problem I have with this ordeal is what does the function means how are things taken care of in odin etc. I got as far as what my original post is but after this like what is setDamageScchedule or Damagetask... How do you know to sent the packet back from sever to client. How do you know that specialMovehandler doesnt calculate damage in the client and much more
Code:
else if (status.getSkill().getId() == 4121004 || status.getSkill().getId() == 4221004) {
final Skill skill = SkillFactory.getSkill(status.getSkill().getId());            
final byte level = from.getSkillLevel(skill);           
final int [COLOR=#ff0000]damage[/COLOR] = (int) ((from.getStr() + from.getLuk()) * (1.5 + (level * 0.05)) * skill.getEffect(level).getDamage());                    

status.setValue(MonsterStatus.NINJA_AMBUSH, (damage));           
[COLOR=#0000ff]status.setDamageSchedule(timerManager.register(new DamageTask(damage, from, status,cancelTask, 2), 100000, 1000));  [/COLOR]        

 map.broadcastMessage(MaplePacketCreator.serverNotice(6, "Damage: " + [COLOR=#ff0000]damage[/COLOR]));         
}
 
Last edited:
Upvote 0
Junior Spellweaver
Joined
Sep 16, 2017
Messages
156
Reaction score
36
1. Using your code I verified that the right damage is being sent back to the client, however isn't obvious it would be the same damage for example. Wouldnt it always return the right damage as its in the same scope?
I wanted to make sure that the value was calculated correctly, as this would exclude issues with incorrect serverside files (skill.getEffect(level).getDamage() relies on the skill info stored in the xml, for example). If the value was calculated as 1 from the start, that would have been our issue.
I know it sounds rather silly, but believe me, you really want to have tested against any possible and "impossible" error, in these cases. c:
2. The code in blue is the cause why Ninja ambush isn't working properly. Its creating a new task(thread) that ends the skill prematurely and rewrites the damage. When commented out the skill animation (the lingering affect) works and the damage now is proper[...]
status.setDamageSchedule(timerManager.register(new DamageTask(damage, from, status,cancelTask, 2), 100000, 1000));

This sounds strange, for a couple of reasons:
1) The line in blue only deals with the serverside damage to the monster, but not to the numbers displayed in the single damage ticks.
2) That schedule is apparently set to run for 100 seconds, for a reason I'm not sure of.

Out of curiosity; if you comment out that line, and then hit a monster with Ninja Ambush for the full duration with correct numbers, does the monster's HP actually decrease?


[...] if I set the damage higher than 32767(same as stats) it does some funky stuff for example it damages the enemy and the number appear is blue 600 like mana or something? The current damage output-ing back to the client is 19660200.
The damage value is registered as an Integer, but the client requires a short for it. 's the relevant packet that passes status activation to the client from the server.

The number you set as damage is passed as argument of writeShort(stat.getValue()).
As we can see from for writeShort(data), the rightmost two bytes of your number are written to the stream in order to form a little-endian short.
If your number was 19660200, which in binary is 0000 0000 0010 1011 1111 1101 1010 1000, what you obtain after the casting to two bytes (which would equal to truncating the rest out) is 1111 1101 1010 1000 , which is -600 if read as a short.
The client reads negative damage values as healing values; thus, the monster is actually getting healed for 600 HP (which is displayed by blue numbers, instead of orange).


Tomorrow I'll add a bit more about how to find out this stuff yourself and how to know about packets, but it's kinda late here so I'll end this post here for today, sorry c:
 
Upvote 0
Newbie Spellweaver
Joined
Aug 27, 2016
Messages
91
Reaction score
1
appreciate the help you have given me. Thank you.

See you have to teach me that, I didnt know theres two ways of taking care of the damage.

I believe the monster Hp decrease I think? Maybe not it never showed the HP Bar regardless

Thats interesting so maybe a if statement should be thrown (conditional check) there as the max damage can only be 65536 then or 2^16-1 ?

Looking forward to hearing you soon
 
Last edited:
Upvote 0
Junior Spellweaver
Joined
Sep 16, 2017
Messages
156
Reaction score
36
I'm alive ~

So, as a first thing; as cliché as it may sound, the best way to get a better understanding on the way Maple works internally is experience, investing time on trial and error.

Your best plan of action would be picking a simple interaction, that you already have a rough idea on how it's supposed to work, and study its flow between client and server: an example could be trying to track the code that a Super Megaphone has to follow in order to be successfully broadcasted.

But here, I'll simply try to answer some of your questions to see if I can put you on the right track.

How does Odin-like code process packets?
What do Tasks and Schedules do in an Odin Maple server?


We already had a brief talk about the route that packets follow once they reach the server, so we can make assumptions based on that.

A couple of general points to remember:


  • The reason we use packets in the first place is because we need to keep all the data of all clients synchronized with one central server.



  • Data relative to player/monster/object stats (such as HP, MP, mesos, and other values) are stored in your server; the client simply asks for them and uses the data received to display.


First thing first, a few words about packets in general.
Packets are of two types: client>server, which is a "received" packet (note: we're using the server's point of view. Client>server packet is received by the server), and server>client, which is a "sent" packet.

Each packet has its own "opcode", which is a unique identifier used to know what kind of action we're dealing with.These opcodes are shorts, passed in the first two bytes of each single packet. In the server, they're listed in two enum classes: for received packets, and for sent packets.

Every user-generated action that needs to be broadcasted will (well, obviously, but still) generate in the user client.
A packet will be created by the client, sent to the server, which will then process it with the appropriate packet processor ( the list of processors that correspond to each packet type. Each processor is its own java class).

If a packet requires any data to be broadcasted back to other players, then one or more to-be-sent packets will be generated. These packets are created via calls to the static functions of .
As we already saw while analyzing Ninja Ambush, to find out the exact process of a packet that's sent to the server you'll need to study its route from messageReceived, and work your way from there.


Now, when you use a skill, you will go through certain stat alterations, such as a reduction of your MP (if it's a skill that consumes mana), an activation/deactivation of a temporary stat (if it's a skill that generates one), an application of damage (again if it's a skill that causes damage).

Let's once again take Ninja Ambush as an example.

The skill costs a set amount of MP, and we already saw where this is taken into account. We also noticed that this MP value is updated in MapleStatEffect, and re-sent to the client via a new packet.

The skill's effect is to activate a specific monster's "temporary stat", called "Ambush".
Temporary stats (both and ) are special stat alterations activated by skills or items, that last only for a finite amount of time. Stuns, seals, poison are all examples of temporary stats applied on monsters: they're distinguishable by a unique animation affecting the monster itself. Ambush's animation is that of the little ninjas hitting the monster from left and right.

Much like the MP change, the application of this effect is communicated to the client(s) via a packet broadcast.
However, keep in mind that what you communicate at this point is only the visual effect of the temporary stat: the little ninjas with the damage number aren't actually dealing damage to the monster.

We mentioned that the monster's health value is stored in the server, like all the other stats. In order to update this value, we need to first change it in the server, and then broadcast the new value to the client.

When I asked you whether the damage was actually getting applied, after commenting out the poisonTask line, it was exactly because of this: the poison task takes care of recalculating the damage after each tick of the animation (every second), of updating the monster's little HP bar, and of checking whether the monster has reached the minimum HP threshold (the skill stops hitting if the monster reaches 1 HP if I'm not wrong). I'll explain more about the tasks and schedules later, though.

If you don't have a packet communicating the actual stats updates, all you have are visual animations and effects. You can verify this easily by suddenly stopping your internet connection while you're ingame: you can still cast skills and see their basic effect (like, the Haste animation, or the Ninja Ambush first animation), but you won't see the effects that require a packet from the server (the actual speed increase for Haste, or the skill effect + damage on the monster for Ambush).

~

Now, for Tasks and Schedules.
Certain actions need to be delayed or repeated, much like the damage processing for Ninja Ambush dot.
A schedule does exactly this: it sets a specific action to begin at a certain time.
A task is what you can schedule. It's an object that contains a runnable function, that will run whenever activated at the scheduled time.

Code:
status.setDamageSchedule(timerManager.register(new DamageTask(damage, from, status,cancelTask, 2), 2000, 1000));
In this relevant example, we're creating a object, which contains a run() function that deals with the server-side damage application, and monster's HP update communication to the client.
This DamageTask object also includes another task, "cancelTask"; this is another runnable, that is called whenever the damageTask needs to be stopped (for example in case the monster reached the minimum HP threshold, as you can see in line 765).
The DamageTask is registered: this means that it will be repeated every 2000 milliseconds, with an initial delay of 1000 milliseconds.

Hopefully this cleared a bit of your doubts, but feel free to ask should you have more. c:
 
Last edited:
Upvote 0
Newbie Spellweaver
Joined
Aug 27, 2016
Messages
91
Reaction score
1
I cant thank you enough for this, really do appreciate it and it did put a lot of things in perspective. Glad youre alive ahaha was worried for a second

But obviously knowing me I got a few questions.

1. You mentioned that "If you don't have a packet communicating the actual stats updates" How would one sent packets back to the client to update its stats?

2. Even if I use Dragon Roar(W.e that dark Knight stkill is) the client still needs to read the damage from the server? I guess what I am trying to say is that for those handlers (close/range) The damage isn't client side right? As you mentioned
"If it's an attack, the damage is calculated and shown in first person, then sent to the server."

3. How does the client know to invoke the right animation? For example Even before this Ninja ambush startup will show (the ninja dashing left and right and then hiding) What shows like you said the ninja going left and right left and right doing damage?

4. And the damage, why is it reading its healing a monster? and Truncating so much bits?
 
Upvote 0
Junior Spellweaver
Joined
Sep 16, 2017
Messages
156
Reaction score
36
1. You mentioned that "If you don't have a packet communicating the actual stats updates" How would one sent packets back to the client to update its stats?

The server takes care of sending back the necessary packets; for example, if we have a mana expenditure to communicate to the client,
of the applyTo function (that's invoked from the skill handler class, as we saw earlier) will be true ("mpchange" is the MP value that's supposed to change due to the skill, aka the MP consumed), and thus applyto.getClient().announce(MaplePacketCreator.updatePlayerStats(hpmpupdate, true)) will be invoked. This is the packet that informs the client about a new stat update.
2. Even if I use Dragon Roar(W.e that dark Knight stkill is) the client still needs to read the damage from the server? I guess what I am trying to say is that for those handlers (close/range) The damage isn't client side right? As you mentioned
"
If it's an attack, the damage is calculated and shown in first person, then sent to the server."
I'm gonna explain this using concepts I learnt after digging in the client and server code.

There are different possible sources of damage against a monster:
  • Melee damage (due to close-range skills, or regular attack);
  • Magic damage (due to magic spells);
  • Ranged damage (due to long-range skills, or long-range regular attack);
  • Summon damage (due to your summon attacking a monster);
  • Status damage (due to a status applied to a monster).

Each of these sources has its own packet: melee attacks have CloseRangeDamageHandler, magic attacks have MagicAttackHandler, and so on.
Dragon Roar is a ranged skill, thus will use RangedAttackHandler.
If your Bahamut summon were to deal an attack, its packet will be handled in SummonDamageHandler.

All of the above packets are created in the client, ofcourse, and sent to the server. They all contain pre-calculated damage, and the damage numbers you see over the monster's head are shown before the packet is sent. However, that's only visual damage, the monster hasn't actually lost any HP yet.
Once the packet reaches the server, the server checks the damage against potential issues (hacked values, for example), updates the monster's HP (or kills the monster if needed) and then broadcasts the results back to the players in the map.

For status damage, the situation is different: what's passed by the skill packet isn't damage, but information about which skill was used. In this case it's the server that has to calculate the damage, then again update the monster, and then broadcast the result back to the players in the map.


3. How does the client know to invoke the right animation? For example Even before this Ninja ambush startup will show (the ninja dashing left and right and then hiding) What shows like you said the ninja going left and right left and right doing damage?

That would be the wz files; namely Skill.wz.
When you activate Ninja Ambush, the client checks what animation corresponds to that skill, and finds it in "Skill.wz/<jobid>.img/skill/<skillid>/effect".
Where to look for is coded directly inside the client. Using STREDIT, you will find out that the client includes this string:
MDBZPJl - Fixing Ninja Ambush Skill in MoopleDev - RaGEZONE Forums

, which is how the path to the correct animation is built.

As for the animation over the monster, once the client receives back a packet that updates the monster status to "Ambush" active, it'll seek for the effect to show over the mob with this string:
diFbwwA - Fixing Ninja Ambush Skill in MoopleDev - RaGEZONE Forums

, which is a path to the animation with little ninjas hitting left and right.

4. And the damage, why is it reading its healing a monster? and Truncating so much bits?
The truncation is necessary because the packet expects a short to be passed as damage value in that spot. Which is to say, no damage exceeding a signed short (32767) is allowed, for damage over time. This is how it was coded inside the client, there's not a specific reason behind it.

Because we need a short, if we're trying to input an int we're going to be losing some data.
Due to the nature of casting, if we have an integer and we cast it as short via (short) value , we will be telling the server to truncate everything that's over the 4-bytes length of a short number.
As we saw before, truncating 19660200 to short gives us -600.

If we pass -600 as damage value to the client, there's internal checks that determine that numbers lower than 0 are to be considered healing, while positive numbers are regular damage.
 

Attachments

You must be registered for see attachments list
Upvote 0
Newbie Spellweaver
Joined
Aug 27, 2016
Messages
91
Reaction score
1
Amazing, thank you. Really helped out. I guess I'll try to fix that skill now hahah. What I find so confusing is that you dont know what each function is doing. Its like looking at someones work and trying to figure it out on your own without the creator telling you whats-what.

Question: I am assuming for DOT damage within maple you have to run that scheduler(damage task) correct?

and how is damage/skills/buffs/etc shown to other players within the map? Whats responsible for that? Like when you use Dragon roar (RangeAttackHandler)

and is this packet responsible for showing the ninjas going left to right and also showing the effecst to others? and to yourself?:
Code:
 byte[] packet = MaplePacketCreator.applyMonsterStatus(getObjectId(), status);

 map.broadcastMessage(packet, getPosition());

Just further touching on the subject about on whats happening to the damage. How come the method that handles the damage back to the client is expecting an int? Is this an error?

Code:
public static byte[] damageMonster(int oid, int damage) {
        final MaplePacketLittleEndianWriter mplew = new MaplePacketLittleEndianWriter();
        mplew.writeShort(SendOpcode.DAMAGE_MONSTER.getValue());
        mplew.writeInt(oid);
        mplew.write(0);
        [COLOR=#0000ff]mplew.writeInt(damage);[/COLOR]
        mplew.write(0);
        mplew.write(0);
        mplew.write(0);
        return mplew.getPacket();
    }
 
Last edited:
Upvote 0
Junior Spellweaver
Joined
Sep 16, 2017
Messages
156
Reaction score
36
Its like looking at someones work and trying to figure it out on your own without the creator telling you whats-what.
Bingo. c: this is the part that takes the most patience, hehe.
Question: I am assuming for DOT damage within maple you have to run that scheduler(damage task) correct?and how is damage/skills/buffs/etc shown to other players within the map? Whats responsible for that? Like when you use Dragon roar (RangeAttackHandler)
and is this packet responsible for showing the ninjas going left to right and also showing the effecst to others? and to yourself?:
Code:
 byte[] packet = MaplePacketCreator.applyMonsterStatus(getObjectId(), status);
map.broadcastMessage(packet, getPosition());
Yep, that one's the packet that shows the ninjas animation: the function "broadcastMessage", in MapleMap.java, does exactly that. It sends a packet to everyone who's able to view it (this group of players is calculated in the function itself, as you can see ).

The packet that shows the activated skill animation to other players in the map, on the other hand, is sent from , the class that handles the effect activation in Odin sources.
Depending on the skill, you'll find that there's different places in that file where the effect is sent: passive skills have their effect displayed by another function (but with the same call - the packet responsible for instructing the client to show a specific skill activation animation is always MaplePacketCreator.showBuffEffect), for example.
Just further touching on the subject about on whats happening to the damage. How come the method that handles the damage back to the client is expecting an int? Is this an error?
Code:
public static byte[] damageMonster(int oid, int damage) {
        final MaplePacketLittleEndianWriter mplew = new MaplePacketLittleEndianWriter();
        mplew.writeShort(SendOpcode.DAMAGE_MONSTER.getValue());
        mplew.writeInt(oid);
        mplew.write(0);
        [COLOR=#0000ff]mplew.writeInt(damage);[/COLOR]
        mplew.write(0);
        mplew.write(0);
        mplew.write(0);
        return mplew.getPacket();
    }
Regular damage against a monster can reach the values of an integer: it's dot damage caused by a status that needs to be passed as a short.
The packet responsible for that is .
 
Upvote 0
Newbie Spellweaver
Joined
Aug 27, 2016
Messages
91
Reaction score
1
Ahh thats the function I was looking at but where is the damage being parsed in there (applyMonsterStatus)?

Code:
public static byte[] applyMonsterStatus(final int oid, final MonsterStatusEffect mse) {
        final MaplePacketLittleEndianWriter mplew = new MaplePacketLittleEndianWriter();
        mplew.writeShort(SendOpcode.APPLY_MONSTER_STATUS.getValue());
        mplew.writeInt(oid);
        mplew.writeLong(0);
        writeIntMask(mplew, mse.getStati());
        for (Map.Entry<MonsterStatus, Integer> stat : mse.getStati().entrySet()) {
            mplew.writeShort(stat.getValue());
            if (mse.isMonsterSkill()) {
                mplew.writeShort(mse.getMobSkill().getSkillId());
                mplew.writeShort(mse.getMobSkill().getSkillLevel());
            } else {
                mplew.writeInt(mse.getSkill().getId());
            }
            mplew.writeShort(-1); // might actually be the buffTime but it's not displayed anywhere
        }
        mplew.writeShort(0);
        mplew.writeInt(0);
        return mplew.getPacket();
    }

Why is the damageTask being passed down a damage variable then?

Code:
private final class DamageTask implements Runnable {

        private final int dealDamage;
        private final MapleCharacter chr;
        private final MonsterStatusEffect status;
        private final Runnable cancelTask;
        private final int type;
        private final MapleMap map;

        private DamageTask(int dealDamage, MapleCharacter chr, MonsterStatusEffect status, Runnable cancelTask, int type) {
            this.dealDamage = dealDamage;
            this.chr = chr;
            this.status = status;
            this.cancelTask = cancelTask;
            this.type = type;
            this.map = chr.getMap();
        }

        @Override
        public void run() {
            int damage = dealDamage;
            if (damage >= hp) {
                damage = hp - 1;
                if (type == 1 || type == 2) {
                    [COLOR=#0000cd]map.broadcastMessage(MaplePacketCreator.damageMonster(getObjectId(), damage), getPosition());[/COLOR]
                    cancelTask.run();
                    status.getCancelTask().cancel(false);
                }
            }
            if (hp > 1 && damage > 0) {
                damage(chr, damage, false);
                if (type == 1) {
                    map.broadcastMessage(MaplePacketCreator.damageMonster(getObjectId(), damage), getPosition());
                }
            }
        }
    }

So when I use dragon roar the packet to show the other players the animation is being invoked in mapleStateEffect?
 
Last edited:
Upvote 0
Junior Spellweaver
Joined
Sep 16, 2017
Messages
156
Reaction score
36
The damage is calculated in , it's the "final int damage" we looked at before, when we checked if the damage was correct by setting that message that would show it in chat c:

It's then stored as value for the "NINJA_AMBUSH" monster stat,
Code:
status.setValue(MonsterStatus.NINJA_AMBUSH, Integer.valueOf(damage));
, which is what's passed as short in the packet.
 
Upvote 0
Newbie Spellweaver
Joined
Aug 27, 2016
Messages
91
Reaction score
1
The damage is calculated in , it's the "final int damage" we looked at before, when we checked if the damage was correct by setting that message that would show it in chat c:

It's then stored as value for the "NINJA_AMBUSH" monster stat,
Code:
status.setValue(MonsterStatus.NINJA_AMBUSH, Integer.valueOf(damage));
, which is what's passed as short in the packet.

Ahh, I see but where in the packet ?
Code:
public static byte[] applyMonsterStatus(final int oid, final MonsterStatusEffect mse) {
        final MaplePacketLittleEndianWriter mplew = new MaplePacketLittleEndianWriter();
        mplew.writeShort(SendOpcode.APPLY_MONSTER_STATUS.getValue());
        mplew.writeInt(oid);
        mplew.writeLong(0);
        writeIntMask(mplew, mse.getStati());
        for (Map.Entry<MonsterStatus, Integer> stat : mse.getStati().entrySet()) {
            [COLOR=#ff0000]mplew.writeShort(stat.getValue());[/COLOR]
            if (mse.isMonsterSkill()) {
               [COLOR=#ff0000] mplew.writeShort(mse.getMobSkill().getSkillId());
                mplew.writeShort(mse.getMobSkill().getSkillLevel());[/COLOR]
            } else {
                mplew.writeInt(mse.getSkill().getId());
            }
            mplew.writeShort(-1); // might actually be the buffTime but it's not displayed anywhere
        }
        mplew.writeShort(0);
        mplew.writeInt(0);
        return mplew.getPacket();
    }

Sorry, I am just getting a little more confused. Just confused as to why the damageTask class is being sent down damage as well but in an int and sending a packet back to the client in terms of damage to the monster when you are already doing that over at applyMonsterStatus, unless the applyMonsterStatus is strictly for telling the client its a buff that does damage to monster in terms of DOT and the DamageTask is responsible for the damage

Code:
private final class DamageTask implements Runnable {

        private final int dealDamage;
        private final MapleCharacter chr;
        private final MonsterStatusEffect status;
        private final Runnable cancelTask;
        private final int type;
        private final MapleMap map;

        private DamageTask([COLOR=#0000ff]int dealDamage[/COLOR], MapleCharacter chr, MonsterStatusEffect status, Runnable cancelTask, int type) {
            this.dealDamage = dealDamage;
            this.chr = chr;
            this.status = status;
            this.cancelTask = cancelTask;
            this.type = type;
            this.map = chr.getMap();
        }

        @Override
        public void run() {
            int damage = dealDamage;
            if (damage >= hp) {
                damage = hp - 1;
                if (type == 1 || type == 2) {
                    map.broadcastMessage(MaplePacketCreator.damageMonster(getObjectId(), [COLOR=#0000ff]damage[/COLOR]), getPosition());
                    cancelTask.run();
                    status.getCancelTask().cancel(false);
                }
            }
            if (hp > 1 && damage > 0) {
                damage(chr, damage, false);
                if (type == 1) {
                    [COLOR=#ff0000]map.broadcastMessage(MaplePacketCreator.damageMonster(getObjectId(), [/COLOR][COLOR=#0000ff]damage[/COLOR][COLOR=#ff0000]), getPosition());[/COLOR]
                }
            }
        }
    }
Code:
public static byte[] damageMonster(int oid, int damage) {
        final MaplePacketLittleEndianWriter mplew = new MaplePacketLittleEndianWriter();
        mplew.writeShort(SendOpcode.DAMAGE_MONSTER.getValue());
        mplew.writeInt(oid);
        mplew.write(0);
        [COLOR=#0000ff]mplew.writeInt(damage);[/COLOR]
        mplew.write(0);
        mplew.write(0);
        mplew.write(0);
        return mplew.getPacket();
    }
 
Last edited:
Upvote 0
Junior Spellweaver
Joined
Sep 16, 2017
Messages
156
Reaction score
36
It's stored as an int because it's being stored in a HashMap that requires integer as value.
I'll explain what this map is about; to do that, we'll need to know what MonsterStatusEffect is.

A MonsterStatusEffect is an object that contains data on which monster statuses are being activated by the current skill that's being used.
A single skill can contain more than one monster status (for example, the thief skill Disorder lowers both the attack and the defense of the monster), so the collection of all statuses is stored as a HashMap: keys for the type of status (watk, wdef, ninja_ambush, and so on), values for the buffed value (in case of atk/def, the value will be how much atk or def is added/subtracted; in case of ninja ambush, the value will be the damage dealt to the monster).
Before you call applyMonsterStatus, you need to set all the statuses and their values; in case of Ninja Ambush, the NINJA_AMBUSH status is set with that setValue call. You can follow that function to MonsterStatusEffect.java, and you'll notice that it takes care of putting the new key-value pair in the map.
Now, when you call applyMonsterStatus, you're preparing a packet that will send to the client data relative to potentially multiple statuses to be activated.
I won't go in depth about that packet, but what it's relevant to us is that each status passes the value (which, in Ambush case, as we said, refers to the damage for the dot) as stat.getValue(), which is the first line you highlighted in red.

~

Since vBulletin adores refreshing the page while I'm still typing, I'll also already answer your edit:

the applyMonsterStatus is strictly for telling the client its a buff that does damage to monster in terms of DOT and the DamageTask is responsible for the damage
It's exactly this c: applyMonsterStatus only activates the visual effect of ninjas plus damage numbers in the client, but as we already saw, the monster's health is stored only in the server, and thus needs to be updated there.

That's where the DamageTask kicks in: it receives the damage in order to update the monster's HP and send the updated value back to the client. In order to have it be synchronized with the animation as much as possible, the damageTask runs every second, like the animation.
 
Upvote 0
Newbie Spellweaver
Joined
Aug 27, 2016
Messages
91
Reaction score
1
I see I see, just to clarify I did some additional testing and the dot damage is coming from the damageTask class, is this correct?

I debugged it and found out that the damage is coming from, the ticks of ninja ambush is reliant in the damageMonster().

Code:
public static byte[] damageMonster(int oid, int damage) {
        final MaplePacketLittleEndianWriter mplew = new MaplePacketLittleEndianWriter();
        mplew.writeShort(SendOpcode.DAMAGE_MONSTER.getValue());
        mplew.writeInt(oid);
        mplew.write(0);
        [COLOR=#0000ff]mplew.writeInt(damage);[/COLOR] -> I changed this into a 1,99, 100 and it all worked.
        mplew.write(0);
        mplew.write(0);
        mplew.write(0);
        return mplew.getPacket();
    }

SO applyMonsterStatus() Is visual where as damageMonster() deals with damage right?!?! god please say yes

anyways What else I found weird was the damage isnt being passed down correctly to the damageMonster(), in the damageTask class my damage calculated is there but when invoking damageMonster(oid, damage) the damage passed down turns either into a 0 or a 7?

EDIT: never mind, the damage of ninja ambush is starting to make a lot of sense, but is the still suppose to drop the monster to 1HP? if so that its working fine.

The only problem is the scheduler its ending the skill to quickly. What tells it to run for 12seconds as it suppose to?

Side question: For kicks I took the condition to dropping monster Hp to 1 and it doesnt kill the monster? Whats the flow for killing a monster?

Once again I want to thank you, everything is starting to make a little bit more sense now.

Update: Its once we talked about how the damage shown from a DOT damage is a short. I am just confusing my some more due to digging. I comment out both the
Code:
//status.setValue(MonsterStatus.NINJA_AMBUSH, Integer.valueOf(damage));
            //status.setDamageSchedule(timerManager.register(new DamageTask(damage, from, status, cancelTask, 2), 1000, 1000));

Okay I think applyMonsterstatus is for the visual damage as well but damageTask is the actual damage

Update 2: OKay I think I know how to fix, however the problem is that damageMonster() and applyMonsterStatus() both show their own damage , how do I make it so damageMonster() damage is hidden but still deals the damage?
 
Last edited:
Upvote 0
Junior Spellweaver
Joined
Sep 16, 2017
Messages
156
Reaction score
36
SO applyMonsterStatus() Is visual where as damageMonster() deals with damage right?!?! god please say yes
Almost, but not exactly: damageMonster() only deals with the visual part, too.

The dot damage numbers that you see are included in the applyMonsterStatus() call with Ninja Ambush: they're part of the effect with the little ninjas.
damageMonster() is a function that simply displays the damage over a monster's head.

Calling both applyMonsterStatus() with Ninja Ambush status and damageMonster() would end up displaying two numbers over the monster's head, because of this.


What deals with the actual serverside damage dealt to the monster is damage().
Let's analyze the final steps of Ninja Ambush damage registration, to clarify things up.

Code:
if (status.getSkill().getId() == 4121004 || status.getSkill().getId() == 4221004) { // Ninja Ambush
    final Skill skill = SkillFactory.getSkill(status.getSkill().getId());
    final byte level = from.getSkillLevel(skill);

    final int damage = (int) ((from.getStr() + from.getLuk()) * (1.5 + (level * 0.05)) * skill.getEffect(level).getDamage());

    [COLOR=#008080]// Here we assign the dot damage to the NINJA_AMBUSH monster status.
[/COLOR]    [COLOR=#0000cd]status.[B]setValue[/B](MonsterStatus.NINJA_AMBUSH, Integer.valueOf(damage));[/COLOR]


    [COLOR=#ffa07a]// Here we schedule the server-side "real" monster HP loss, which will happen once every second,
    // to be consistent with the Ninja Ambush animation damage display.
[/COLOR]    [COLOR=#b22222]status.setDamageSchedule(timerManager.register(new [B]DamageTask[/B](damage, from, status, cancelTask, 2), 1000, 1000));[/COLOR]
}

for (MonsterStatus stat : status.getStati().keySet()) {
    stati.put(stat, status);
}

int animationTime = status.getSkill().getAnimationTime();

[COLOR=#00ff00]// Here we prepare and broadcast the packet that instructs the client to display
// the animation of Ninja Ambush on this monster.
[/COLOR][COLOR=#006400]byte[] packet = MaplePacketCreator.[B]applyMonsterStatus[/B](getObjectId(), status);
map.broadcastMessage(packet, getPosition());[/COLOR]


So, what do we have here?

The first step is to calculate the dot damage and register it as value of the Ninja Ambush effect; we will be using this later, when displaying the actual Ninja Ambush animation.

But before that, we deal with the server-side damage ticks.

It's time to look into what DamageTask does. NOTE: I only include the run() function of the runnable class DamageTask, as the rest of it is of no interest for us at the moment.

Code:
public void run() {
    int damage = dealDamage;
    
    if (damage >= hp) {
    [COLOR=#b22222]// We get here only if the damage we're about to deal exceeds the monster's HP.
    // DOT damage is only allowed to bring the monster's HP down to 1, but not to kill the monster by itself.

    // So, this code portion adjusts the new damage so that it won't kill the monster.[/COLOR]
        damage = hp - 1;
        
        if (type == 1 || type == 2) {
            [COLOR=#b22222]// Type can be 0 (regular poison), 1 (Shadow Web) or 2 (Ninja Ambush).[/COLOR]
            map.broadcastMessage(MaplePacketCreator.damageMonster(getObjectId(), damage), getPosition());
            
            cancelTask.run();
            status.getCancelTask().cancel(false);
        }
    }
    
    if (hp > 1 && damage > 0) {

        [COLOR=#b22222]// Here, we finally damage the monster's actual HP pool serverside.
[/COLOR]        [B]damage[/B](chr, damage, false);
        
        if (type == 1) {
        [COLOR=#b22222]// We display the damage numbers only if the DOT effect we're dealing with is Shadow Web.
        // This is because Shadow Web does [B]not[/B] have DOT numbers included in its client processing.
[/COLOR]            map.broadcastMessage(MaplePacketCreator.damageMonster(getObjectId(), damage), getPosition());
        }
    }
}

With this analysis, we should now know what deals with which animation, and what deals with the damage.

I hadn't noticed that before, but it looks like there's still an issue with this code.
Let's look once more at the code portion that handles the case when the damage tick exceeds a monster's total HP pool:

Code:
if (damage >= hp) {    damage = hp - 1;
        
    if (type == 1 || type == 2) {
        [COLOR=#b22222]// Type can be 0 (regular poison), [B]1 (Shadow Web)[/B] or [B]2 (Ninja Ambush)[/B].[/COLOR]
        map.broadcastMessage(MaplePacketCreator.[B]damageMonster[/B](getObjectId(), damage), getPosition());
            
        cancelTask.run();
        status.getCancelTask().cancel(false);
    }
}

When the damage exceeds the monster's HP pool, and if the type is 1 or 2, the packet to show the damage numbers is sent. This is fine for Shadow Web, but we don't need it for Ninja Ambush, because it has its own way to show those numbers.

So, try adding an extra if around that broadcastMessage call:

Code:
    if (type == 1 || type == 2) {

        [B]if (type == 1) {
[/B]            map.broadcastMessage(MaplePacketCreator.damageMonster(getObjectId(), damage), getPosition());
        [B]}[/B]
            
        cancelTask.run();
        status.getCancelTask().cancel(false);
    }

This should deal with the extra unwanted numbers in the Ambush animation, and hopefully I explained things clearly enough. c:
 
Upvote 0
Newbie Spellweaver
Joined
Aug 27, 2016
Messages
91
Reaction score
1
I see, Ill see what I can do around that if statement. Ill triple read your passage.

But some things to clarify:

If damageMonster() is another visual packet why does it do damage then? I have double check the hp variable and let say blue snail initially ninja ambush would do 14 damage cause of the damage = hp - 1 (15-1) then when I do it again hp would then be 1?

Why is there two visual ways of showing damage then via damageMonster() and applyMonsterStatus()
 
Upvote 0
Back
Top