• Scheduled Server Maintenance | Friday 6 November

    Our hardware provider has scheduled RAM maintenance to address the recent server instability. Work is expected by 17:00 Central European time.

    Brief downtime or interruptions may occur while the faulty memory modules are replaced. We apologise for the recent disruption and appreciate your patience.


    Special thanks to our sponsor, HyperFilter!

    We keep our community safe. They keep our servers safe. Running your own game servers? Keep them protected with HyperFilter.

Crossbow visual desync bug S5.2 Main source

Status
Not open for further replies.
Joined
Dec 8, 2007
Messages
181
Reaction score
23
I am playing with an Elf character and whenever I am moving and start a normal ranged attack on monsters using a crossbow I see the Elf stuttering or jumping its position which points to a desync visual bug. Investigating I found that the bug is unique to the crossbow weapon because its base attack speed is calculated differently and often land on odd frames that mess the animation. Bows don't suffer from this issue.

But I tried with a Silver Bow and a Light Crossbow as both have a base attack speed of 40 and the bug only appeared on the Crossbow, so I am at lost at the real cause of this, thank you in advance for any help.
 
Last edited:
Solution
Everything seems good except the bug I showed you on video, I downloaded your sources from post #12 and replaced only the last ZzzInterface.cpp you uploaded then compiled and same error, I compiled the original sources I uploaded with the last ZzzInterface.cpp changes applied still the same :S

Ok, Houston...
Using our working source code as a reference, we found and fixed several issues in your manual crossbow attack logic at once.

These included unsafe path replacement, missing client/server movement synchronization, repeated path resets, endless back-and-forth chasing, missing timeout recovery, outdated target and line-of-sight checks, missing ammunition revalidation, server-echo position snapping, and incorrect...

Short answer​

The weapon's displayed base attack speed is probably not the cause. A bow and a crossbow can both have an attack speed of 40 while still using different character actions, animation tracks, weapon slots, frame counts, and projectile timing.

The most likely visual jump occurs when the client receives an attack action while the character is moving. The action handler stops movement and snaps the rendered model to the center of a logical map tile before changing the animation. The crossbow animation makes this correction much more visible than the bow animation.

Version note: the code below uses a Season 6 client as an example. The same principle applies to other generations, but function names, packet layouts, action constants, structures, calling conventions, and addresses may differ.

Why equal attack speed does not mean equal behavior​

In the Season 6 example, bows and crossbows select separate actions:

Code:
PLAYER_ATTACK_BOW
PLAYER_ATTACK_CROSSBOW

They may also have separate flying, mounted, Fenrir, and raised-shot actions. The item attack-speed value controls timing calculations, but it does not make the two animation tracks identical.

Consequently, rounding attack speed to an even value or changing the server attack delay is not a proper correction. It may hide the symptom at one speed while introducing a real mismatch between visual attacks and server-side damage timing.

Primary suspect: position snapping in the action handler​

A typical client action handler contains logic similar to this:

Code:
c->Object.Angle[2] = ((float)(Data->Angle) - 1.0f) * 45.0f;
c->Movement = false;

c->Object.Position[0] =
    c->TargetX * TERRAIN_SCALE + TERRAIN_SCALE * 0.5f;

c->Object.Position[1] =
    c->TargetY * TERRAIN_SCALE + TERRAIN_SCALE * 0.5f;

While moving, the rendered character can be between two logical tiles. When the normal-attack confirmation is processed, the code above moves the model immediately to the center of TargetX/TargetY. That produces the visible jump.

Focused source-code correction​

Do not disable all server position corrections. Suppress this particular render-position snap only for the local moving Elf starting a normal crossbow attack:

Code:
const bool isNormalAttack =
    Data->Action == AT_ATTACK1 ||
    Data->Action == AT_ATTACK2;

const bool isLocalCrossbowAttack =
    c == Hero &&
    isNormalAttack &&
    gCharacterManager.GetBaseClass(c->Class) == CLASS_ELF &&
    gCharacterManager.GetEquipedBowType(c) == BOWTYPE_CROSSBOW;

const bool wasMoving = (c->Movement != false);

c->Object.Angle[2] = ((float)(Data->Angle) - 1.0f) * 45.0f;
c->Movement = false;

