Skip to content

Fix ConcurrentModificationException in MonsterInformationProvider caches - #352

Open
asaf-shitrit wants to merge 1 commit into
P0nk:masterfrom
asaf-shitrit:fix/continent-drops-concurrent-map
Open

asaf-shitrit wants to merge 1 commit into
P0nk:masterfrom
asaf-shitrit:fix/continent-drops-concurrent-map

Conversation

@asaf-shitrit

@asaf-shitrit asaf-shitrit commented Sep 14, 2026 •

Copy link
Copy Markdown

The bug

MonsterInformationProvider.continentDrops is a plain HashMap, populated lazily via computeIfAbsent in getRelevantGlobalDrops:

private final Map<Integer, List<MonsterGlobalDropEntry>> continentDrops = new HashMap<>();
...
public final List<MonsterGlobalDropEntry> getRelevantGlobalDrops(int mapId) {
    final int continentId = mapId / 100000000;
    return continentDrops.computeIfAbsent(continentId, this::loadContinentDrops);
}

This is called from MapleMap.dropFromMonster, which runs on channel threads during monster-death handling (killMonster -> damageMonster -> dropFromMonster). When monsters in the same continent die concurrently on different channel threads, they race on computeIfAbsent for the same continentId key, and a plain HashMap isn't safe for concurrent structural modification:

java.util.ConcurrentModificationException
    at java.base/java.util.HashMap.computeIfAbsent(HashMap.java:1229)
    at server.life.MonsterInformationProvider.getRelevantGlobalDrops(MonsterInformationProvider.java:81)
    at server.maps.MapleMap.dropFromMonster(MapleMap.java:773)
    at server.maps.MapleMap.killMonster(MapleMap.java:1445)
    at server.maps.MapleMap.killMonster(MapleMap.java:1361)
    at server.maps.MapleMap.damageMonster(MapleMap.java:1319)

Why it matters: the getRelevantGlobalDrops call sits near the top of dropFromMonster, before any drop item is created. When it throws, the exception propagates out of the whole method and aborts the drop routine entirely - the monster's normal drops, quest drops, and global drops are all silently lost. Nothing rolls back or retries; the only trace is the stack trace in the log.

Sibling fields

mobAttackInfo, mobBossCache, and mobNameCache are the same pattern: plain HashMaps used as lazy caches, read and written from packet handlers on channel threads (Monster attack info lookups, isBoss() checks, getMobNameFromId from mob-vs-mob damage handlers). They have the same concurrent-access exposure as continentDrops, so they're fixed the same way here.

Other HashMap fields in the same class (drops, dropsChancePool, extraMultiEquipDrops, mobAttackAnimationTime, mobSkillAnimationTime) were left alone - out of scope for this fix, and would need their own look.

The fix

Switch the four affected fields to ConcurrentHashMap. loadContinentDrops only reads globaldrops and doesn't mutate continentDrops itself, so computeIfAbsent on a ConcurrentHashMap is safe here (no re-entrant modification). clearDrops()'s continentDrops.clear() still works the same way.

How this was found

Found while running multiple automated bot clients that killed monsters concurrently, much faster than a human plays - over 8 minutes of continuous concurrent kills, not a single expected drop appeared. That's also why this is rarely seen in normal play: it needs genuinely concurrent kills in the same continent to race.

Testing

  • mvn compile succeeds
  • mvn test passes (no existing tests cover this class)

continentDrops was a plain HashMap accessed via computeIfAbsent from
getRelevantGlobalDrops, which runs on channel threads during monster
death. Concurrent kills on maps in the same continent race on the same
map entry and can throw ConcurrentModificationException. That call sits
at the top of MapleMap.dropFromMonster, before any item is created, so
the exception aborts the whole drop routine and the monster's drops are
silently lost.

mobAttackInfo, mobBossCache and mobNameCache are the same pattern
(lazy caches populated from packet handlers on channel threads), so
they get the same treatment. loadContinentDrops doesn't mutate the map
it's called from, so switching to ConcurrentHashMap is a safe drop-in
fix.

Found while running multiple automated bot clients killing monsters
concurrently, much faster than human play - which is why this is rarely
seen in normal play.
MinatoNami added a commit to MinatoNami/Cosmic-AI that referenced this pull request Sep 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant