GiveBadge could treat a missing offline user as eligible for a badge and insert through a nullable user subquery. Depending on SQL mode this could fail late or persist an orphaned user_id value. Resolve the offline user first, return HABBO_NOT_FOUND when absent, and insert badges with the resolved user id only.
SetMotto updated the in-memory motto and then unconditionally broadcast RoomUserData through the current room. Online users without a current room could throw a null-pointer exception after the state change, making the RCON call report an error despite mutating the user. Only broadcast room data when a room is present and cover the invariant with a contract test.
GiveRespect inverted the offline SQL parameters for respects_given and respects_received. Online users received the intended counters, but offline users had the two persisted counters swapped. Bind respect_given to respects_given and respect_received to respects_received, with a contract test to keep the RCON offline path aligned.
GiveCredits treated offline UPDATE execution as success without checking whether any user row was changed. Nonexistent user ids could therefore return an offline success response while granting nothing. Use executeUpdate(), return HABBO_NOT_FOUND when no row is affected, and keep SQL errors from falling through to the offline success message.
RCONServerHandler released the inbound ByteBuf only after successfully parsing, writing, flushing, and closing the response. Any exception before the tail release could leak Netty buffers and let malformed RCON traffic consume memory over time. Guard non-ByteBuf messages, release accepted buffers from a finally block, and add a contract test for the release invariant.
RCON GivePixels previously used an UPDATE for offline users, so users without an existing users_currency type 0 row received no pixels while the command still returned success. Match the GivePoints and housekeeping paths with an upsert and add a contract test that keeps offline pixel grants creating missing currency rows.
Expose whether a marketplace offer was persisted before mutating inventory state, refuse sells whose database insert failed, and synchronize the sold timestamp into the online seller's in-memory offer when present. This keeps failed or racing marketplace operations from desynchronizing credits/items.
Use executeUpdate with generated keys for offline ban inserts, return an empty result when an offline target cannot be loaded, and make ban commands handle empty results instead of indexing blindly. Modtool chatlog requests now guard missing users instead of dereferencing null.
Add a forced dispose path for bans, RCON disconnects, logout/account endpoints, plugin-cancelled login, duplicate login replacement, and late MAC-ban enforcement. Soft channel closes still park a session for reconnect, while security-driven closes now bypass session resume. Also null-guard client/channel disposal and cover the contract with focused tests.
The Nitro renderer sends the UniqueID (machine fingerprint) packet right AFTER the SSOTicket, so Habbo.connect() ran before the machineId was set and returned false on the empty machineId — silently disposing the client (WS closed with Netty's default "Bye"), so login never completed and no SecureLoginOK was sent.
- Habbo.connect(): only set machineID + run the MAC-ban check when the fingerprint is already present; never reject the login solely for a missing machineId (also drop a duplicated MAC/IP-ban block).
- MachineIDEvent: enforce the MAC ban when the fingerprint arrives after login, preserving the ban check that connect() now defers.
Add commented examples for the config keys introduced by this PR so operators
can discover and tune them (defaults apply if unset):
- ws.ip.header.trusted (trusted reverse-proxy gate for the forwarded-IP header)
- io.packet.handler.threads (game packet-handler pool, off the Netty I/O loop)
- auth.http.pool.size (dedicated /api/auth/* worker pool)
- io.netty.allocator.pooled (opt-in pooled ByteBuf allocator)
Modernization following the dependency upgrades:
- Joda-Time was used in exactly one place (ModToolSanctionInfoComposer, to
subtract probation days from a Date). Migrated to java.time
(Instant/ZoneId.systemDefault, calendar-accurate like the old Joda call) and
removed the joda-time dependency entirely — confirmed gone from the shaded jar.
- Make string<->bytes conversions explicitly UTF-8 instead of relying on the
platform default. Most importantly the wire codec (ClientMessage.readString /
ServerMessage.appendString) — both sides now pinned to UTF-8 so international
characters are robust regardless of -Dfile.encoding. Also RCONServerHandler,
PluginManager and the WS origin-forbidden response.
Verified: clean compile, 15/15 tests, shaded jar.
Netty 4.2 deprecates NioEventLoopGroup in favour of the generic
MultiThreadIoEventLoopGroup driven by an IoHandlerFactory. Server.java now
builds its boss/worker groups with MultiThreadIoEventLoopGroup(..,
NioIoHandler.newFactory()) — functionally equivalent, and the codebase now
compiles with zero deprecation warnings. Verified: compile, 15/15 tests, shaded
jar.
Major-version upgrades of the three the previous bump deliberately held back.
Verified: clean compile, all 15 tests run green (surefire 3.5.2 drives the JUnit 6
platform fine — no extra launcher dep needed), and the shaded jar assembles.
- io.netty:netty-all 4.1.135.Final -> 4.2.15.Final
- com.zaxxer:HikariCP 6.3.3 -> 7.0.2
- org.junit.jupiter:junit-jupiter 5.14.4 -> 6.1.0
Notes:
- Stayed on Netty 4.2 (GA), not 5.0 which is still Alpha. No source changes
needed; the channel ALLOCATOR is set explicitly so 4.2's new default adaptive
allocator doesn't apply. NioEventLoopGroup is deprecated in 4.2 but still
functions as before (left as-is to avoid an event-loop behavioural change).
netty-all 4.2 pulls more transitive modules, so the fat jar grows (~20->32 MB).
- HikariCP 7.x baselines Java 17; our HikariConfig usage is unchanged.
#2 — tunable thread pools (sensible defaults kept):
- io.packet.handler.threads overrides the packet-handler EventExecutorGroup size
(default max(16, 2x cores)).
- auth.http.pool.size overrides the auth HTTP pool max threads (default 16).
#5 — Netty buffer pooling:
- Make the crypto handlers pool-safe: GameByteEncryption/GameByteDecryption no
longer call ByteBuf.array() on a readBytes-derived buffer (whose arrayOffset is
non-zero under a pooled allocator, which would have read/encrypted the wrong
region). They now copy the readable region into a plain byte[] (offset-safe)
and wrap the result — also drops one intermediate buffer allocation. This is
correct for the current unpooled allocator too. (ServerMessage uses its own
Unpooled buffer, and ClientMessage reads via buffer methods, so both are
already offset-safe.)
- Add a shared channel allocator selected by io.netty.allocator.pooled
(default false = unpooled-heap, unchanged). Set true for a pooled HEAP
allocator (preferDirect=false, so array-backed paths keep working) to cut
per-packet alloc/GC churn. Opt-in until validated under load with the Netty
leak detector, since unreleased pooled buffers accumulate rather than being
GC-reclaimed.
New optional config keys (insert into emulator_settings to set/silence the
"key not found" notice): io.packet.handler.threads, auth.http.pool.size,
io.netty.allocator.pooled.
Security:
- HousekeepingAuditLog: append-only audit trail of privileged actions. There was
no record of which operator granted ranks/currency to whom. SetUserRank,
GiveCredits and GiveCurrency now log operator id+name, action, target, detail
and IP. Writes are async; the housekeeping_log table is created on first use
(CREATE TABLE IF NOT EXISTS) so no manual migration is needed.
Speed (minor):
- RCONServerHandler / PluginManager: reuse a shared Gson instead of allocating a
new parser per request/plugin-config load (Gson is thread-safe). The wired
Gson builders were already cached singletons.
Security / speed:
- New util SqlLikeEscaper: escapes %, _ and \ in user search input. Applied to
the user-facing LIKE searches (messenger user search, marketplace search,
furni-editor search, housekeeping room search, guild member search) so a query
like "%" can no longer match everything or trigger a needless full scan, and
usernames containing "_" are matched literally.
Stability (item-loss fixes):
- RecycleEvent: compute the recycler reward BEFORE consuming the 8 inputs. The
inputs were deleted from the DB first, so a null reward (misconfig) destroyed
them permanently with nothing back. Now the inputs are only removed once the
reward is confirmed.
- CraftingCraftItemEvent: restore the pulled ingredients to the inventory if the
recipe can't be completed (not enough ingredients mid-pull, or reward creation
returns null) — previously they silently vanished from the inventory.
Regressions found by an adversarial review of this branch's own diff:
- RoomCycleManager: stop holding the currentBots/currentPets monitor across the
whole bot/pet tick — snapshot under the lock then cycle off-lock. The previous
fix blocked place/pickup and room dispose for the full tick and inverted lock
order vs roomUnitLock->currentBots (latent deadlock for any future cycle code
touching roomUnitLock).
- HabboInfo: complete the currencyLock invariant — getCurrencies() now returns a
snapshot under the lock (UserInfoCommand iterated the live Trove map off-lock,
the exact rehash corruption the lock guards); canBuy() uses the lock-guarded
getCredits()/getCurrencyAmount(); run() reads credits under the lock for save.
- RoomSpecialTypes: synchronize the by-id pet getters (getNest/getPetDrink/
getPetFood/getPetToy/getPetTree) to match their now-synchronized mutators.
- AuthHttpUtil.isTrustedProxy: exact-match trusted IPs; only treat an entry as a
range when it ends with "."/":" so "10.0.0.1" can't also trust "10.0.0.12".
Pre-existing logic bugs found by the deep subsystem analysis:
- RoomUsersComposer: the bulk (room-entry) branch wrote the guild id twice;
the second field must be the 1/-1 group-membership flag (matches the single
branch) — every user showed a wrong group indicator on room entry.
- BotManager.pickUpBot: room owners (and ACC_PLACEFURNI) couldn't pick up bots
placed in their own room — added the room-owner clause that placeBot has.
- PetPickupEvent: compared user id to pet.getId() instead of pet.getUserId(), so
a pet owner who isn't the room owner couldn't pick up their own pet.
- RoomRightsManager.refreshRightsForHabbo: in guild rooms, explicit room_rights
were stripped (overwritten by guild level NONE); now takes the stronger of
explicit rights and guild level.
- RoomRequestBannedUsersEvent: `!hasRights || !ACC_ANYROOMOWNER` required BOTH,
denying legitimate owners the banned-users list — corrected to `&&`.
- InteractionPetBreedingNest.breed: a crafted packet on a not-full nest deleted
the nest furni then NPE'd (furni loss); guard petOne/petTwo/room before the
destructive delete; ConfirmPetBreedingEvent null-checks the room.
- WiredEffectTeleport/UserFurniBase: appended item id instead of sprite id in the
incompatible-triggers list (cosmetic wired-dialog mismatch) — matched the ~10
other effects' getBaseItem().getSpriteId() convention.