// Preserve the interpolated visual position during the
// local move-to-crossbow-attack transition.
if (!(isLocalCrossbowAttack && wasMoving))
{
    c->Object.Position[0] =
        c->TargetX * TERRAIN_SCALE + TERRAIN_SCALE * 0.5f;

    c->Object.Position[1] =
        c->TargetY * TERRAIN_SCALE + TERRAIN_SCALE * 0.5f;
}

switch (Data->Action)
{
    case AT_ATTACK1:
    case AT_ATTACK2:
        SetPlayerAttack(c);
        c->AttackTime = 1;
        c->Object.AnimationFrame = 0.0f;
        break;
}

Keep the correction narrowly scoped at first. If it fixes the local Elf but remote observers still see a jump, investigate the move/action packet order and the coordinates sent by the GameServer.

Secondary Season 6 issue: bow speed used for crossbow animation​

Some Season 6 Main sources use the bow action's playback speed for both bow and crossbow weapon animation:

Code:
if (o->CurrentAction == PLAYER_ATTACK_BOW ||
    o->CurrentAction == PLAYER_ATTACK_CROSSBOW ||
    o->CurrentAction == PLAYER_ATTACK_FLY_BOW ||
    o->CurrentAction == PLAYER_ATTACK_FLY_CROSSBOW)
{
    w->PlaySpeed =
        Models[MODEL_PLAYER].Actions[PLAYER_ATTACK_BOW].PlaySpeed;
}

The crossbow branch is incorrectly synchronized to PLAYER_ATTACK_BOW. Use the current player action instead:

Code:
if (o->CurrentAction == PLAYER_ATTACK_BOW ||
    o->CurrentAction == PLAYER_ATTACK_CROSSBOW ||
    o->CurrentAction == PLAYER_ATTACK_FLY_BOW ||
    o->CurrentAction == PLAYER_ATTACK_FLY_CROSSBOW)
{
    w->PlaySpeed =
        Models[MODEL_PLAYER].Actions[o->CurrentAction].PlaySpeed;
}

If the particular client does not allow direct use of CurrentAction here, use an explicit bow/crossbow action mapping. This is a secondary correction: it synchronizes the weapon and character animation, but the position-snap fix may still be required.

If the Main source is unavailable​

The issue can still be corrected by installing a hook from .dll into the relevant function inside main.exe. The faulty function remains in Main; the DLL acts as the patch carrier.

A low-risk approach is to save the interpolated position, call the original action handler, and restore only the local visual X/Y position when the exact problematic transition occurs:

Code:
using ReceiveAction_t = void(__cdecl*)(BYTE* buffer, int size);
ReceiveAction_t OriginalReceiveAction = nullptr;

void __cdecl HookedReceiveAction(BYTE* buffer, int size)
{
    LPPRECEIVE_ACTION packet =
        reinterpret_cast<LPPRECEIVE_ACTION>(buffer);
    CHARACTER* character = FindCharacterFromActionPacket(packet);

    bool preservePosition = false;
    float savedX = 0.0f;
    float savedY = 0.0f;

    if (character != nullptr)
    {
        const bool normalAttack =
            packet->Action == AT_ATTACK1 ||
            packet->Action == AT_ATTACK2;

        preservePosition =
            character == Hero &&
            character->Movement &&
            normalAttack &&
            GetBaseClass(character->Class) == CLASS_ELF &&
            GetEquipedBowType(character) == BOWTYPE_CROSSBOW;

        if (preservePosition)
        {
            savedX = character->Object.Position[0];
            savedY = character->Object.Position[1];
        }
    }

    OriginalReceiveAction(buffer, size);

    if (preservePosition)
    {
        character->Object.Position[0] = savedX;
        character->Object.Position[1] = savedY;
    }
}

The hook can be installed using MinHook, Detours, or the project's existing hook/trampoline system:

Code:
MH_CreateHook(
    reinterpret_cast<void*>(receiveActionAddress),
    &HookedReceiveAction,
    reinterpret_cast<void**>(&OriginalReceiveAction));

MH_EnableHook(
    reinterpret_cast<void*>(receiveActionAddress));

Do not copy an address from another Main. Even two executables with the same visible version can have different offsets because of compiler settings, protection, regional builds, or custom modifications.

Locating the function in IDA or Ghidra​

Without symbols, identify the action handler by its behavior rather than by an absolute address:

  • Look for a packet handler containing a switch for stand, attack, sit, pose, greeting, and similar actions.
  • Look for calls equivalent to SetPlayerAttack.
  • Look for a write of zero to the character's animation frame.
  • Look for conversion from tile coordinates to world coordinates, commonly equivalent to TargetX * 100.0f + 50.0f.
  • Confirm the calling convention and packet structure in the exact executable before installing the hook.
