From f2159e8901ca31b4b29eea10d798ba5b9e67f213 Mon Sep 17 00:00:00 2001 From: DuckieTM Date: Fri, 5 Jun 2026 21:42:01 +0200 Subject: [PATCH 01/38] =?UTF-8?q?=F0=9F=86=99=20=20typo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pixiPatch.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pixiPatch.ts b/src/pixiPatch.ts index 8fd5a9f..bea0f24 100644 --- a/src/pixiPatch.ts +++ b/src/pixiPatch.ts @@ -1,5 +1,3 @@ -actly once, idempotent across HMR reloads. - */ import * as PIXI from 'pixi.js'; type AnyFn = (...args: unknown[]) => unknown; From 322a4f368ca64d7207bfb1cb8d407014fce406e5 Mon Sep 17 00:00:00 2001 From: DuckieTM Date: Sat, 6 Jun 2026 07:10:26 +0200 Subject: [PATCH 02/38] =?UTF-8?q?=F0=9F=86=99=20Small=20update=20text?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/configuration/UITexts_en.json5.example | 4 +--- public/configuration/UITexts_it.json5.example | 4 +--- public/configuration/UITexts_nl.json5.example | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/public/configuration/UITexts_en.json5.example b/public/configuration/UITexts_en.json5.example index 9a91bca..46097ba 100644 --- a/public/configuration/UITexts_en.json5.example +++ b/public/configuration/UITexts_en.json5.example @@ -640,8 +640,6 @@ 'wheel.extra': 'Extra spins: %count%', 'wheel.spin': 'SPIN', 'wheel.buy': 'Buy spin', - 'wheel.settings': 'Settings', - 'wheel.settings.title': 'Wheel of Fortune Settings', 'wheel.winners': 'Latest winners', 'wheel.winners.empty': 'No winners yet', 'wheel.win.title': 'You won!', @@ -716,7 +714,7 @@ 'memenu.settings.other.place.multiple.objects': "Place multiple objects", 'memenu.settings.other.skip.purchase.confirmation': "Skip purchase confirmation", 'memenu.settings.other.enable.chat.window': "Enable chat window", - 'memenu.settings.other.catalog.classic.style': "Catalog: classic style", + 'memenu.settings.other.catalog.classic.style': "New style", 'usersettings.open.title': "User settings", 'usersettings.open.subtitle': "Password & account", 'usersettings.themes.custom': "Custom theme", diff --git a/public/configuration/UITexts_it.json5.example b/public/configuration/UITexts_it.json5.example index 81bd966..289b3b6 100644 --- a/public/configuration/UITexts_it.json5.example +++ b/public/configuration/UITexts_it.json5.example @@ -640,8 +640,6 @@ 'wheel.extra': 'Giri extra: %count%', 'wheel.spin': 'GIRA', 'wheel.buy': 'Acquista giro', - 'wheel.settings': 'Configurações', - 'wheel.settings.title': 'Configuração de Sistema da Roleta', 'wheel.winners': 'Ultimi vincitori', 'wheel.winners.empty': 'Ancora nessun vincitore', 'wheel.win.title': 'Hai vinto!', @@ -716,7 +714,7 @@ 'memenu.settings.other.place.multiple.objects': "Posiziona più oggetti", 'memenu.settings.other.skip.purchase.confirmation': "Salta la conferma d'acquisto", 'memenu.settings.other.enable.chat.window': "Abilita finestra chat", - 'memenu.settings.other.catalog.classic.style': "Catalogo: stile classico", + 'memenu.settings.other.catalog.classic.style': "Nuovo stile", 'usersettings.open.title': "Impostazioni utente", 'usersettings.open.subtitle': "Password e account", 'usersettings.themes.custom': "Tema personalizzato", diff --git a/public/configuration/UITexts_nl.json5.example b/public/configuration/UITexts_nl.json5.example index ebd3616..1c34f63 100644 --- a/public/configuration/UITexts_nl.json5.example +++ b/public/configuration/UITexts_nl.json5.example @@ -716,7 +716,7 @@ 'memenu.settings.other.place.multiple.objects': "Meerdere objecten plaatsen", 'memenu.settings.other.skip.purchase.confirmation': "Aankoopbevestiging overslaan", 'memenu.settings.other.enable.chat.window': "Chatvenster inschakelen", - 'memenu.settings.other.catalog.classic.style': "Catalogus: klassieke stijl", + 'memenu.settings.other.catalog.classic.style': "Nieuwe stijl", 'usersettings.open.title': "Gebruikersinstellingen", 'usersettings.open.subtitle': "Wachtwoord & account", 'usersettings.themes.custom': "Aangepast thema", From e91aa0c2023f5b4c8a1aa87b5cc544afdd987477 Mon Sep 17 00:00:00 2001 From: DuckieTM Date: Sat, 6 Jun 2026 07:23:52 +0200 Subject: [PATCH 03/38] =?UTF-8?q?=F0=9F=86=99=20Added=20config=20for=20the?= =?UTF-8?q?=20mentions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/configuration/ui-config.example | 3 +++ 1 file changed, 3 insertions(+) diff --git a/public/configuration/ui-config.example b/public/configuration/ui-config.example index b544b4a..9db01e2 100644 --- a/public/configuration/ui-config.example +++ b/public/configuration/ui-config.example @@ -27,6 +27,9 @@ "radio_ui.enabled": false, "mentions_ui.enabled": true, "mentions_ui.sound": true, + "mentions_ui.aliases.everyone": [ "all", "everyone" ], + "mentions_ui.aliases.friends": [ "friends" ], + "mentions_ui.aliases.room": [ "room" ], "guides.enabled": true, "housekeeping.enabled": true, "toolbar.hide.quests": true, From bf37aef9afd2656fc6cf22ccae0f1d5e5ebdfb17 Mon Sep 17 00:00:00 2001 From: DuckieTM Date: Sat, 6 Jun 2026 08:10:40 +0200 Subject: [PATCH 04/38] =?UTF-8?q?=F0=9F=86=99=20Catalog=20&=20Inventory=20?= =?UTF-8?q?preview=20now=20to=20original?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/layout/LayoutRoomPreviewerView.tsx | 10 + .../widgets/CatalogViewProductWidgetView.tsx | 24 +- .../inventory/views/bot/InventoryBotView.tsx | 4 +- .../furniture/InventoryFurnitureView.tsx | 23 +- .../inventory/views/pet/InventoryPetView.tsx | 4 +- src/css/catalog/CatalogClassicView.css | 98 +++++++ src/css/navigator/HabboNavigatorDesktop.css | 242 ------------------ src/pixiPatch.ts | 34 ++- 8 files changed, 186 insertions(+), 253 deletions(-) delete mode 100644 src/css/navigator/HabboNavigatorDesktop.css diff --git a/src/common/layout/LayoutRoomPreviewerView.tsx b/src/common/layout/LayoutRoomPreviewerView.tsx index bee10e8..fa8b7df 100644 --- a/src/common/layout/LayoutRoomPreviewerView.tsx +++ b/src/common/layout/LayoutRoomPreviewerView.tsx @@ -8,6 +8,14 @@ export const LayoutRoomPreviewerView: FC<{ { const { roomPreviewer = null, height = 0 } = props; const elementRef = useRef(null); + // Counter that disables further renders once Pixi throws in this + // previewer too many times in a row. The Pixi v8 null-texture bug + // (see src/pixiPatch.ts) is mostly absorbed at the prototype level, + // but any stray throw still cascades every animation frame. Allow + // a small number of consecutive failures so a transient bad frame + // self-recovers; permanently disable only if the previewer is truly + // wedged, which is what produces the "disabling further renders" + // log the user sees. const renderFailuresRef = useRef(0); const MAX_RENDER_FAILURES = 6; @@ -62,6 +70,8 @@ export const LayoutRoomPreviewerView: FC<{ canvas.height = 0; elementRef.current.style.backgroundImage = `url(${ base64 })`; + // A successful paint is the signal we've recovered from + // a transient bad frame; reset the failure counter. renderFailuresRef.current = 0; } catch(error) diff --git a/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx b/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx index df4cc24..e79c7d9 100644 --- a/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx +++ b/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx @@ -1,4 +1,4 @@ -import { GetAvatarRenderManager, GetSessionDataManager, Vector3d } from '@nitrots/nitro-renderer'; +import { GetAvatarRenderManager, GetRoomEngine, GetSessionDataManager, RoomObjectVariable, Vector3d } from '@nitrots/nitro-renderer'; import { FC, useEffect } from 'react'; import { BuildPurchasableClothingFigure, FurniCategory, Offer, ProductTypeEnum } from '../../../../../api'; import { AutoGrid, Column, LayoutGridItem, LayoutRoomPreviewerView } from '../../../../../common'; @@ -19,7 +19,25 @@ export const CatalogViewProductWidgetView: FC<{}> = props => if(!product) return; roomPreviewer.reset(false); - roomPreviewer.updateObjectRoom('default', 'default', 'default'); + + // Mirror the user's current room so the catalog preview shows + // the item against the wallpaper / floor / landscape they + // actually have decorated. Same approach as + // InventoryFurnitureView - read the active room's pattern ids + // off the room engine, fall back to '101' / '101' / '1.1' if + // the user isn't in a room yet (those are real Habbo pattern + // ids, the literal 'default' we used before is not and made + // the previewer fall back to blank white surfaces). + const roomEngine = GetRoomEngine(); + let floorType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_FLOOR_TYPE); + let wallType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_WALL_TYPE); + let landscapeType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_LANDSCAPE_TYPE); + + floorType = (floorType && floorType.length) ? floorType : '3002'; + wallType = (wallType && wallType.length) ? wallType : '3001'; + landscapeType = (landscapeType && landscapeType.length) ? landscapeType : '1.1'; + + roomPreviewer.updateObjectRoom(floorType, wallType, landscapeType); roomPreviewer.updateRoomWallsAndFloorVisibility(true, true); const populate = () => @@ -91,7 +109,7 @@ export const CatalogViewProductWidgetView: FC<{}> = props => return; } default: - roomPreviewer.updateObjectRoom('default', 'default', 'default'); + roomPreviewer.updateObjectRoom('101', '101', '1.1'); roomPreviewer.addWallItemIntoRoom(product.productClassId, new Vector3d(90), product.extraParam); return; } diff --git a/src/components/inventory/views/bot/InventoryBotView.tsx b/src/components/inventory/views/bot/InventoryBotView.tsx index 23bb76b..7f3dd04 100644 --- a/src/components/inventory/views/bot/InventoryBotView.tsx +++ b/src/components/inventory/views/bot/InventoryBotView.tsx @@ -29,8 +29,8 @@ export const InventoryBotView: FC<{ let floorType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_FLOOR_TYPE); let landscapeType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_LANDSCAPE_TYPE); - wallType = (wallType && wallType.length) ? wallType : '101'; - floorType = (floorType && floorType.length) ? floorType : '101'; + wallType = (wallType && wallType.length) ? wallType : '3001'; + floorType = (floorType && floorType.length) ? floorType : '3002'; landscapeType = (landscapeType && landscapeType.length) ? landscapeType : '1.1'; roomPreviewer.reset(false); diff --git a/src/components/inventory/views/furniture/InventoryFurnitureView.tsx b/src/components/inventory/views/furniture/InventoryFurnitureView.tsx index 58a049f..c9666c6 100644 --- a/src/components/inventory/views/furniture/InventoryFurnitureView.tsx +++ b/src/components/inventory/views/furniture/InventoryFurnitureView.tsx @@ -61,8 +61,8 @@ export const InventoryFurnitureView: FC<{ let floorType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_FLOOR_TYPE); let landscapeType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_LANDSCAPE_TYPE); - wallType = (wallType && wallType.length) ? wallType : '101'; - floorType = (floorType && floorType.length) ? floorType : '101'; + wallType = (wallType && wallType.length) ? wallType : '3001'; + floorType = (floorType && floorType.length) ? floorType : '3002'; landscapeType = (landscapeType && landscapeType.length) ? landscapeType : '1.1'; roomPreviewer.updateRoomWallsAndFloorVisibility(true, true); @@ -83,7 +83,24 @@ export const InventoryFurnitureView: FC<{ return; } - roomPreviewer.updateObjectRoom('default', 'default', 'default'); + // Mirror the active room's pattern ids so the furniture + // preview lands on the same wallpaper / floor / landscape the + // user is decorated with. Same fallback as the + // isRoomDecoration branch above; 'default' isn't a real + // pattern id, so passing it made the previewer fall back to + // blank white surfaces. + { + const roomEngine = GetRoomEngine(); + let wallType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_WALL_TYPE); + let floorType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_FLOOR_TYPE); + let landscapeType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_LANDSCAPE_TYPE); + + wallType = (wallType && wallType.length) ? wallType : '3001'; + floorType = (floorType && floorType.length) ? floorType : '3002'; + landscapeType = (landscapeType && landscapeType.length) ? landscapeType : '1.1'; + + roomPreviewer.updateObjectRoom(floorType, wallType, landscapeType); + } roomPreviewer.updateRoomWallsAndFloorVisibility(true, true); if(selectedItem.isWallItem) diff --git a/src/components/inventory/views/pet/InventoryPetView.tsx b/src/components/inventory/views/pet/InventoryPetView.tsx index e78c742..aee990a 100644 --- a/src/components/inventory/views/pet/InventoryPetView.tsx +++ b/src/components/inventory/views/pet/InventoryPetView.tsx @@ -44,8 +44,8 @@ export const InventoryPetView: FC<{ let floorType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_FLOOR_TYPE); let landscapeType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_LANDSCAPE_TYPE); - wallType = (wallType && wallType.length) ? wallType : '101'; - floorType = (floorType && floorType.length) ? floorType : '101'; + wallType = (wallType && wallType.length) ? wallType : '3001'; + floorType = (floorType && floorType.length) ? floorType : '3002'; landscapeType = (landscapeType && landscapeType.length) ? landscapeType : '1.1'; roomPreviewer.reset(false); diff --git a/src/css/catalog/CatalogClassicView.css b/src/css/catalog/CatalogClassicView.css index f71e155..af08391 100644 --- a/src/css/catalog/CatalogClassicView.css +++ b/src/css/catalog/CatalogClassicView.css @@ -12,10 +12,19 @@ --catalog-swf-select-outer: #82d1ed; --catalog-swf-bc: #ff8d00; --catalog-swf-bc-outer: #ffb53c; + /* Light gray secondary button - cropped from catalog_skin1.png + at (10, 190, 25x22). Drives the gift button "Cadeau", the + preview-room control button and the generic .nitro-catalog-swf- + button via border-image-slice 3 3 3 3 fill. */ --habbo-slice-button-default: url("../../assets/images/catalog/buttons/btn_secondary.png"); --habbo-slice-button-hover: url("../../assets/images/catalog/buttons/btn_secondary_hover.png"); --habbo-slice-button-pressed: url("../../assets/images/catalog/buttons/btn_secondary_pressed.png"); --habbo-slice-button-disabled: url("../../assets/images/catalog/buttons/btn_secondary_disabled.png"); + /* Classic Habbo "Osta!" Buy button - cropped from catalog_skin3.png + yellow band. The historical name says "green" but the user's + skin sheet ships yellow for the action colour, so that's what + we paint. The 27x34 sprite border-image-slices nicely at 6px + since the rounded corner is ~5px. */ --habbo-slice-button-buy: url("../../assets/images/catalog/buttons/buy.png"); --habbo-slice-button-large: url("../../assets/images/catalog/buttons/buy.png"); --habbo-slice-button-large-hover: url("../../assets/images/catalog/buttons/buy_hover.png"); @@ -39,6 +48,9 @@ --habbo-stepper-minus-hover: url("../../assets/images/catalog/buttons/minus_hover.png"); --habbo-stepper-minus-pressed: url("../../assets/images/catalog/buttons/minus_pressed.png"); --habbo-stepper-minus-disabled: url("../../assets/images/catalog/buttons/minus_disabled.png"); + /* Scrollbar sprites cropped from catalog_skin1.png. The single-piece + thumb has caps + grip baked into one 17x34 image - stretch it + full-height with background-size: 17px 100%. */ --habbo-scrollbar-up: url("../../assets/images/catalog/scrollbar/scroll_v_up.png"); --habbo-scrollbar-up-pressed: url("../../assets/images/catalog/scrollbar/scroll_v_up_pressed.png"); --habbo-scrollbar-down: url("../../assets/images/catalog/scrollbar/scroll_v_down.png"); @@ -877,6 +889,11 @@ } .nitro-catalog-classic-window .layout-grid-item { + /* Let the tile flex to whatever min/max width the AutoGrid sets + via repeat(auto-fill, minmax(N, 1fr)) - hard-pinning 53x74 was + overriding the layout's columnMinWidth prop, so the row count + never changed when we reduced it. Width is now 100% of the + column cell, height tracks --nitro-grid-column-min-height. */ width: 100% !important; height: var(--nitro-grid-column-min-height, 70px) !important; min-width: 0 !important; @@ -888,10 +905,19 @@ overflow: visible !important; } +/* Furni tiles drive their look from the icon image and need a clear + background. Color-grouping swatches use itemHighlight (.has-highlight) + to ask LayoutGridItem for a solid colour via inline backgroundColor - + keep the transparent override off those so the swatch is visible. */ .nitro-catalog-classic-window .layout-grid-item:not(.has-highlight) { background-color: transparent !important; } +/* Pets breed picker: the pet-head image renders at its native ~81x69px + regardless of the grid cell size. The old grid-cols-6 stretched + each .layout-grid-item to ~150-200px so the head looked huge in + acres of empty tile. Pin each tile just big enough to host the + head and let the container flex-wrap. */ .nitro-catalog-classic-window .nitro-catalog-classic-pet-breeds .layout-grid-item { width: 84px !important; min-width: 84px !important; @@ -912,6 +938,12 @@ inset -2px -2px 0 #ecece4 !important; } +/* Habbo-classic colour swatches: small chip with a 1px dark border + and a subtle inner highlight so light tones still read as buttons. + Hover lifts the border; the selected swatch is "pressed" with a + sunken inner shadow and a bright cyan ring matching the catalog + selection accent. The cream inset from the generic .is-active rule + above would wash out the swatch colour, so we replace it here. */ .nitro-catalog-classic-window .layout-grid-item.has-highlight { width: 26px !important; height: 26px !important; @@ -975,10 +1007,18 @@ text-shadow: 0 1px 0 rgba(255, 255, 255, 0.75); } +/* When the tile shows a full-tile bot/pet avatar (instead of a small + icon), pin the price strip to the bottom of the tile and give it a + translucent backdrop so it doesn't overlap with the avatar body. */ .nitro-catalog-classic-grid .layout-grid-item:has(.avatar-image) .nitro-catalog-classic-grid-price, .nitro-catalog-classic-grid .layout-grid-item:has(> .avatar-image) > .nitro-catalog-classic-grid-price, .nitro-catalog-classic-grid .avatar-image ~ .nitro-catalog-classic-grid-price { top: auto !important; + /* Re-anchor horizontally too: the parent rule's left: 2px / + right: 2px combined with content-sized inner flex was visually + parking the pill at the left side of the tile. Center it via + explicit left/right + transform so it lands smack in the + middle regardless of inner content width. */ left: 50% !important; right: auto !important; bottom: 4px !important; @@ -996,6 +1036,10 @@ z-index: 5 !important; } +/* Tighten the price entry inside the avatar-tile pill so the number + and currency icon center on the same baseline (the global + .grid-price-entry height: 13px clipped the 15px wallet icon and + pushed it visually below the number). */ .nitro-catalog-classic-grid .layout-grid-item:has(.avatar-image) .nitro-catalog-classic-grid-price-entry, .nitro-catalog-classic-grid .avatar-image ~ .nitro-catalog-classic-grid-price .nitro-catalog-classic-grid-price-entry { height: auto !important; @@ -1048,6 +1092,8 @@ .nitro-catalog-classic-price-row { position: absolute; left: 0; + /* Anchored from the bottom so the Aantal/Prezzo row sits just + above the Cadeau/Koop buttons regardless of layout height. */ bottom: 38px; width: 360px; height: 25px; @@ -1065,6 +1111,8 @@ .nitro-catalog-classic-total-price-slot { position: absolute; + /* Anchored to the right of the now-100% wide price row so the + Prezzo + amount stays flush with the right edge of the panel. */ right: 2px; top: 0; width: auto; @@ -1087,6 +1135,9 @@ .nitro-catalog-classic-purchase-row { position: absolute; left: 0; + /* Anchored to the bottom of the panel with a 4px breathing strip + so the Cadeau / Koop buttons stay flush at the bottom of the + window no matter how tall the catalog is. */ bottom: 4px; width: 360px; height: 30px; @@ -1100,6 +1151,9 @@ align-items: flex-start; justify-content: space-between; gap: 10px !important; + /* Fill the now-100% wide purchase row instead of staying pinned at + 330px (which used to match the old 360px column - 15px each + side). */ width: auto; height: 24px; margin-left: 15px; @@ -1159,6 +1213,13 @@ opacity: 1 !important; } +/* Buy / Gift buttons - pure CSS. border-image-slicing the bitmap + sprites produced thin highlight/shadow stripes at the top and + bottom because the source rounded corners are ~5-6px tall but the + buttons render at 22-24px, so the slice rows stretched into a + visible band. CSS gradients give a crisp pixel-art classic-habbo + look without those artefacts. */ + .nitro-catalog-classic-window .nitro-catalog-swf-buy-button { width: 160px !important; min-width: 160px !important; @@ -1168,6 +1229,8 @@ border-radius: 4px !important; border-image: none !important; border-image-source: none !important; + /* Yellow body with the same #f0a318 / #ffd54d tones as the + skin3-yellow Buy sprite. */ background: linear-gradient(180deg, #ffe66b 0%, #ffc828 45%, #f0a318 100%) !important; box-shadow: @@ -1194,6 +1257,9 @@ .nitro-catalog-classic-window .nitro-catalog-swf-buy-button.pointer-events-none, .nitro-catalog-classic-window .nitro-catalog-swf-buy-button:disabled { + /* Stay yellow when disabled - the user wants the action colour + to be recognisable regardless of state. Drop opacity + flip + the cursor so it still reads as non-interactive. */ background: linear-gradient(180deg, #ffe66b 0%, #ffc828 45%, #f0a318 100%) !important; color: #4a2b00 !important; @@ -1211,6 +1277,7 @@ border-radius: 4px !important; border-image: none !important; border-image-source: none !important; + /* Cream / light-gray body matching the catalog cardstock. */ background: linear-gradient(180deg, #ececec 0%, #cfcfc4 100%) !important; box-shadow: @@ -1243,12 +1310,19 @@ text-shadow: none !important; } +/* Pet purchase card lives in a tight flex row alongside the price, + so the main 160px Buy button doesn't fit. Shrink it down here. */ .nitro-catalog-classic-pet-card .nitro-catalog-swf-buy-button { width: auto !important; min-width: 0 !important; padding: 0 14px !important; } +/* All catalog grids must scroll vertically only - horizontal overflow + produces a stray horizontal scrollbar at the bottom of the items + strip on narrow columns (e.g. guild_furni). minmax(N, 1fr) usually + contains content but the safety net stops any odd item from + triggering a horizontal bar. */ .nitro-catalog-classic-window .layout-grid, .nitro-catalog-classic-window [class*="grid-cols-["] { overflow-x: hidden !important; @@ -1295,6 +1369,8 @@ image-rendering: pixelated !important; } +/* react-icons FaMinus/FaPlus glyphs ride inside these buttons; hide + them - the sprite already contains the +/- mark. */ .nitro-catalog-classic-window button.nitro-catalog-swf-spinner-button svg { display: none !important; } @@ -1390,6 +1466,9 @@ min-width: 25px; padding: 0 !important; overflow: hidden; + /* font-size: 0 was killing the SVG: react-icons emits + , so 0em -> 0x0. Use a real + font-size and pin the SVG to explicit pixels below. */ font-size: 14px !important; line-height: 1 !important; display: inline-flex !important; @@ -1416,6 +1495,13 @@ right: 6px; } +/* Bulletproof override for the rotate/state buttons. The shared SWF + button rule above lays a transparent body + border-image skin on + top, which works only when the catalog/buttons/btn_secondary*.png + sprites resolve - if they're missing the button renders 0x0 + invisible. Pin the box and paint a visible gradient + outline so + the controls are always discoverable, and force z-index above the + room-previewer DIV so they sit on top of the rendered scene. */ .nitro-catalog-classic-window button.nitro-catalog-classic-preview-btn { width: 28px !important; height: 26px !important; @@ -1456,6 +1542,12 @@ height: 17px; } +/* ===== Classic catalog scrollbar (pure CSS, no sprites) ===== + Drew this with CSS gradients instead of stretching the 17x34 + skin1 thumb sprite. The sprite version pixelated into visible + horizontal bands on tall scroll areas because every source row + stretched 5-10x. CSS gradients stay crisp at any height. */ + .nitro-catalog-classic-window * { scrollbar-color: auto !important; scrollbar-width: auto; @@ -1474,6 +1566,10 @@ border: 0 !important; } +/* Habbo thumb: symmetric light-edges -> darker-middle gradient (the + "pinched in the middle" look of the classic Ubuntu scrollbar), + 1px near-black outline, three central grip lines via SVG centered + no-repeat. */ .nitro-catalog-classic-window *::-webkit-scrollbar-thumb { min-height: 28px !important; border: 1px solid #2a2a26 !important; @@ -1503,6 +1599,8 @@ inset 0 -1px 0 rgba(255, 255, 255, 0.25) !important; } +/* Arrow buttons: cream cap with a 1px black outline + dark inset + chevron. SVG glyphs so they stay crisp at any zoom. */ .nitro-catalog-classic-window *::-webkit-scrollbar-button:single-button:vertical:decrement { display: block !important; width: 17px !important; diff --git a/src/css/navigator/HabboNavigatorDesktop.css b/src/css/navigator/HabboNavigatorDesktop.css deleted file mode 100644 index dd75c44..0000000 --- a/src/css/navigator/HabboNavigatorDesktop.css +++ /dev/null @@ -1,242 +0,0 @@ -.habbo-navigator-desktop { - border: 1px solid #000; - border-radius: 7px; - background: #e9e9e1; - box-shadow: 0 4px 4px rgba(0, 0, 0, 0.35); - color: #111; - font-family: Ubuntu, Arial, sans-serif; -} - -.habbo-navigator-desktop .nitro-card-header-shell { - min-height: 32px; - max-height: 32px; - background: #418db0; - border-bottom: 1px solid #000; - border-radius: 6px 6px 0 0; -} - -.habbo-navigator-desktop .nitro-card-title { - color: #fff; - font-family: UbuntuCondensed, Ubuntu, Arial, sans-serif; - font-size: 18px; - font-weight: 700; - line-height: 1; - text-shadow: 1px 1px 0 #000; -} - -.habbo-navigator-desktop .nitro-card-close-button { - width: 19px; - height: 20px; - right: 6px; - border: 2px solid #000; - border-radius: 4px; - background: #c73a32; - box-shadow: inset 1px 1px 0 rgba(255, 255, 255, 0.35); -} - -.habbo-navigator-desktop .nitro-card-close-button::before, -.habbo-navigator-desktop .nitro-card-close-button::after { - content: ""; - position: absolute; - width: 10px; - height: 2px; - background: #fff; - box-shadow: 1px 1px 0 #64120f; -} - -.habbo-navigator-desktop .nitro-card-close-button::before { - transform: rotate(45deg); -} - -.habbo-navigator-desktop .nitro-card-close-button::after { - transform: rotate(-45deg); -} - -.habbo-navigator-desktop .nitro-card-tabs-shell { - justify-content: flex-start; - gap: 0; - min-height: 27px; - max-height: 27px; - padding: 4px 8px 0; - background: #e9e9e1; - border-bottom: 1px solid #b8b8ad; -} - -.habbo-navigator-desktop .nitro-card-tab-item { - min-height: 23px; - margin-right: -1px; - padding: 4px 12px 3px; - border: 1px solid #555; - border-bottom: 0; - border-radius: 6px 6px 0 0; - background-color: #d5d8cf; - background-image: url("../../assets/images/navigator/swf/tab_bg_unsel.png"); - background-repeat: repeat-x; - background-size: auto 100%; - color: #111; - font-size: 12px; - font-weight: 400; - line-height: 1; - box-shadow: inset 1px 1px 0 #fff; -} - -.habbo-navigator-desktop .nitro-card-tab-item:hover { - background-color: #e7e8df; - background-image: url("../../assets/images/navigator/swf/tab_bg_hilite.png"); -} - -.habbo-navigator-desktop .nitro-card-tab-item-active { - z-index: 2; - margin-bottom: -1px; - background-color: #f4f4ed; - background-image: url("../../assets/images/navigator/swf/tab_bg_sel.png"); - border-bottom: 1px solid #f4f4ed; - font-weight: 700; -} - -.habbo-navigator-desktop .habbo-navigator-desktop-content { - padding: 8px 9px 9px; - overflow: hidden; - background: #f4f4ed; - border-radius: 0 0 6px 6px; - color: #111; -} - -.habbo-navigator-desktop .habbo-navigator-desktop-content input, -.habbo-navigator-desktop .habbo-navigator-desktop-content select, -.habbo-navigator-desktop .habbo-navigator-desktop-content textarea { - height: 22px; - border: 1px solid #a0a49c; - border-radius: 3px; - background-color: #fff; - background-image: url("../../assets/images/navigator/swf/hdr_search.png"); - background-repeat: repeat-x; - color: #333; - font-size: 12px; - box-shadow: inset 1px 1px 0 rgba(0, 0, 0, 0.08); -} - -.habbo-navigator-desktop .habbo-navigator-desktop-content button, -.habbo-navigator-desktop .habbo-navigator-desktop-content .btn { - min-height: 22px; - border: 1px solid #3a3a3a; - border-radius: 4px; - background-color: #d6d6d1; - background-image: url("../../assets/images/navigator/swf/button.png"); - background-repeat: repeat-x; - background-size: auto 100%; - color: #111; - font-size: 12px; - font-weight: 700; - box-shadow: inset 1px 1px 0 #fff; -} - -.habbo-navigator-desktop .nitro-card-panel { - border: 1px solid #babdb4; - border-radius: 6px; - background: #efefe8; - box-shadow: inset 1px 1px 0 #fff; - overflow: hidden; -} - -.habbo-navigator-desktop .nitro-card-panel > .flex:first-child { - min-height: 28px; - padding: 5px 8px; - border-bottom: 1px solid #d3d5cd; - background: #efefe8; -} - -.habbo-navigator-desktop .navigator-grid { - padding: 0 4px 5px; - background: #fff; -} - -.habbo-navigator-desktop .navigator-item { - min-height: 28px; - margin: 2px 0; - border: 1px solid transparent; - border-radius: 5px; - background: #f5f5ef; - color: #111; -} - -.habbo-navigator-desktop .navigator-item:nth-child(even) { - background: #e7e8e0; -} - -.habbo-navigator-desktop .navigator-item:hover { - border-color: #777; - background: #fff; -} - -.habbo-navigator-desktop .nitro-navigator-search-saves-result { - width: 155px; - min-width: 155px; - height: 100%; - border: 1px solid #babdb4; - border-radius: 6px; - background: #efefe8; - padding: 4px; -} - -.habbo-navigator-desktop .nitro-navigator-search-saves-result > .flex:first-child { - min-height: 24px; - border: 1px solid #d58e00; - border-radius: 4px; - background: #f8a900; - box-shadow: inset 1px 1px 0 rgba(255, 255, 255, 0.45); -} - -.habbo-navigator-desktop .nitro-navigator-search-saves-result span { - color: #111; - font-weight: 700; -} - -.habbo-navigator-desktop .nitro-icon.icon-navigator-info { - width: 16px; - height: 16px; -} - -.habbo-navigator-desktop ::-webkit-scrollbar { - width: 17px !important; - height: 17px !important; -} - -.habbo-navigator-desktop ::-webkit-scrollbar-track { - border: 0 !important; - background-color: transparent !important; - background-image: url("../../assets/images/catalog/swf/ubuntu_scrollbar_track_v_17x2.png") !important; - background-repeat: repeat-y !important; - background-position: center top !important; -} - -.habbo-navigator-desktop ::-webkit-scrollbar-thumb { - min-height: 24px !important; - border: 0 !important; - border-radius: 0 !important; - background-color: transparent !important; - background-image: - url("../../assets/images/catalog/swf/ubuntu_scrollbar_thumb_v_grip_7x10.png"), - url("../../assets/images/catalog/swf/ubuntu_scrollbar_thumb_v_top_17x2.png"), - url("../../assets/images/catalog/swf/ubuntu_scrollbar_thumb_v_bottom_17x2.png"), - url("../../assets/images/catalog/swf/ubuntu_scrollbar_thumb_v_mid_17x1.png") !important; - background-repeat: repeat-y, no-repeat, no-repeat, repeat-y !important; - background-position: center center, center top, center bottom, center top !important; - background-size: 7px 10px, 17px 2px, 17px 2px, 17px 1px !important; - box-shadow: none !important; -} - -.habbo-navigator-desktop ::-webkit-scrollbar-button { - width: 17px !important; - height: 16px !important; - background-color: transparent !important; - border: 0 !important; -} - -.habbo-navigator-desktop ::-webkit-scrollbar-button:single-button:vertical:decrement { - background-image: url("../../assets/images/catalog/swf/ubuntu_scrollbar_up_17x16.png") !important; -} - -.habbo-navigator-desktop ::-webkit-scrollbar-button:single-button:vertical:increment { - background-image: url("../../assets/images/catalog/swf/ubuntu_scrollbar_down_17x16.png") !important; -} diff --git a/src/pixiPatch.ts b/src/pixiPatch.ts index bea0f24..401ae8d 100644 --- a/src/pixiPatch.ts +++ b/src/pixiPatch.ts @@ -1,3 +1,34 @@ +/** + * Runtime patches for pixi.js v8 batcher edge cases. + * + * Pixi v8 (through 8.19 at least) has a long-running family of crashes + * where `getAdjustedBlendModeBlend(blendMode, textureSource)` is invoked + * with a `null` textureSource and throws on `null.alphaMode` or + * `null.uid`. We've seen it from at least four call sites: + * - Batcher.break() (FilterPipe, StencilMaskPipe, AlphaMaskPipe) + * - Batcher.checkAndUpdateTexture() (SpritePipe.validateRenderable) + * + * The trigger varies, but the symptom is always the same: a single bad + * frame inside the catalog room previewer (or anywhere RoomSpriteCanvas + * drives Pixi) tanks the whole render loop with an endless cascade of + * requestAnimationFrame errors. + * + * We removed the two custom filters we owned (BlackToAlphaFilter, + * PlaneMaskFilter) earlier, but several call sites are inside Pixi + * itself or inside renderer-side mask setup that we can't sensibly + * delete (RoomSpriteCanvas pins a Sprite mask on the master display to + * clip the room to the canvas). + * + * Patch every known throwing entry point so that when it throws because + * of a null textureSource we treat the frame as a no-op instead of + * propagating the exception. The visible cost is a missed batch this + * tick - the next tick re-renders cleanly. Without this patch the + * LayoutRoomPreviewerView safety latch fires permanently on the first + * affected offer. + * + * Importing this module has the side effect of installing the patch + * exactly once, idempotent across HMR reloads. + */ import * as PIXI from 'pixi.js'; type AnyFn = (...args: unknown[]) => unknown; @@ -64,8 +95,9 @@ const installPatch = (): void => const proto = (ctor as { prototype?: MethodHost } | undefined)?.prototype; if(!proto) continue; + // break() is called during FilterPipe / StencilMaskPipe / AlphaMaskPipe.pop if(guardMethod(proto, 'break', name)) patched = true; - + // checkAndUpdateTexture() is called during SpritePipe.validateRenderable if(guardMethod(proto, 'checkAndUpdateTexture', name)) patched = true; } From 816779a6140d4f8bd1d6588ad38bcf92547a3ee9 Mon Sep 17 00:00:00 2001 From: DuckieTM Date: Sat, 6 Jun 2026 08:42:18 +0200 Subject: [PATCH 05/38] =?UTF-8?q?=F0=9F=86=99=20Fixed=20the=20colorid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../widgets/CatalogViewProductWidgetView.tsx | 35 +---------------- .../inventory/views/bot/InventoryBotView.tsx | 14 +------ .../furniture/InventoryFurnitureView.tsx | 38 ++++--------------- .../inventory/views/pet/InventoryPetView.tsx | 13 +------ src/pixiPatch.ts | 33 ---------------- 5 files changed, 13 insertions(+), 120 deletions(-) diff --git a/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx b/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx index e79c7d9..46c0184 100644 --- a/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx +++ b/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx @@ -1,4 +1,4 @@ -import { GetAvatarRenderManager, GetRoomEngine, GetSessionDataManager, RoomObjectVariable, Vector3d } from '@nitrots/nitro-renderer'; +import { GetAvatarRenderManager, GetSessionDataManager, Vector3d } from '@nitrots/nitro-renderer'; import { FC, useEffect } from 'react'; import { BuildPurchasableClothingFigure, FurniCategory, Offer, ProductTypeEnum } from '../../../../../api'; import { AutoGrid, Column, LayoutGridItem, LayoutRoomPreviewerView } from '../../../../../common'; @@ -19,25 +19,7 @@ export const CatalogViewProductWidgetView: FC<{}> = props => if(!product) return; roomPreviewer.reset(false); - - // Mirror the user's current room so the catalog preview shows - // the item against the wallpaper / floor / landscape they - // actually have decorated. Same approach as - // InventoryFurnitureView - read the active room's pattern ids - // off the room engine, fall back to '101' / '101' / '1.1' if - // the user isn't in a room yet (those are real Habbo pattern - // ids, the literal 'default' we used before is not and made - // the previewer fall back to blank white surfaces). - const roomEngine = GetRoomEngine(); - let floorType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_FLOOR_TYPE); - let wallType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_WALL_TYPE); - let landscapeType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_LANDSCAPE_TYPE); - - floorType = (floorType && floorType.length) ? floorType : '3002'; - wallType = (wallType && wallType.length) ? wallType : '3001'; - landscapeType = (landscapeType && landscapeType.length) ? landscapeType : '1.1'; - - roomPreviewer.updateObjectRoom(floorType, wallType, landscapeType); + roomPreviewer.updateObjectRoom('111', '217', '1.1'); roomPreviewer.updateRoomWallsAndFloorVisibility(true, true); const populate = () => @@ -124,13 +106,6 @@ export const CatalogViewProductWidgetView: FC<{}> = props => }; populate(); - - // RoomPreviewer.addFurnitureIntoRoom / addAvatarIntoRoom flip - // _automaticStateChange to true, which makes the ticker advance - // the room object's state every AUTOMATIC_STATE_CHANGE_INTERVAL. - // In the catalog we want the preview to sit still until the - // user clicks the state button explicitly - turn it back off - // after populate() runs. roomPreviewer.setAutomaticStateChange(false); }, [ currentOffer, previewStuffData, roomPreviewer ]); @@ -150,11 +125,5 @@ export const CatalogViewProductWidgetView: FC<{}> = props => ); } - // Re-mount the previewer whenever the offer changes so the render - // latch / texture handle in LayoutRoomPreviewerView resets cleanly. - // Without this a single broken offer (e.g. blackhole's Pixi filter - // crash) latches the previewer permanently and every following - // offer paints nothing - the singleton roomPreviewer + 240px height - // keep the same component mounted otherwise. return ; }; diff --git a/src/components/inventory/views/bot/InventoryBotView.tsx b/src/components/inventory/views/bot/InventoryBotView.tsx index 7f3dd04..6963f2e 100644 --- a/src/components/inventory/views/bot/InventoryBotView.tsx +++ b/src/components/inventory/views/bot/InventoryBotView.tsx @@ -1,4 +1,4 @@ -import { GetRoomEngine, IRoomSession, RoomObjectVariable, RoomPreviewer } from '@nitrots/nitro-renderer'; +import { IRoomSession, RoomPreviewer } from '@nitrots/nitro-renderer'; import { FC, useEffect, useState } from 'react'; import { IBotItem, LocalizeText, UnseenItemCategory, attemptBotPlacement } from '../../../../api'; import { LayoutRoomPreviewerView } from '../../../../common'; @@ -23,19 +23,9 @@ export const InventoryBotView: FC<{ const botData = selectedBot.botData; - const roomEngine = GetRoomEngine(); - - let wallType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_WALL_TYPE); - let floorType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_FLOOR_TYPE); - let landscapeType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_LANDSCAPE_TYPE); - - wallType = (wallType && wallType.length) ? wallType : '3001'; - floorType = (floorType && floorType.length) ? floorType : '3002'; - landscapeType = (landscapeType && landscapeType.length) ? landscapeType : '1.1'; - roomPreviewer.reset(false); roomPreviewer.updateRoomWallsAndFloorVisibility(true, true); - roomPreviewer.updateObjectRoom(floorType, wallType, landscapeType); + roomPreviewer.updateObjectRoom('111', '217', '1.1'); roomPreviewer.addAvatarIntoRoom(botData.figure, 0); }, [ roomPreviewer, selectedBot ]); diff --git a/src/components/inventory/views/furniture/InventoryFurnitureView.tsx b/src/components/inventory/views/furniture/InventoryFurnitureView.tsx index c9666c6..fd0f38e 100644 --- a/src/components/inventory/views/furniture/InventoryFurnitureView.tsx +++ b/src/components/inventory/views/furniture/InventoryFurnitureView.tsx @@ -1,5 +1,5 @@ import { InfiniteGrid } from '@layout/InfiniteGrid'; -import { GetRoomEngine, GetSessionDataManager, IRoomSession, RoomObjectVariable, RoomPreviewer, Vector3d } from '@nitrots/nitro-renderer'; +import { GetSessionDataManager, IRoomSession, RoomPreviewer, Vector3d } from '@nitrots/nitro-renderer'; import { FC, useEffect, useState } from 'react'; import { FaPowerOff, FaSyncAlt, FaTrashAlt } from 'react-icons/fa'; import { DispatchUiEvent, FurniCategory, GroupItem, LocalizeText, UnseenItemCategory, attemptItemPlacement } from '../../../../api'; @@ -53,24 +53,17 @@ export const InventoryFurnitureView: FC<{ const isRoomDecoration = (furnitureItem.category === FurniCategory.WALL_PAPER) || (furnitureItem.category === FurniCategory.FLOOR) || (furnitureItem.category === FurniCategory.LANDSCAPE); + let floorType = '111'; + let wallType = '217'; + let landscapeType = '1.1'; + if(isRoomDecoration) { - const roomEngine = GetRoomEngine(); - - let wallType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_WALL_TYPE); - let floorType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_FLOOR_TYPE); - let landscapeType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_LANDSCAPE_TYPE); - - wallType = (wallType && wallType.length) ? wallType : '3001'; - floorType = (floorType && floorType.length) ? floorType : '3002'; - landscapeType = (landscapeType && landscapeType.length) ? landscapeType : '1.1'; - - roomPreviewer.updateRoomWallsAndFloorVisibility(true, true); - floorType = ((furnitureItem.category === FurniCategory.FLOOR) ? selectedItem.stuffData.getLegacyString() : floorType); wallType = ((furnitureItem.category === FurniCategory.WALL_PAPER) ? selectedItem.stuffData.getLegacyString() : wallType); landscapeType = ((furnitureItem.category === FurniCategory.LANDSCAPE) ? selectedItem.stuffData.getLegacyString() : landscapeType); + roomPreviewer.updateRoomWallsAndFloorVisibility(true, true); roomPreviewer.updateObjectRoom(floorType, wallType, landscapeType); if(furnitureItem.category === FurniCategory.LANDSCAPE) @@ -83,24 +76,7 @@ export const InventoryFurnitureView: FC<{ return; } - // Mirror the active room's pattern ids so the furniture - // preview lands on the same wallpaper / floor / landscape the - // user is decorated with. Same fallback as the - // isRoomDecoration branch above; 'default' isn't a real - // pattern id, so passing it made the previewer fall back to - // blank white surfaces. - { - const roomEngine = GetRoomEngine(); - let wallType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_WALL_TYPE); - let floorType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_FLOOR_TYPE); - let landscapeType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_LANDSCAPE_TYPE); - - wallType = (wallType && wallType.length) ? wallType : '3001'; - floorType = (floorType && floorType.length) ? floorType : '3002'; - landscapeType = (landscapeType && landscapeType.length) ? landscapeType : '1.1'; - - roomPreviewer.updateObjectRoom(floorType, wallType, landscapeType); - } + roomPreviewer.updateObjectRoom(floorType, wallType, landscapeType); roomPreviewer.updateRoomWallsAndFloorVisibility(true, true); if(selectedItem.isWallItem) diff --git a/src/components/inventory/views/pet/InventoryPetView.tsx b/src/components/inventory/views/pet/InventoryPetView.tsx index aee990a..a684cc2 100644 --- a/src/components/inventory/views/pet/InventoryPetView.tsx +++ b/src/components/inventory/views/pet/InventoryPetView.tsx @@ -1,4 +1,4 @@ -import { DeletePetMessageComposer, GetRoomEngine, IRoomSession, RoomObjectVariable, RoomPreviewer } from '@nitrots/nitro-renderer'; +import { DeletePetMessageComposer, IRoomSession, RoomPreviewer } from '@nitrots/nitro-renderer'; import { FC, useEffect, useState } from 'react'; import { FaTrashAlt } from 'react-icons/fa'; import { IPetItem, LocalizeText, SendMessageComposer, UnseenItemCategory, attemptPetPlacement } from '../../../../api'; @@ -38,19 +38,10 @@ export const InventoryPetView: FC<{ if(!selectedPet || !roomPreviewer) return; const petData = selectedPet.petData; - const roomEngine = GetRoomEngine(); - - let wallType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_WALL_TYPE); - let floorType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_FLOOR_TYPE); - let landscapeType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_LANDSCAPE_TYPE); - - wallType = (wallType && wallType.length) ? wallType : '3001'; - floorType = (floorType && floorType.length) ? floorType : '3002'; - landscapeType = (landscapeType && landscapeType.length) ? landscapeType : '1.1'; roomPreviewer.reset(false); roomPreviewer.updateRoomWallsAndFloorVisibility(true, true); - roomPreviewer.updateObjectRoom(floorType, wallType, landscapeType); + roomPreviewer.updateObjectRoom('111', '217', '1.1'); roomPreviewer.addPetIntoRoom(petData.figureString); }, [ roomPreviewer, selectedPet ]); diff --git a/src/pixiPatch.ts b/src/pixiPatch.ts index 401ae8d..8074720 100644 --- a/src/pixiPatch.ts +++ b/src/pixiPatch.ts @@ -1,34 +1,3 @@ -/** - * Runtime patches for pixi.js v8 batcher edge cases. - * - * Pixi v8 (through 8.19 at least) has a long-running family of crashes - * where `getAdjustedBlendModeBlend(blendMode, textureSource)` is invoked - * with a `null` textureSource and throws on `null.alphaMode` or - * `null.uid`. We've seen it from at least four call sites: - * - Batcher.break() (FilterPipe, StencilMaskPipe, AlphaMaskPipe) - * - Batcher.checkAndUpdateTexture() (SpritePipe.validateRenderable) - * - * The trigger varies, but the symptom is always the same: a single bad - * frame inside the catalog room previewer (or anywhere RoomSpriteCanvas - * drives Pixi) tanks the whole render loop with an endless cascade of - * requestAnimationFrame errors. - * - * We removed the two custom filters we owned (BlackToAlphaFilter, - * PlaneMaskFilter) earlier, but several call sites are inside Pixi - * itself or inside renderer-side mask setup that we can't sensibly - * delete (RoomSpriteCanvas pins a Sprite mask on the master display to - * clip the room to the canvas). - * - * Patch every known throwing entry point so that when it throws because - * of a null textureSource we treat the frame as a no-op instead of - * propagating the exception. The visible cost is a missed batch this - * tick - the next tick re-renders cleanly. Without this patch the - * LayoutRoomPreviewerView safety latch fires permanently on the first - * affected offer. - * - * Importing this module has the side effect of installing the patch - * exactly once, idempotent across HMR reloads. - */ import * as PIXI from 'pixi.js'; type AnyFn = (...args: unknown[]) => unknown; @@ -95,9 +64,7 @@ const installPatch = (): void => const proto = (ctor as { prototype?: MethodHost } | undefined)?.prototype; if(!proto) continue; - // break() is called during FilterPipe / StencilMaskPipe / AlphaMaskPipe.pop if(guardMethod(proto, 'break', name)) patched = true; - // checkAndUpdateTexture() is called during SpritePipe.validateRenderable if(guardMethod(proto, 'checkAndUpdateTexture', name)) patched = true; } From 0a6aeafe1163669b8c7f30a44cb6eb1a05d0dbbc Mon Sep 17 00:00:00 2001 From: DuckieTM Date: Sat, 6 Jun 2026 11:35:02 +0200 Subject: [PATCH 06/38] =?UTF-8?q?=F0=9F=86=99=20Small=20visual=20updates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../page/common/CatalogGridOfferView.tsx | 13 +- .../page/layout/CatalogLayoutDefaultView.tsx | 2 +- .../inventory/views/bot/InventoryBotView.tsx | 1 - src/css/catalog/CatalogClassicView.css | 227 +----------------- 4 files changed, 25 insertions(+), 218 deletions(-) diff --git a/src/components/catalog/views/page/common/CatalogGridOfferView.tsx b/src/components/catalog/views/page/common/CatalogGridOfferView.tsx index 60ab937..3926e42 100644 --- a/src/components/catalog/views/page/common/CatalogGridOfferView.tsx +++ b/src/components/catalog/views/page/common/CatalogGridOfferView.tsx @@ -40,7 +40,17 @@ export const CatalogGridOfferView: FC = props => if(className?.length) { - const param = (product.productType === ProductTypeEnum.WALL && product.extraParam?.length) ? `_${ product.extraParam }` : ''; + let param = ''; + + if(product.productType === ProductTypeEnum.WALL && product.extraParam?.length) + { + param = `_${ product.extraParam }`; + } + else if(product.productType === ProductTypeEnum.FLOOR && product.furnitureData?.hasIndexedColor && (product.furnitureData.colorIndex > 0)) + { + param = `_${ product.furnitureData.colorIndex }`; + } + const configuredIconUrl = GetConfigurationValue('furni.asset.icon.url', ''); if(configuredIconUrl?.length) @@ -104,6 +114,7 @@ export const CatalogGridOfferView: FC = props => return ( = props =>
{ GetConfigurationValue('catalog.headers') && } - +
{ currentOffer && diff --git a/src/components/inventory/views/bot/InventoryBotView.tsx b/src/components/inventory/views/bot/InventoryBotView.tsx index 6963f2e..e4bd09d 100644 --- a/src/components/inventory/views/bot/InventoryBotView.tsx +++ b/src/components/inventory/views/bot/InventoryBotView.tsx @@ -22,7 +22,6 @@ export const InventoryBotView: FC<{ if(!selectedBot || !roomPreviewer) return; const botData = selectedBot.botData; - roomPreviewer.reset(false); roomPreviewer.updateRoomWallsAndFloorVisibility(true, true); roomPreviewer.updateObjectRoom('111', '217', '1.1'); diff --git a/src/css/catalog/CatalogClassicView.css b/src/css/catalog/CatalogClassicView.css index af08391..d7351c2 100644 --- a/src/css/catalog/CatalogClassicView.css +++ b/src/css/catalog/CatalogClassicView.css @@ -12,19 +12,10 @@ --catalog-swf-select-outer: #82d1ed; --catalog-swf-bc: #ff8d00; --catalog-swf-bc-outer: #ffb53c; - /* Light gray secondary button - cropped from catalog_skin1.png - at (10, 190, 25x22). Drives the gift button "Cadeau", the - preview-room control button and the generic .nitro-catalog-swf- - button via border-image-slice 3 3 3 3 fill. */ --habbo-slice-button-default: url("../../assets/images/catalog/buttons/btn_secondary.png"); --habbo-slice-button-hover: url("../../assets/images/catalog/buttons/btn_secondary_hover.png"); --habbo-slice-button-pressed: url("../../assets/images/catalog/buttons/btn_secondary_pressed.png"); --habbo-slice-button-disabled: url("../../assets/images/catalog/buttons/btn_secondary_disabled.png"); - /* Classic Habbo "Osta!" Buy button - cropped from catalog_skin3.png - yellow band. The historical name says "green" but the user's - skin sheet ships yellow for the action colour, so that's what - we paint. The 27x34 sprite border-image-slices nicely at 6px - since the rounded corner is ~5px. */ --habbo-slice-button-buy: url("../../assets/images/catalog/buttons/buy.png"); --habbo-slice-button-large: url("../../assets/images/catalog/buttons/buy.png"); --habbo-slice-button-large-hover: url("../../assets/images/catalog/buttons/buy_hover.png"); @@ -48,9 +39,6 @@ --habbo-stepper-minus-hover: url("../../assets/images/catalog/buttons/minus_hover.png"); --habbo-stepper-minus-pressed: url("../../assets/images/catalog/buttons/minus_pressed.png"); --habbo-stepper-minus-disabled: url("../../assets/images/catalog/buttons/minus_disabled.png"); - /* Scrollbar sprites cropped from catalog_skin1.png. The single-piece - thumb has caps + grip baked into one 17x34 image - stretch it - full-height with background-size: 17px 100%. */ --habbo-scrollbar-up: url("../../assets/images/catalog/scrollbar/scroll_v_up.png"); --habbo-scrollbar-up-pressed: url("../../assets/images/catalog/scrollbar/scroll_v_up_pressed.png"); --habbo-scrollbar-down: url("../../assets/images/catalog/scrollbar/scroll_v_down.png"); @@ -163,10 +151,6 @@ display: none !important; } -/* Publish button: lives inside the catalog window, absolutely - positioned in the header just to the left of the close X. Renders - only when adminMode is true (see CatalogClassicView.tsx). Uses the - Habbo yellow buy-button skin so it matches the Koop button. */ .nitro-catalog-classic-window .nitro-catalog-classic-header-publish { position: absolute !important; top: 5px !important; @@ -191,17 +175,7 @@ 50% { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.65), inset 0 -2px 0 rgba(140, 75, 0, 0.35), 0 0 8px rgba(255, 200, 0, 0.75); } } -/* Catalog default-layout admin row (Pagina bewerken / Nieuwe - aanbieding / Aanbieding bewerken). These are inline text buttons - but the .habbo-swf-window button + .habbo-swf-window - button[class*="success"] global rules were dressing them up as - SWF skin buttons (one yellow!) and forcing min-height: 22px which - broke the layout. Reset and re-skin as compact pill chips. */ .nitro-catalog-classic-window .nitro-catalog-classic-default-admin { - /* Keep all admin buttons on one row so the product-view doesn't - get pushed down into the absolutely-positioned grid-shell. If - a future label makes them overflow, the row scrolls - horizontally instead of wrapping. */ flex-wrap: nowrap !important; align-items: center !important; gap: 6px !important; @@ -251,8 +225,6 @@ box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.08), 0 0 0 rgba(0, 0, 0, 0) !important; } -/* The "Nieuwe aanbieding" button uses text-success - give it the - habbo-yellow buy-button palette to mark it as the create action. */ .nitro-catalog-classic-window .nitro-catalog-classic-default-admin button.text-success, .nitro-catalog-classic-window .nitro-catalog-classic-default-admin button[class*="success"] { border-color: #8a5b00 !important; @@ -282,10 +254,6 @@ margin-left: 4px !important; } -/* Admin cog tab at the end of the tab strip - only renders when the - user is a mod, so leaving it visible at all times is safe. Style - it as a compact square that sits flush with the other tabs - instead of stretching to flex: 1 like a category tab. */ .nitro-catalog-classic-tabs-shell .nitro-card-tab-item.nitro-catalog-classic-admin-tab { flex: 0 0 auto !important; width: 32px !important; @@ -355,9 +323,6 @@ } .nitro-catalog-classic-tabs-shell { - /* Strip just tall enough to hold the 36px tab + 4px of breathing - room above. Trims the dead blue band between the header and - the tabs so the catalog body doesn't lose vertical space. */ flex: 0 0 40px; height: 40px; min-height: 40px; @@ -365,8 +330,6 @@ gap: 0; padding: 4px 6px 0 !important; align-items: flex-end; - /* Horizontal scroll so every category tab stays reachable when the - card is narrower than the total tab width. */ overflow-x: auto; overflow-y: hidden; flex-wrap: nowrap; @@ -377,8 +340,6 @@ border-bottom: 1px solid #c8c8bf; } -/* The tabs strip uses a slim 6px scrollbar - opt it out of the - 17px Habbo-sprite scrollbar applied to the rest of the catalog. */ .nitro-catalog-classic-tabs-shell::-webkit-scrollbar { width: 6px !important; height: 6px !important; @@ -409,18 +370,11 @@ gap: 4px; height: 36px; min-width: 0; - /* Equal-width tabs that share the strip exactly - the right - edge of the last tab now lines up with the right edge of the - catalog window, no trailing gap. */ flex: 1 1 0; max-width: none; padding: 6px 6px 7px; margin: 0 2px 0 0; flex-shrink: 1; - /* Classic Habbo tab: gray rounded-top rectangle with a 1px black - outline. ubuntu_tab3_*.png isn't shipped, so we draw the - habbo-look ourselves instead of border-image-slicing a missing - sprite. */ border: 1px solid #000 !important; border-bottom: 0 !important; border-radius: 6px 6px 0 0 !important; @@ -437,9 +391,6 @@ border-image-source: none !important; } -/* Bring the last tab flush with whatever follows (admin cog, edge), - instead of leaving the negative right margin tugging on empty - space. */ .nitro-catalog-classic-tabs-shell .nitro-card-tab-item:last-of-type { margin-right: 0; } @@ -454,9 +405,6 @@ line-height: 17px; } -/* Category icon that sits before the label. The blanket "hide every - img/svg inside a tab" rule is gone - we explicitly size the - classic tab icon and let everything else fall through. */ .nitro-catalog-classic-tab-icon { width: 18px; height: 18px; @@ -472,9 +420,6 @@ .nitro-catalog-classic-tabs-shell .nitro-card-tab-item-active { z-index: 2; - /* Active tab: the catalog-header habbo-blue with the cream catalog - body bleeding up into it. Drop the bottom border so the tab - "merges" with the panel below. */ background: linear-gradient(180deg, #4fb3ff 0%, var(--catalog-swf-blue) 100%) !important; color: #ffffff !important; @@ -502,10 +447,6 @@ .nitro-catalog-classic-stage { display: grid; - /* Sidebar pinned at 184px; the layout column takes the rest of - the stage row so the right edge of the right column lines up - with the right edge of the catalog window instead of leaving a - wide cream strip. */ grid-template-columns: 184px 1fr; gap: 8px; width: 100%; @@ -527,14 +468,9 @@ .nitro-catalog-classic-search-shell { position: relative; - /* Use flex so the input vertically centers inside the 24px shell - regardless of the input's own intrinsic baseline. */ display: flex; align-items: center; height: 24px; - /* Outer padding 0, negative horizontal margin bleeds the shell a - couple of pixels past its sidebar column on each side without - touching the grid template - cheap way to look ~4px wider. */ padding: 0; margin: -2px -1px 0 -1px; border: 1px solid #b7b7ae; @@ -542,10 +478,6 @@ background: #f7f7f2; } -/* Clear the magnifying-glass on the left and the X-clear button on the - right. The shell's own outer padding is essentially zero, so the - input claims the full sidebar column width minus just enough to - keep the icons from overlapping the text. */ .nitro-catalog-classic-search-shell input { flex: 1 1 auto; width: 100% !important; @@ -562,8 +494,6 @@ vertical-align: middle !important; } -/* The wrapping div the React component renders is 100% wide so the - input fills the shell instead of shrinking to content. */ .nitro-catalog-classic-search-shell > div { width: 100% !important; height: 100% !important; @@ -574,13 +504,10 @@ font-size: 11px !important; } -/* The search icon ships with absolute + left-2; nudge it tight to the - shell edge so the input can keep its left padding small. */ .nitro-catalog-classic-search-shell svg:first-child { left: 4px !important; } -/* X-clear button on the right edge - keep it tight too. */ .nitro-catalog-classic-search-shell button { right: 4px !important; } @@ -715,20 +642,11 @@ .nitro-catalog-classic-default-layout { position: relative; display: block !important; - /* Fill the layout container in both axes - the stage was - previously 552px wide and this column was pinned at 360px, but - now that the stage uses 1fr, hardcoding 360px would leave a - wide blank strip on the right of every default-layout catalog - page. */ width: 100%; height: 100%; min-height: 460px; } -/* The product-view, grid-shell, price-row and purchase-row inside - the default-layout were each pinned at 360px to match the old - stage width. Widen them in lockstep so they fill the new - 1fr-sized container. */ .nitro-catalog-classic-product-view, .nitro-catalog-classic-grid-shell, .nitro-catalog-classic-price-row, @@ -768,12 +686,6 @@ height: 100%; } -/* Default-3x3 layout: .offer-info is rendered but hidden via the - display: none rule below, so the panel reserved the full width - while only the 360px preview was visible (empty strip on the - right). Center the preview inside the panel instead so the gap - becomes symmetric padding on both sides. Color-grouping doesn't - render .offer-info so its panel keeps the existing layout. */ .nitro-catalog-classic-offer-panel:has(> .nitro-catalog-classic-offer-info) { justify-content: center; } @@ -786,13 +698,6 @@ background: #000; } -/* The default-3x3 layout puts the preview next to .offer-info inside - .offer-panel and needs the 360px column. Scope the pin to that - context so other layouts (color-grouping, etc.) can put the same - preview class inside a flex/grid column and let it track the - container width. Without this scoping the absolute-positioned - rotate/state buttons sit past the column's right edge and get - clipped by overflow: hidden. */ .nitro-catalog-classic-offer-panel > .nitro-catalog-classic-offer-preview { width: 360px; min-width: 360px; @@ -858,8 +763,6 @@ position: absolute; left: 0; top: 245px; - /* Stretch down to just above the price + purchase rows so the - grid soaks up any extra height the bigger window gives us. */ bottom: 68px; width: 360px; min-height: 0; @@ -867,20 +770,12 @@ overflow: auto; } -/* When the admin row is rendered above the product-view it adds - ~30px (22px button + flex gap) to the flex column, but the - grid-shell is absolutely positioned and doesn't shift on its own. - Push it (and the bottom-anchored price/purchase rows stay put) - down so the preview panel no longer bleeds into the grid. */ .nitro-catalog-classic-default-layout:has(.nitro-catalog-classic-default-admin) .nitro-catalog-classic-grid-shell { top: 280px !important; } .nitro-catalog-classic-grid { - /* Don't pin a fixed column track here - AutoGrid sets the inline - grid-template-columns from its columnMinWidth prop. The earlier - `repeat(6, 53px) !important` was clobbering that and freezing - the row at 6 tiles regardless of what the React layout passed. */ + grid-template-columns: repeat(6, 1fr) !important; grid-auto-rows: var(--nitro-grid-column-min-height, 70px); align-content: start; justify-content: start; @@ -889,11 +784,6 @@ } .nitro-catalog-classic-window .layout-grid-item { - /* Let the tile flex to whatever min/max width the AutoGrid sets - via repeat(auto-fill, minmax(N, 1fr)) - hard-pinning 53x74 was - overriding the layout's columnMinWidth prop, so the row count - never changed when we reduced it. Width is now 100% of the - column cell, height tracks --nitro-grid-column-min-height. */ width: 100% !important; height: var(--nitro-grid-column-min-height, 70px) !important; min-width: 0 !important; @@ -905,19 +795,10 @@ overflow: visible !important; } -/* Furni tiles drive their look from the icon image and need a clear - background. Color-grouping swatches use itemHighlight (.has-highlight) - to ask LayoutGridItem for a solid colour via inline backgroundColor - - keep the transparent override off those so the swatch is visible. */ .nitro-catalog-classic-window .layout-grid-item:not(.has-highlight) { background-color: transparent !important; } -/* Pets breed picker: the pet-head image renders at its native ~81x69px - regardless of the grid cell size. The old grid-cols-6 stretched - each .layout-grid-item to ~150-200px so the head looked huge in - acres of empty tile. Pin each tile just big enough to host the - head and let the container flex-wrap. */ .nitro-catalog-classic-window .nitro-catalog-classic-pet-breeds .layout-grid-item { width: 84px !important; min-width: 84px !important; @@ -938,12 +819,6 @@ inset -2px -2px 0 #ecece4 !important; } -/* Habbo-classic colour swatches: small chip with a 1px dark border - and a subtle inner highlight so light tones still read as buttons. - Hover lifts the border; the selected swatch is "pressed" with a - sunken inner shadow and a bright cyan ring matching the catalog - selection accent. The cream inset from the generic .is-active rule - above would wash out the swatch colour, so we replace it here. */ .nitro-catalog-classic-window .layout-grid-item.has-highlight { width: 26px !important; height: 26px !important; @@ -975,30 +850,20 @@ } .nitro-catalog-classic-grid-offer-icon { - position: absolute; - left: 50%; - top: 20px; width: auto !important; height: auto !important; - max-width: 36px; - max-height: 36px; + image-rendering: pixelated; object-fit: contain; - transform: translate(-50%, -50%); pointer-events: none; } .nitro-catalog-classic-grid-price { - position: absolute; - left: 2px; - right: 2px; - top: 36px; - bottom: auto; display: flex; - flex-direction: column; + flex-direction: row; align-items: center; - justify-content: flex-start; - gap: 0; - min-height: 24px; + justify-content: center; + gap: 3px; + width: 100%; color: #000; font-size: 11px; font-weight: 700; @@ -1007,18 +872,10 @@ text-shadow: 0 1px 0 rgba(255, 255, 255, 0.75); } -/* When the tile shows a full-tile bot/pet avatar (instead of a small - icon), pin the price strip to the bottom of the tile and give it a - translucent backdrop so it doesn't overlap with the avatar body. */ .nitro-catalog-classic-grid .layout-grid-item:has(.avatar-image) .nitro-catalog-classic-grid-price, .nitro-catalog-classic-grid .layout-grid-item:has(> .avatar-image) > .nitro-catalog-classic-grid-price, .nitro-catalog-classic-grid .avatar-image ~ .nitro-catalog-classic-grid-price { top: auto !important; - /* Re-anchor horizontally too: the parent rule's left: 2px / - right: 2px combined with content-sized inner flex was visually - parking the pill at the left side of the tile. Center it via - explicit left/right + transform so it lands smack in the - middle regardless of inner content width. */ left: 50% !important; right: auto !important; bottom: 4px !important; @@ -1036,10 +893,6 @@ z-index: 5 !important; } -/* Tighten the price entry inside the avatar-tile pill so the number - and currency icon center on the same baseline (the global - .grid-price-entry height: 13px clipped the 15px wallet icon and - pushed it visually below the number). */ .nitro-catalog-classic-grid .layout-grid-item:has(.avatar-image) .nitro-catalog-classic-grid-price-entry, .nitro-catalog-classic-grid .avatar-image ~ .nitro-catalog-classic-grid-price .nitro-catalog-classic-grid-price-entry { height: auto !important; @@ -1059,11 +912,15 @@ } .nitro-catalog-classic-grid-price.is-single-price { - height: 19px; + height: auto; + min-height: 0; } .nitro-catalog-classic-grid-price.is-multi-price { - height: 38px; + height: auto; + min-height: 0; + flex-wrap: wrap; + row-gap: 1px; } .nitro-catalog-classic-grid-price-entry { @@ -1092,8 +949,6 @@ .nitro-catalog-classic-price-row { position: absolute; left: 0; - /* Anchored from the bottom so the Aantal/Prezzo row sits just - above the Cadeau/Koop buttons regardless of layout height. */ bottom: 38px; width: 360px; height: 25px; @@ -1111,8 +966,6 @@ .nitro-catalog-classic-total-price-slot { position: absolute; - /* Anchored to the right of the now-100% wide price row so the - Prezzo + amount stays flush with the right edge of the panel. */ right: 2px; top: 0; width: auto; @@ -1135,9 +988,6 @@ .nitro-catalog-classic-purchase-row { position: absolute; left: 0; - /* Anchored to the bottom of the panel with a 4px breathing strip - so the Cadeau / Koop buttons stay flush at the bottom of the - window no matter how tall the catalog is. */ bottom: 4px; width: 360px; height: 30px; @@ -1151,9 +1001,6 @@ align-items: flex-start; justify-content: space-between; gap: 10px !important; - /* Fill the now-100% wide purchase row instead of staying pinned at - 330px (which used to match the old 360px column - 15px each - side). */ width: auto; height: 24px; margin-left: 15px; @@ -1213,13 +1060,6 @@ opacity: 1 !important; } -/* Buy / Gift buttons - pure CSS. border-image-slicing the bitmap - sprites produced thin highlight/shadow stripes at the top and - bottom because the source rounded corners are ~5-6px tall but the - buttons render at 22-24px, so the slice rows stretched into a - visible band. CSS gradients give a crisp pixel-art classic-habbo - look without those artefacts. */ - .nitro-catalog-classic-window .nitro-catalog-swf-buy-button { width: 160px !important; min-width: 160px !important; @@ -1229,8 +1069,6 @@ border-radius: 4px !important; border-image: none !important; border-image-source: none !important; - /* Yellow body with the same #f0a318 / #ffd54d tones as the - skin3-yellow Buy sprite. */ background: linear-gradient(180deg, #ffe66b 0%, #ffc828 45%, #f0a318 100%) !important; box-shadow: @@ -1257,9 +1095,6 @@ .nitro-catalog-classic-window .nitro-catalog-swf-buy-button.pointer-events-none, .nitro-catalog-classic-window .nitro-catalog-swf-buy-button:disabled { - /* Stay yellow when disabled - the user wants the action colour - to be recognisable regardless of state. Drop opacity + flip - the cursor so it still reads as non-interactive. */ background: linear-gradient(180deg, #ffe66b 0%, #ffc828 45%, #f0a318 100%) !important; color: #4a2b00 !important; @@ -1277,7 +1112,6 @@ border-radius: 4px !important; border-image: none !important; border-image-source: none !important; - /* Cream / light-gray body matching the catalog cardstock. */ background: linear-gradient(180deg, #ececec 0%, #cfcfc4 100%) !important; box-shadow: @@ -1310,19 +1144,12 @@ text-shadow: none !important; } -/* Pet purchase card lives in a tight flex row alongside the price, - so the main 160px Buy button doesn't fit. Shrink it down here. */ .nitro-catalog-classic-pet-card .nitro-catalog-swf-buy-button { width: auto !important; min-width: 0 !important; padding: 0 14px !important; } -/* All catalog grids must scroll vertically only - horizontal overflow - produces a stray horizontal scrollbar at the bottom of the items - strip on narrow columns (e.g. guild_furni). minmax(N, 1fr) usually - contains content but the safety net stops any odd item from - triggering a horizontal bar. */ .nitro-catalog-classic-window .layout-grid, .nitro-catalog-classic-window [class*="grid-cols-["] { overflow-x: hidden !important; @@ -1369,8 +1196,6 @@ image-rendering: pixelated !important; } -/* react-icons FaMinus/FaPlus glyphs ride inside these buttons; hide - them - the sprite already contains the +/- mark. */ .nitro-catalog-classic-window button.nitro-catalog-swf-spinner-button svg { display: none !important; } @@ -1466,9 +1291,6 @@ min-width: 25px; padding: 0 !important; overflow: hidden; - /* font-size: 0 was killing the SVG: react-icons emits - , so 0em -> 0x0. Use a real - font-size and pin the SVG to explicit pixels below. */ font-size: 14px !important; line-height: 1 !important; display: inline-flex !important; @@ -1495,13 +1317,6 @@ right: 6px; } -/* Bulletproof override for the rotate/state buttons. The shared SWF - button rule above lays a transparent body + border-image skin on - top, which works only when the catalog/buttons/btn_secondary*.png - sprites resolve - if they're missing the button renders 0x0 - invisible. Pin the box and paint a visible gradient + outline so - the controls are always discoverable, and force z-index above the - room-previewer DIV so they sit on top of the rendered scene. */ .nitro-catalog-classic-window button.nitro-catalog-classic-preview-btn { width: 28px !important; height: 26px !important; @@ -1542,12 +1357,6 @@ height: 17px; } -/* ===== Classic catalog scrollbar (pure CSS, no sprites) ===== - Drew this with CSS gradients instead of stretching the 17x34 - skin1 thumb sprite. The sprite version pixelated into visible - horizontal bands on tall scroll areas because every source row - stretched 5-10x. CSS gradients stay crisp at any height. */ - .nitro-catalog-classic-window * { scrollbar-color: auto !important; scrollbar-width: auto; @@ -1566,10 +1375,6 @@ border: 0 !important; } -/* Habbo thumb: symmetric light-edges -> darker-middle gradient (the - "pinched in the middle" look of the classic Ubuntu scrollbar), - 1px near-black outline, three central grip lines via SVG centered - no-repeat. */ .nitro-catalog-classic-window *::-webkit-scrollbar-thumb { min-height: 28px !important; border: 1px solid #2a2a26 !important; @@ -1599,8 +1404,6 @@ inset 0 -1px 0 rgba(255, 255, 255, 0.25) !important; } -/* Arrow buttons: cream cap with a 1px black outline + dark inset - chevron. SVG glyphs so they stay crisp at any zoom. */ .nitro-catalog-classic-window *::-webkit-scrollbar-button:single-button:vertical:decrement { display: block !important; width: 17px !important; @@ -1702,12 +1505,6 @@ padding: 6px !important; } - /* Mobile: drop the per-page welcome row (image + localization - blurb shown when no offer is selected). On a narrow viewport - it eats most of the visible space and pushes the actual grid - off-screen. Hide it and also collapse the surrounding - product-view (otherwise its 240px height reservation stays - and leaves a blank strip above the grid). */ .nitro-catalog-classic-welcome { display: none !important; } From 7e72e34088dfd261fe8804d3e6fc0e2ba47e50f8 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sat, 6 Jun 2026 02:43:45 +0200 Subject: [PATCH 07/38] feat(furni-editor): updateFurnidata/revertFurnidata hook actions --- src/hooks/furni-editor/useFurniEditor.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/hooks/furni-editor/useFurniEditor.ts b/src/hooks/furni-editor/useFurniEditor.ts index cb62d86..201b735 100644 --- a/src/hooks/furni-editor/useFurniEditor.ts +++ b/src/hooks/furni-editor/useFurniEditor.ts @@ -1,4 +1,4 @@ -import { FurniEditorBySpriteComposer, FurniEditorDeleteComposer, FurniEditorDetailComposer, FurniEditorDetailResultEvent, FurniEditorInteractionsComposer, FurniEditorInteractionsResultEvent, FurniEditorResultEvent, FurniEditorSearchComposer, FurniEditorSearchResultEvent, FurniEditorUpdateComposer } from '@nitrots/nitro-renderer'; +import { FurniEditorBySpriteComposer, FurniEditorDeleteComposer, FurniEditorDetailComposer, FurniEditorDetailResultEvent, FurniEditorInteractionsComposer, FurniEditorInteractionsResultEvent, FurniEditorResultEvent, FurniEditorRevertFurnidataComposer, FurniEditorSearchComposer, FurniEditorSearchResultEvent, FurniEditorUpdateComposer, FurniEditorUpdateFurnidataComposer } from '@nitrots/nitro-renderer'; import { useCallback, useRef, useState } from 'react'; import { NotificationAlertType, SendMessageComposer } from '../../api'; import { useMessageEvent, useNotification } from '../../hooks'; @@ -246,6 +246,20 @@ export const useFurniEditor = () => SendMessageComposer(new FurniEditorDeleteComposer(id)); }, []); + const updateFurnidata = useCallback((id: number, name: string, description: string) => + { + pendingActionRef.current = { action: 'update', itemId: id }; + setLoading(true); + SendMessageComposer(new FurniEditorUpdateFurnidataComposer(id, JSON.stringify({ name, description }))); + }, []); + + const revertFurnidata = useCallback((id: number) => + { + pendingActionRef.current = { action: 'update', itemId: id }; + setLoading(true); + SendMessageComposer(new FurniEditorRevertFurnidataComposer(id)); + }, []); + const loadInteractions = useCallback(() => { SendMessageComposer(new FurniEditorInteractionsComposer()); @@ -255,6 +269,7 @@ export const useFurniEditor = () => items, total, page, loading, error, clearError, selectedItem, setSelectedItem, catalogItems, furniDataEntry, interactions, - searchItems, loadDetail, loadBySpriteId, updateItem, deleteItem, loadInteractions + searchItems, loadDetail, loadBySpriteId, updateItem, deleteItem, loadInteractions, + updateFurnidata, revertFurnidata }; }; From d7e294b343b843ee2894be2f1390aa1e5b6fda1b Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sat, 6 Jun 2026 02:46:48 +0200 Subject: [PATCH 08/38] feat(furni-editor): editable furnidata name/desc section + read-only classname/public_name + diff-confirm + revert --- .../furni-editor/FurniEditorView.tsx | 5 +- .../views/FurniEditorEditView.tsx | 66 +++++++++++++------ 2 files changed, 51 insertions(+), 20 deletions(-) diff --git a/src/components/furni-editor/FurniEditorView.tsx b/src/components/furni-editor/FurniEditorView.tsx index 65206ba..6133b9b 100644 --- a/src/components/furni-editor/FurniEditorView.tsx +++ b/src/components/furni-editor/FurniEditorView.tsx @@ -18,7 +18,8 @@ export const FurniEditorView: FC<{}> = () => items, total, page, loading, error, clearError, selectedItem, setSelectedItem, furniDataEntry, interactions, - searchItems, loadDetail, loadBySpriteId, updateItem, deleteItem, loadInteractions + searchItems, loadDetail, loadBySpriteId, updateItem, deleteItem, loadInteractions, + updateFurnidata, revertFurnidata } = useFurniEditor(); const isMod = useHasPermission('acc_catalogfurni'); @@ -155,6 +156,8 @@ export const FurniEditorView: FC<{}> = () => onUpdate={ updateItem } onDelete={ deleteItem } onBack={ handleBack } + onUpdateFurnidata={ updateFurnidata } + onRevertFurnidata={ revertFurnidata } /> } diff --git a/src/components/furni-editor/views/FurniEditorEditView.tsx b/src/components/furni-editor/views/FurniEditorEditView.tsx index 0d9fe2d..22c7e14 100644 --- a/src/components/furni-editor/views/FurniEditorEditView.tsx +++ b/src/components/furni-editor/views/FurniEditorEditView.tsx @@ -11,6 +11,8 @@ interface FurniEditorEditViewProps onUpdate: (id: number, fields: Record) => void; onDelete: (id: number) => void; onBack: () => void; + onUpdateFurnidata: (id: number, name: string, description: string) => void; + onRevertFurnidata: (id: number) => void; } const FIELD_TIPS: Record = { @@ -65,7 +67,7 @@ const Tip: FC<{ field: string }> = ({ field }) => export const FurniEditorEditView: FC = props => { - const { item, furniDataEntry, interactions, loading, onUpdate, onDelete, onBack } = props; + const { item, furniDataEntry, interactions, loading, onUpdate, onDelete, onBack, onUpdateFurnidata, onRevertFurnidata } = props; const saveRef = useRef<() => void>(null); const [ form, setForm ] = useState({ @@ -91,6 +93,9 @@ export const FurniEditorEditView: FC = props => }); const [ showDeleteDialog, setShowDeleteDialog ] = useState(false); + const [ furniName, setFurniName ] = useState(''); + const [ furniDescription, setFurniDescription ] = useState(''); + const [ confirmFurnidata, setConfirmFurnidata ] = useState(false); useEffect(() => { @@ -119,7 +124,10 @@ export const FurniEditorEditView: FC = props => }); setShowDeleteDialog(false); - }, [ item ]); + setFurniName(String(furniDataEntry?.name ?? '')); + setFurniDescription(String(furniDataEntry?.description ?? '')); + setConfirmFurnidata(false); + }, [ item, furniDataEntry ]); const setField = useCallback((key: string, value: unknown) => { @@ -209,6 +217,7 @@ export const FurniEditorEditView: FC = props => const inputClass = (field?: string) => `w-full px-2 py-1 text-xs leading-normal rounded-sm border border-[#ccc] min-h-[calc(1.5em+0.5rem+2px)] ${ field && validation[field] ? 'border-red-500 bg-red-50' : '' }`; const labelClass = 'text-[11px] font-bold text-[#333] mb-0 flex items-center gap-0.5'; + const readonlyClass = 'w-full px-2 py-1 text-sm font-mono rounded-sm border border-[#ddd] bg-[#f2f2eb] text-[#555] select-all'; return ( @@ -232,14 +241,12 @@ export const FurniEditorEditView: FC = props =>
- - setField('itemName', e.target.value) } /> - { validation.itemName && { validation.itemName } } + +
{ form.itemName }
- - setField('publicName', e.target.value) } /> - { validation.publicName && { validation.publicName } } + +
{ form.publicName }
@@ -320,18 +327,24 @@ export const FurniEditorEditView: FC = props =>
- { furniDataEntry && -
-
- { Object.entries(furniDataEntry).map(([ key, value ]) => ( -
- { key } - { String(value ?? '') } -
- )) } +
+ +
+ + setFurniName(e.target.value) } maxLength={ 256 } />
-
- } +
+ +