- Room Cleanup Optimization (RoomManager.java)
Added roomsByOwner ConcurrentHashMap that tracks which rooms belong to which owner
clearInactiveRooms() now iterates unique owners instead of ALL rooms
Went from O(rooms × clients) to O(unique_owners × clients) every 120s
- Volatile Fields (Room.java)
Removed volatile from 27 room config fields (score, category, chatMode, allowPets, etc.)
Kept volatile only on 8 fields that genuinely need cross-thread visibility (loaded, preLoaded, needsUpdate, muted, etc.)
Reduces CPU cache line invalidation on every room cycle tick
- Search Cache TTL (SearchUserEvent.java + CleanerThread.java)
SearchUserEvent now has 30-second TTL per entry instead of full wipe every 10s
SearchRoomsEvent already had LRU eviction (max 200) — removed redundant .clear() call
Frequently searched users stay cached, only stale entries get cleaned
- scheduledComposers/scheduledTasks — After reading the code, these are actually already handled correctly: processScheduledTasks() swaps the set with a fresh one before processing, and processScheduledComposers() calls .clear() after sending. No leak risk.
1. HabboManager - O(1) username lookup
Before: getHabbo(String) held a synchronized lock and iterated ALL online users every call
After: Secondary ConcurrentHashMap<String, Habbo> keyed by lowercase username → instant get() lookup, no lock contention
2. ItemsComponent - Batch DB saves
Before: dispose() spawned a separate thread per dirty item, each opening its own DB connection
After: Single connection, JDBC addBatch()/executeBatch() for both UPDATE and DELETE, flushed every 100 items. A user with 500 dirty items now does 5 batch executions instead of 500 thread spawns + 500 connections.
3. AcceptFriendRequestEvent - N+1 elimination
Before: For each offline user: query 1 (getOfflineHabboInfo by ID) → query 2 (load full Habbo by username) = 2 queries × up to 100 users = 200 queries
After: Single query by user ID directly = 1 query × up to 100 users = 100 queries (50% reduction)
4. RoomManager - Direct HashMap lookup
Before: getCategory(int id) iterated all categories checking getId() == id even though the map is already keyed by ID
After: Direct roomCategories.get(id) → O(1) instead of O(n)
The Nitro client's NodeData parser enforces safety limits (max 1000 offers,
500 children, 20 depth) using Math.min() on the count but the server was
sending all data beyond those limits. The unread bytes left in the buffer
corrupted parsing of subsequent nodes, causing RangeError.
Cap server-side output to match client limits and snapshot offerIds array
to prevent potential race conditions between size() and iteration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- SQL injection fix: Replaced string concatenation of minPrice/maxPrice with proper parameterized ? placeholders using a paramIndex counter
- IDOR fix: Added user_id ownership check in MarketPlace.takeBackItem() — now verifies the DB user_id matches the requesting player before allowing item retrieval, with warning log on mismatch