The weapon playback-speed issue can be located by following references to the bow and crossbow attack-action constants and finding the code that assigns PlaySpeed to the equipped weapon part.

How to confirm the real cause​

  1. Test normal attacks while completely stationary.
  2. Test normal attacks while moving between two tiles.
  3. Compare a bow and crossbow with the same displayed attack speed.
  4. Log the action, animation frame, movement flag, target tile, and rendered position immediately before and after the action packet.
  5. Observe the character from a second client.
  6. Repeat with wings and mounts because they use different actions.
If only the local player sees the jump, the client-side interpolation/action transition is the main suspect. If other players see the same jump at the same moment, inspect the GameServer's move/action packet sequence and transmitted coordinates as well.

Recommended order of work​

  1. Do not change the server attack-speed formula.
  2. Instrument the action handler and confirm that the position changes when the normal crossbow attack starts.
  3. Apply the narrowly scoped local-position preservation fix.
  4. Correct the crossbow weapon animation's playback-speed source.
  5. Test local and remote rendering, then wings and mounts.
 
Last edited:
Upvote 0
Thanks for your answers, I applied your changes and tried the steps you suggested:
Normal attacks while stationary seems unaffected
Compared bow and crossbow with the same base attack speed and only the crossbow is affected
When I tested the normal attacks only the issue appeared when moving (walking, running, mounted, flying)

After applying your changes the issue is greatly alleviated, only thing that remains is that instead of jumping the frames to the normal attack now I have the Elf "slide" to the tile and it only occurs when moving between two tiles

Also I forgot to mention that even monster that used a crossbow where affected by this issue, they desynced even more when attacking. Now after your changes they appear to be fixed!!!

 
Last edited:
Upvote 0
Your latest result is consistent with the original issue being split into two bugs. The crossbow animation/play-speed correction was valid, which is why the crossbow monsters are now fixed. The remaining Elf slide happens because the first position patch preserves a rendered coordinate between two tiles but also stops movement. The logical tile and path cursor still point elsewhere, so the client later reconciles the model by sliding it to a tile centre.

Replace the render-position save/restore patch with a coherent path commit: choose the nearer tile of the active path segment, assign that tile to PositionX/Y and TargetX/Y, clear the path cursor, set Movement = false, and place the rendered model at the same tile centre before starting the attack. If a completely seamless transition is required, defer the attack until the current segment reaches a tile boundary, but that approach is more invasive because it changes local input timing.


What the new test results confirm​

These results separate two different defects that originally looked like one problem:

  1. Crossbow animation playback mismatch. Correcting the crossbow action/play-speed mapping fixed the monsters. This confirms that the generic crossbow animation branch was wrong.
  2. Interrupted player path reconciliation. The remaining Elf slide occurs only while the model is between two map tiles. This is not an attack-speed problem; it is the transition from interpolated movement to the attack action.
Root cause: the first patch prevented the large snap to TargetX/TargetY, but it left the rendered model at an in-between coordinate while setting Movement = false. The logical tile, path cursor, target tile, and rendered X/Y no longer described the same state. A later client update reconciled them, producing the visible slide.
The fact that stationary attacks remain correct and the defect appears specifically between two tiles strongly supports this diagnosis.

Version note: the implementation below is shown using a Season 6-style Main source. Other client generations may use different structures, constants, packet layouts, and function names, but the state-consistency rule is the same.

Why the old position-preservation patch is incomplete​

The movement routine in this Season 6 example maintains two related but different positions:

  • PositionX/PositionY and PathX/PathY represent logical map tiles.
  • Object.Position[0/1] is the smoothly interpolated render position between those tiles.
During MovePath, the logical position can already advance to the next path tile while the visible model is still travelling toward it. The original action handler then does this:
Code:
c->Movement = false;
c->Object.Position[0] = c->TargetX * TERRAIN_SCALE
                      + TERRAIN_SCALE * 0.5f;
c->Object.Position[1] = c->TargetY * TERRAIN_SCALE
                      + TERRAIN_SCALE * 0.5f;
That can jump all the way to the final clicked target. The first fix skipped those two writes and preserved the in-between render position. However, it still stopped the path, so the object later slid toward whichever logical tile remained authoritative.

