From bf37aef9afd2656fc6cf22ccae0f1d5e5ebdfb17 Mon Sep 17 00:00:00 2001 From: DuckieTM Date: Sat, 6 Jun 2026 08:10:40 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=86=99=20Catalog=20&=20Inventory=20previe?= =?UTF-8?q?w=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; }