L2JMobius
Public Development => Bug Reports => Topic started by: zTkenai on February 21, 2026, 03:32:22 AM
-
Chronicle: Grand Crusade
Bug Description:
There is a critical issue with the quest completion window. After finishing any quest, the "Complete Quest" button (or the reward button) does not disappear immediately after the first click. This allows players to click the button multiple times before the window closes, receiving the rewards (EXP, SP, and Items) for every click.
Steps to reproduce:
Start and finish any quest.
Talk to the NPC to claim the reward.
Click the completion button rapidly multiple times.
Observe that the rewards are delivered multiple times into the inventory/character sheet.
Expected Result:
The quest window should close or the button should be disabled immediately after the first packet is sent to the server to prevent reward duplication.
-
Chronicle: Grand Crusade
Bug Description:
There is a critical issue with the quest completion window. After finishing any quest, the "Complete Quest" button (or the reward button) does not disappear immediately after the first click. This allows players to click the button multiple times before the window closes, receiving the rewards (EXP, SP, and Items) for every click.
Steps to reproduce:
Start and finish any quest.
Talk to the NPC to claim the reward.
Click the completion button rapidly multiple times.
Observe that the rewards are delivered multiple times into the inventory/character sheet.
Expected Result:
The quest window should close or the button should be disabled immediately after the first packet is sent to the server to prevent reward duplication.
I dident have this issue can you provide any video or quest example?
-
I recompile project wit Java 17 and 25, with game version Helios and Grand Crusade, with clients and system folders diferents, and same bug, when i finhish any quest, when i click to claim reward, if i click several times on button, i can receive reward for each click.
Example:
First quest when we born, i receive 70 exp for finhish, but if i click 3 times to claim reward, i receive 3 times 70exp. This behavior occurs for all quests.
i dont know how post one video here for exemple.
-
I'm still learning about this project, and I received help from an AI. See if this works:
Quest Completion Race Condition Fix
Problem
When a player clicks the "Complete Quest" button multiple times quickly, the server processes all concurrent requests in parallel. Multiple threads simultaneously pass the !isStarted() guard check in QuestState.exitQuest() (since the state is still STARTED for all of them), and each grants the full quest reward — resulting in players receiving the reward 5–10 times.
Root Cause
In
QuestState.java
, the private method
exitQuest(boolean repeatable)
(line 777) does:
java
if (!isStarted()) {
return; // guard check
}
// ... removes items, gives rewards, changes state to COMPLETED
There is no synchronization between the guard check and the state change. Two threads can both pass !isStarted() at the same time before either has changed _state to COMPLETED.
Proposed Changes
QuestState Core
[MODIFY]
QuestState.java
Add a synchronized(this) block to the private
exitQuest(boolean repeatable)
method so that the check-and-set of the quest state is atomic per
QuestState
instance.
diff
private void exitQuest(boolean repeatable)
{
if (_simulated)
{
return;
}
_player.removeNotifyQuestOfDeath(this);
- if (!isStarted())
- {
- return;
- }
+ synchronized (this)
+ {
+ if (!isStarted())
+ {
+ return;
+ }
+ // Mark as no longer started immediately to block re-entry
+ if (!repeatable)
+ {
+ _state = State.COMPLETED;
+ }
+ else
+ {
+ _state = State.CREATED; // temporary sentinel so re-entry fails
+ }
+ }
Wait — this approach is incomplete because after marking state we still need to remove items and save to DB. A cleaner approach is to use synchronized(this) around only the guard + state flip:
The safest, cleanest fix is wrapping the entire body of
exitQuest(boolean repeatable)
in synchronized (this), so only one thread can execute it at a time per quest state object:
java
private void exitQuest(boolean repeatable)
{
if (_simulated)
{
return;
}
synchronized (this)
{
_player.removeNotifyQuestOfDeath(this);
if (!isStarted())
{
return;
}
// Clean registered quest items
getQuest().removeRegisteredQuestItems(_player);
Quest.deleteQuestInDb(this, repeatable);
if (repeatable)
{
_player.delQuestState(_questName);
_player.sendPacket(new QuestList(_player));
}
else
{
setState(State.COMPLETED);
}
_vars = null;
}
}
NOTE
synchronized(this) uses the
QuestState
object as the monitor. Since each player has one
QuestState
per quest, this ensures only one thread can complete a given quest for a given player at a time. Other players' quests are completely unaffected (they use different
QuestState
objects).
Verification Plan
Automated Tests
No automated test suite was found that covers quest state logic. Manual in-game testing is the recommended verification approach.
Manual Verification
Start the game server in dev mode
Pick any quest and complete its objectives on a test character
Talk to the quest NPC and open the completion dialog
Click the "Complete Quest" button rapidly multiple times (5–10 clicks within 1 second)
Expected result: Only one reward is received (visible in inventory and system messages)
Before fix: Multiple copies of the reward would appear in inventory
IMPORTANT
This can also be simulated by sending multiple
RequestBypassToServer
packets with the quest completion bypass command programmatically (e.g., via a packet injector or custom client). Both approaches verify the same race condition path.
Quest Completion Race Condition — Fix Walkthrough
What was done:
1 file modified: QuestState.java
The private method `exitQuest(boolean repeatable)`
(line 777) received a `synchronized(this)` block around its entire logical body.
Diff applied
1
1
/*
2
2
* Copyright (c) 2013 L2jMobius
3
3
*
⋯ Expand 777 more lines
781
781
return;
782
782
}
783
783
784
_player.removeNotifyQuestOfDeath(this);
785
786
if (!isStarted())
784
synchronized (this)
787
785
{
788
return;
786
_player.removeNotifyQuestOfDeath(this);
787
788
if (!isStarted())
789
{
790
return;
791
}
792
793
// Clean registered quest items
794
getQuest().removeRegisteredQuestItems(_player);
795
796
Quest.deleteQuestInDb(this, repeatable);
797
if (repeatable)
798
{
799
_player.delQuestState(_questName);
800
_player.sendPacket(new QuestList(_player));
801
}
802
else
803
{
804
setState(State.COMPLETED);
805
}
806
807
_vars = null;
789
808
}
790
791
// Clean registered quest items
792
getQuest().removeRegisteredQuestItems(_player);
793
794
Quest.deleteQuestInDb(this, repeatable);
795
if (repeatable)
796
{
797
_player.delQuestState(_questName);
798
_player.sendPacket(new QuestList(_player));
799
}
800
else
801
{
802
setState(State.COMPLETED);
803
}
804
805
_vars = null;
806
809
}
807
810
808
811
/**
⋯ Expand 61 more lines
870
873
_simulated = simulated;
871
874
}
872
875
}
Why it works
Situation Before After
Player clicks 1x Quest completes normally ✅ Quest completes normally ✅
Player clicks 5-10x quickly 5-10 threads pass the !isStarted() guard and deliver multiple rewards ❌ Only 1 thread enters the synchronized state. The others wait in the queue and when they enter they find _state == COMPLETED, the !isStarted() passes and they return immediately without reward ✅
The synchronized monitor is the QuestState object itself
—which is unique per player per quest. This means:
No performance impact for other players (each has their own QuestState)
No deadlock possible (the lock is acquired and released in the same method)
Works for normal, repeatable, and daily quests (all go through the same internal method)
How to test:
Pick up any quest and complete the objectives.
Open the dialogue with the NPC to complete the quest.
Click the completion button 5-10 times quickly.
Check your inventory — the reward should only appear once.
-
I recompile project wit Java 17 and 25, with game version Helios and Grand Crusade, with clients and system folders diferents, and same bug, when i finhish any quest, when i click to claim reward, if i click several times on button, i can receive reward for each click.
Example:
First quest when we born, i receive 70 exp for finhish, but if i click 3 times to claim reward, i receive 3 times 70exp. This behavior occurs for all quests.
i dont know how post one video here for exemple.
Java 17 wtf? Curent version will not compile with java 17... so I have no idea where you get the source code but for sure not us. And again is a problema that only you face. We have a isSumltaniusTalk method to prevent exploits on quest...
-
Having a race condition in a place like this can mean two things.
Either there is something wrong with compiled files You are using, which means You don't really use gitlab provided sources, or You modified them to the point of breaking a natural flow of functions within threads.
Or You did not follow the instructions provided, or not really using L2jMobius sources.
Whatever it is, the fact that You are mentioning compiling with Java17 means You did not follow instructions.
There is nothing wrong with exitQuest and I've been trying to reproduce what You posted without any success.