Do not keep restoring only Object.Position[0/1] after the original handler. That is the exact pattern which changes the jump into a slide.


Recommended correction: commit the interrupted path coherently​

When a moving Elf starts a normal crossbow attack, choose the nearest of the two tiles belonging to the active path segment and commit all movement state to that tile. Do not use the final clicked TargetX/TargetY, and do not preserve a fractional position after movement has been cancelled.

Season 6-style helper based on the path structure

​

Code:
static void CommitToNearestPathTile(CHARACTER* c)
{
    PATH_t& path = c->Path;

    unsigned char commitX = c->PositionX;
    unsigned char commitY = c->PositionY;

    if (path.PathNum > 0)
    {
        int current = static_cast<int>(path.CurrentPath);

        if (current < 0)
            current = 0;
        if (current >= path.PathNum)
            current = path.PathNum - 1;

        int next = current + 1;
        if (next >= path.PathNum)
            next = path.PathNum - 1;

        const float currentX =
            (static_cast<float>(path.PathX[current]) + 0.5f)
            * TERRAIN_SCALE;
        const float currentY =
            (static_cast<float>(path.PathY[current]) + 0.5f)
            * TERRAIN_SCALE;

        const float nextX =
            (static_cast<float>(path.PathX[next]) + 0.5f)
            * TERRAIN_SCALE;
        const float nextY =
            (static_cast<float>(path.PathY[next]) + 0.5f)
            * TERRAIN_SCALE;

        const float dxCurrent = c->Object.Position[0] - currentX;
        const float dyCurrent = c->Object.Position[1] - currentY;
        const float dxNext = c->Object.Position[0] - nextX;
        const float dyNext = c->Object.Position[1] - nextY;

        const float currentDistanceSq =
            dxCurrent * dxCurrent + dyCurrent * dyCurrent;
        const float nextDistanceSq =
            dxNext * dxNext + dyNext * dyNext;

        const int chosen =
            (nextDistanceSq < currentDistanceSq) ? next : current;

        commitX = path.PathX[chosen];
        commitY = path.PathY[chosen];
    }

    // Logical position, final target, path cursor, movement flag and
    // rendered position must all describe the same committed tile.
    c->PositionX = commitX;
    c->PositionY = commitY;
    c->TargetX = commitX;
    c->TargetY = commitY;

    path.PathNum = 0;
    path.CurrentPath = 0;
    path.CurrentPathFloat = 0;
    path.Success = false;
    path.Error = false;

    c->Movement = false;
    c->Object.Position[0] =
        (static_cast<float>(commitX) + 0.5f) * TERRAIN_SCALE;
    c->Object.Position[1] =
        (static_cast<float>(commitY) + 0.5f) * TERRAIN_SCALE;
}

Use it in the action handler​

​

Code:
const bool isNormalAttack =
    Data->Action == AT_ATTACK1 ||
    Data->Action == AT_ATTACK2;

const bool isMovingCrossbowElf =
    c == Hero &&
    c->Movement &&
    isNormalAttack &&
    gCharacterManager.GetBaseClass(c->Class) == CLASS_ELF &&
    gCharacterManager.GetEquipedBowType(c) == BOWTYPE_CROSSBOW;

c->Object.Angle[2] =
    (static_cast<float>(Data->Angle) - 1.0f) * 45.0f;

if (isMovingCrossbowElf)
{
    CommitToNearestPathTile(c);
}
else
{
    // Preserve the original behavior for every unrelated action.
    c->Movement = false;
    c->Object.Position[0] =
        c->TargetX * TERRAIN_SCALE + TERRAIN_SCALE * 0.5f;
    c->Object.Position[1] =
        c->TargetY * TERRAIN_SCALE + TERRAIN_SCALE * 0.5f;
}

switch (Data->Action)
{
    case AT_ATTACK1:
    case AT_ATTACK2:
        SetPlayerAttack(c);
        c->AttackTime = 1;
        c->Object.AnimationFrame = 0.0f;
        break;
}

Why nearest tile? Committing to the current logical tile unconditionally can pull the model backwards. Committing to the next tile unconditionally can push it forward. Selecting the nearer of the active segment's two tile centres keeps the correction bounded and deterministic.

Smoother alternative: defer the attack to the tile boundary​

The coherent commit above is the safer correction for an old client. It may still cause a very small, bounded correction of less than roughly half a tile. If absolutely continuous motion is required, queue the attack until the current path segment reaches its next tile boundary:

