Move voucher exhaustion checks and history persistence behind a synchronized per-voucher claim path. Rewards are now applied only after the history row is inserted successfully, preventing duplicate or failed-claim redemption from granting credits, points, or catalog items.
Adds a contract test for claim ordering. Maven verification was attempted but blocked by sandbox network/plugin resolution after escalation usage was exhausted; diff --check passes.
FurniEditorUpdateFurnidataEvent (10046) was edit-only: FurnidataWriter.write()
refuses classnames absent from furnidata, so a furni with no entry showed the
DB-fallback name with locked fields and "Classname not found". Make it an upsert:
- FurnidataWriter.create(): append a complete entry (JSON5-preserving, atomic +
backup) into the matching roomitemtypes/wallitemtypes furnitype array; guards
against duplicate classname (ALREADY_EXISTS) and id collision (ID_COLLISION);
split-tier writes to items.furnidata.create_tier (default "custom", file
created with a shell if absent), single-file writes to the source.
- FurnidataEntryBuilder: build the complete entry from the item's items_base row
(id = sprite id, classname, type-driven section, xdim/ydim, canstandon/
cansiton/canlayon, name/desc, sane defaults matching existing entries).
- Handler: on write()==false, load the Item, build + create the entry, map
CreateResult to a precise message; then the existing reindex + 10047 broadcast
+ public_name mirror run for both paths; audit action is "create" vs "edit".
No renderer change, no new packet. Pairs with the client unlocking name/desc when
the entry is missing (separate Nitro-V3 change).
Resolve furnidata from the renderer config and asset base before falling back to the legacy items.furnidata.path override. This keeps the emulator aligned with the same furnidata URL the UI/renderer already consume.
Keep the legacy path as a compatibility fallback for older installs, but stop exposing absolute furnidata file paths in the startup log. The provider now reports a compact manager-style source label instead.
Add coverage proving renderer-config furnidata.url wins over the legacy path when both are present.
Guard the guild acceptance update with level_id = REQUESTED so a stale or concurrent accept cannot promote a membership row that has already changed state.
Tests: mvn '-Dtest=GuildManagerMembershipContractTest,GuildMembershipManagementContractTest,GuildMembershipRequestContractTest' test
Guard RoomTradeManager.startTrade while holding the activeTrades lock so concurrent trade starts cannot register the same participant in multiple active trades before room status updates settle.
Add a contract test covering the lock-scoped participant guard and keep the existing trade safety tests green.
Room owners can remove bots from their room, but picking up another user's bot must return it to the original owner instead of transferring ownership to the picker.
Tests: mvn -Dtest=BotPickupOwnershipContractTest test; mvn -DskipTests package
Redeeming clothing furniture now inserts the wardrobe grant before removing/deleting the voucher furniture. If the DB insert fails, the item remains in the room and the in-memory wardrobe is not updated.
Tests: mvn -Dtest=RedeemClothingContractTest test; mvn -DskipTests package
Keep the housekeeping rank ceiling for normal staff, but treat the highest configured rank as the core rank so rank 7 can act on other rank 7 users without opening peer actions for lower staff ranks.
Tests: mvn '-Dtest=HousekeepingTargetRankGuardContractTest,HousekeepingMutationGuardTest,HousekeepingSetUserRankEventTest,HousekeepingTargetRankGuardContractTest' test
Deduct the computed rent cost when a user rents an InteractionRentableSpace. The previous flow only checked that the user had enough credits, then marked the space as rented without charging them, allowing free weekly rentals.
Honor ACC_INFINITE_CREDITS for staff accounts and add a contract test that keeps the charge before the rented state is assigned.
Reject monsterplant seed redemption when the caller does not own the placed seed. Without this guard, a user in the same room could trigger ToggleFloorItemEvent against another user's seed and have the server delete that item while creating the monsterplant pet for the attacker.
Add a contract test covering the ownership guard before createMonsterplant is reached.
Reject client-supplied room ids for self-moderation packets unless they match the caller's current room. This prevents users with saved rights or ownership in another room from muting, banning, or unbanning users remotely via crafted packets.
RoomUserBanEvent now also ignores invalid ban type values instead of letting valueOf throw through the message handler.
Add a contract test covering ban, mute, and unban current-room scoping.
Route console log level and logger columns through custom Logback converters so terminals with ANSI support get colored severity badges and compact colored class names.
Keep the same habbo.console.style auto/ansi/plain behavior as the startup splash, including plain fallback for non-interactive output, NO_COLOR, and legacy Windows console paths.
The file appenders keep their existing verbose patterns unchanged, so debug/error log files remain plain and grep-friendly.
Cover the level formatter, logger formatter, override behavior, and Logback pattern wiring with tests.
Add an auto-detected styled startup splash for terminals that support ANSI colors, including Windows Terminal, ANSICON, ConEmu ANSI, and common TERM-based consoles.
Keep the default and redirected-output path plain text so legacy CMD, logs, and service wrappers remain readable. The style can also be forced with -Dhabbo.console.style=ansi or disabled with -Dhabbo.console.style=plain.
Cover the styled splash, Windows Terminal detection, non-interactive fallback, and forced plain mode with startup console tests.
Keep the Morningstar ASCII logo while using a structured plain-text startup card that works in CMD, Windows Terminal, and other consoles without ANSI support.
Compact the Logback console pattern to use simple class names, clean separators, and a wider message column so startup logs do not wrap as aggressively. Simplify Infostand startup output to a one-line asset count while preserving category breakdown at DEBUG level.
Also normalize generic server start/stop messages so Game Server and RCON Server are labeled correctly instead of being glued to host:port output.
Replace the temporary ASCII-art banner with a structured startup splash that uses plain ASCII, aligned fields, and no ANSI or terminal-specific features. This keeps the emulator startup readable across CMD, PowerShell, Linux terminals, Docker logs, CI output, and copied log files. Add a contract test to keep the splash universal.
Add a clean ASCII startup banner for the emulator CMD window and use it instead of the legacy wide block logo. The new banner stays ASCII-only for Windows console compatibility and keeps the Morningstar identity visible before the startup logs.
Shorten the infostand background startup message into a compact asset summary and print the project/version/build details as a single ASCII startup card instead of several timestamped log lines. Add a small contract test for the compact infostand summary format.
PacketNames reflects public static final packet constants and warns when two names share the same header. RequestCatalogIndexEvent is a legacy alias for the active Builders Club catalog index header, and InClientLinkComposer shares the NUX link payload/header. Keep those aliases available to existing code while removing them from the reflected packet-name set, and add a contract test so future public final packet names stay unique.
MarketPlace.getCredits previously removed sold offers from memory and granted credits before knowing whether marketplace_items.user_id had been detached in the database. If that update failed, the same sold offer could be loaded as claimable again later. Make removeUser report success, keep the offer claimable on failure, and only grant credits after the database detach succeeds.
RoomTrade previously caught SQLException during ownership updates but continued into the in-memory inventory and credit transfer path. That could desynchronize or duplicate trade results if the database batch failed while the live session still completed the exchange. Keep item owner mutations after the successful batch, return offered items on failed completion, and add a contract test that prevents SQL failures from falling through to the transfer path.
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.
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.
#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.