Code:
// Pseudocode: this must be integrated at the local input/send layer.
if (NormalCrossbowAttackRequested() && Hero->Movement)
{
    PendingCrossbowAttack.Store(targetKey, angle, action);
    return; // Do not start/send it in the middle of a path segment.
}

// Called from movement update after a segment reaches a tile centre.
if (PendingCrossbowAttack.Active() && HeroReachedTileBoundary())
{
    StopPathCoherentlyAtCurrentTile(Hero);
    SendAndStartPendingCrossbowAttack();
    PendingCrossbowAttack.Clear();
}

This gives the cleanest visual result, but it changes input timing and requires careful cancellation when the target dies, leaves range, becomes invalid, or the player issues another command. Implement it only after the coherent nearest-tile fix is verified.

If only .dll can be modified​

The DLL hook must no longer save and restore only the rendered X/Y coordinate. Instead, calculate the committed tile before the original action handler and make it the action handler's target:

Code:
void __cdecl HookedReceiveAction(BYTE* buffer, int size)
{
    LPPRECEIVE_ACTION packet =
        reinterpret_cast<LPPRECEIVE_ACTION>(buffer);

    CHARACTER* c = FindCharacterFromActionPacket(packet);

    if (IsLocalMovingElfCrossbowNormalAttack(c, packet))
    {
        // Updates PositionX/Y, TargetX/Y, Path and Object.Position X/Y.
        // The original handler will now snap to this same committed tile,
        // so it cannot create either the old jump or the later slide.
        CommitToNearestPathTile(c);
    }

    OriginalReceiveAction(buffer, size);
}

If calling the shared helper is impossible from the DLL, reproduce the same state writes using the exact structure layout of that Main. The important part is not the hook library; it is updating the complete movement state atomically.

Build-specific warning: locate the handler and structure offsets in the exact target main.exe. Never reuse addresses, field offsets, calling conventions, or packet definitions from another build without verification.


Keep the monster correction​

The monster result is strong evidence that the earlier playback-speed fix was correct. Do not revert it. In the Season 6 example, the problematic pattern is:
Code:
w->PlaySpeed =
    Models[MODEL_PLAYER].Actions[PLAYER_ATTACK_BOW].PlaySpeed;

for a branch containing both bow and crossbow actions. It should use the actual current action or an explicit bow/crossbow mapping:
Code:
w->PlaySpeed =
    Models[MODEL_PLAYER].Actions[o->CurrentAction].PlaySpeed;

That correction addresses weapon/character animation synchronization. The path-commit correction addresses movement interruption. Both fixes are needed because they solve different layers of the same visible symptom.

If it doesn’t work out, send over the source code, Johnny. We’ll sort it all out.
 
Last edited:
Upvote 0
A storm caused to break one of my hard drives, so my setup is messed up right now., I ordered a new one, hopefully I will have my dev env ready as soon as possible to test your suggestions and I have the sources but your dll segments are greatly appreciated for people looking to hook the main with a dll

UPDATE: I applied your suggestions and indeed it only slide every now and then, still there is a slide though with the "nearest tile" approach so I will give you my sources:

Client S5 and server ready to mount:

Sources of Client and Server:
 
Last edited:
Upvote 0
A storm caused to break one of my hard drives, so my setup is messed up right now., I ordered a new one, hopefully I will have my dev env ready as soon as possible to test your suggestions and I have the sources but your dll segments are greatly appreciated for people looking to hook the main with a dll

UPDATE: I applied your suggestions and indeed it only slide every now and then, still there is a slide though with the "nearest tile" approach so I will give you my sources:

Client S5 and server ready to mount:

Sources of Client and Server:
I’ll only be able to do it in a couple of days.
 
Upvote 0
Hey, Houston, I've got for you. Here you go. Hope everything will be OK. If not, you know where to find me.

I finally found the real cause of the remaining slide and fixed it in the sources.

The fixes removed most of the problem, but the residual slide had a different cause: while walking, the client advances the logical PositionX/Y to the next tile before the model visually reaches it. If the Elf attacks at that moment, the client sends this forward position to the server, and PushingCharacter then pulls the model toward it — causing the slide.

I fixed it by committing the hero to the nearest actual path tile before sending the position and when MOVEMENT_ATTACK starts. Now the rendered position, logical position, and server position all match.


The fix is client-side only and doesn't affect monsters, other players, or attack range logic.
 
Upvote 0
Glad to see you back. I tested the new changes but unfortunately now it teleports to the tile instead of sliding. I think that queueing the attack maybe the solution but as you said we must be careful to cancel the attack when the target dies, leaves range, becomes invalid, or the player issues another command.
 
Upvote 0
What the bug was
While the hero walks a path, MovePath() works like this: the logical position (PositionX/PositionY) jumps to the next path tile at the very start of each segment, while the rendered model is still smoothly interpolating toward it. So during movement these two positions are almost a full tile apart, and they coincide for only one frame — when the path segment wraps (the tile boundary).The server was always receiving correct coordinates — the problem was purely visual, purely local.All previous fix attempts did the same thing: they force-snapped the model to a tile at the moment of attack (CommitToNearestPathTile):
snapping before sending the packet → the model jumps one tile forward = teleport;
snapping after the server reply → the model gets dragged back = slide.
Any such "jump" mid-segment is visible to the eye. There was also a second source of the teleport: when the server echoed your own attack back (ReceiveAction), the client snapped the model to the packet tile again.

How I fixed it
I don't move or snap anything. Instead:
Attack queue. If an Elf with a bow/crossbow clicks a target while moving, the shot doesn't fire immediately — it goes into a queue (g_DeferredAttack).
Every frame, MoveHero() calls UpdateDeferredAttack(), which re-validates the queued target: is it still alive, still in range, etc.
The shot fires on the tile-boundary frame — the only moment where the rendered model, PositionX/Y and the coordinates in the server packet already agree. There is nothing to correct → no teleport, no slide. Visually the Elf takes one or two more steps, stops exactly on a tile center and shoots. The shot goes through the exact same code as a stationary attack — the server just sees an ordinary SendRequestAttack packet.

Give it a try. Hopefully, everything will work properly this time.
 

Attachments

Upvote 0
I tested your changes and the snapping seems to be fixed, tested bow and crossbow separately, I mean I made some changes to your code to only affect when an elf is equipped with a crossbow so I can test both quickly. Upon further testing crossbow only issue now is that it prefers to attack at melee range, rarely it engages at range while moving.

But this approach is the correct since I saw that the game only attacks at range with a bow on a moving Elf if it is on the center of a tile, if not, it delays the attack until it reach visually the center of a tile. But while the bow triggers the ranged attack the crossbow more often than not pursues the enemy until it is at melee range and only attacks if it is on a center of a tile when you clicked the Left button. Thanks again for all your efforts
 
Upvote 0
I tested your changes and the snapping seems to be fixed, tested bow and crossbow separately, I mean I made some changes to your code to only affect when an elf is equipped with a crossbow so I can test both quickly. Upon further testing crossbow only issue now is that it prefers to attack at melee range, rarely it engages at range while moving.

But this approach is the correct since I saw that the game only attacks at range with a bow on a moving Elf if it is on the center of a tile, if not, it delays the attack until it reach visually the center of a tile. But while the bow triggers the ranged attack the crossbow more often than not pursues the enemy until it is at melee range and only attacks if it is on a center of a tile when you clicked the Left button. Thanks again for all your efforts



The crossbow attack queue was bypassing the normal ranged path calculation, causing the Elf to pursue targets into melee range. Position snapping also caused sliding or teleporting.

I removed the snapping logic, preserved the crossbow’s 6-tile attack range, delayed attacks until the nearest tile center, and prevented the server response from repositioning the local model.

The Elf should now stop smoothly at a tile center and fire the crossbow at range without stuttering, sliding, teleporting, or moving into melee range. :msmile:
 
Upvote 0
Thank you for all your efforts in this matter, I think Webzen screw up this beyond repair, your last corrections seems to be closer to what the game intended to be so I will take that as a solution

Hey, Houston! So, as I understand it, we actually pulled it off? 😄 What exactly did WebZen mess up? I didn’t quite get that part. But judging by your reaction, you seem pretty happy with the result overall... :sneaky:
 
Upvote 0
I mean, Webzen had the bow attack sorted out, why they didn't implemented the crossbow in that way too, I will never understand. I guess they didn't have beta testers as half the gameplay of the Elf characters is attacking with a crossbow and this wouldn't have passed a basic quality control.

While your changes mostly of the time addresses this issue, they are still sometimes that the Elf prefer to follow the monster and path to its back before attacking at melee range, but I can live with it. Also the change make the Bow attack teleport sometimes so I changed the code to only affect the Elf with a crossbow, again thank you for all the effort you put into this...
 
Upvote 0
Hey dude, I like to see things through to the end, so...

The deferred attack fires only when the distance to the tile center is no greater than CharacterMoveSpeed(c) + 1, normally 13–16 units. However, the original MovePath() completes the segment at <= 20 units. Between 17 and 20 units, the queue does not fire, but movement already changes the path phase from 3 → 0. The attack boundary is missed and the Elf continues walking.

In addition, the mouse handler initially creates a path with fDistance = 0, leading directly to the monster's tile. If the boundary is missed, this route takes the Elf into melee range. The pathfinder does not consider the monster's facing direction, so it may route the Elf beside or behind the monster.

Fix 1: use MovePath's boundary threshold​

In IsApproachingTileCentre(), replace:​

C++:
const float nextStep = CharacterMoveSpeed(c) + 1.0f;
return dx * dx + dy * dy <= nextStep * nextStep;

with:
C++:
const float boundaryThreshold = 20.0f;
return dx * dx + dy * dy <=
    boundaryThreshold * boundaryThreshold;
The queue now fires on the same frame in which MovePath() is about to complete the segment. StopAtCurrentTileCentre() then aligns the rendered and logical positions before starting the attack.

Fix 2: do not route a crossbow user to the monster's tile​

In the mouse attack handler, replace:
C++:
PathFinding2(c->PositionX, c->PositionY,
    TargetX, TargetY, &c->Path)

with a weapon-specific path distance:
C++:
const bool isCrossbow =
    gCharacterManager.GetEquipedBowType() == BOWTYPE_CROSSBOW;

const float pathDistance =
    isCrossbow ? CROSSBOW_ATTACK_RANGE : 0.0f;

PathFinding2(c->PositionX, c->PositionY,
    TargetX, TargetY, &c->Path, pathDistance)

The crossbow path now ends on a tile within its six-tile attack radius, instead of on the target's tile. Bow behavior remains unchanged.
 

Attachments

Last edited:
Upvote 0
Sure buddy, I am interested to see this issue fixed as well, is almost there... last test works 90% of the time but...



I honestly don’t see the problem. You mean she doesn’t shoot across half the map like Legolas? I’ve never seen her do that anyway — only with a skill.

Or maybe I’m still half asleep and missed something?
 
Upvote 0
Towards the end of the video she gets stuck following the monster on melee without ever attacking him

Only ZzzInterface.cpp was changed.

Short explanation: previously, every PathFinding2() == false result cancelled the attack, even though false can also mean that the character is already within attack range. The stale path remained active, causing the Elf to move back and forth without attacking.

The fix prevents active paths from being rebuilt every frame, handles the one-node “already in range” result as a valid attack, and clears genuinely unavailable stale paths before retrying with a short delay.


Old code:

C++:
if (PathFinding2(c->PositionX, c->PositionY,
    targetX, targetY, &c->Path, CROSSBOW_ATTACK_RANGE))
{
    c->Movement = true;
}
else
{
    CancelDeferredAttack();
}

Replacement:

C++:
if (c->Movement && c->Path.PathNum > 0)
{
    return false;
}

if (g_DeferredAttack.RepathDelay > 0)
{
    g_DeferredAttack.RepathDelay--;
    return false;
}

if (PathFinding2(c->PositionX, c->PositionY,
    targetX, targetY, &c->Path, CROSSBOW_ATTACK_RANGE))
{
    c->Movement = true;
    g_DeferredAttack.RepathDelay = 6;
    return false;
}

const float dx = static_cast<float>(c->PositionX - targetX);
const float dy = static_cast<float>(c->PositionY - targetY);

if (dx * dx + dy * dy <=
    CROSSBOW_ATTACK_RANGE * CROSSBOW_ATTACK_RANGE)
{
    CancelDeferredAttack();
    g_FiringDeferredAttack = true;
    g_DeferredLogicalRangeFallback = true;

    Action(c, o, true);

    g_DeferredLogicalRangeFallback = false;
    g_FiringDeferredAttack = false;
    return true;
}

c->Path.PathNum = 0;
c->Path.CurrentPath = 0;
c->Path.CurrentPathFloat = 0;
c->Movement = false;
g_DeferredAttack.RepathDelay = 6;

return false;

All required supporting fields and checks are already included in the provided file.
 

Attachments

Upvote 0
Status
Not open for further replies.
Back