checkpoint: secure assets and login flow baseline

This commit is contained in:
Lorenzune
2026-04-23 07:01:09 +02:00
parent f6096371be
commit 237c523f9a
17 changed files with 3573 additions and 694 deletions
+155 -60
View File
@@ -1,5 +1,5 @@
import { GetAssetManager, GetAvatarRenderManager, GetCommunication, GetConfiguration, GetLocalizationManager, GetRoomEngine, GetRoomSessionManager, GetSessionDataManager, GetSoundManager, GetStage, GetTexturePool, GetTicker, HabboWebTools, LegacyExternalInterface, LoadGameUrlEvent, NitroEventType, NitroLogger, NitroVersion, PrepareRenderer } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react';
import { FC, useCallback, useEffect, useRef, useState } from 'react';
import { GetUIVersion } from './api';
import { Base } from './common';
import { LoadingView } from './components/loading/LoadingView';
@@ -10,13 +10,51 @@ import { useMessageEvent, useNitroEvent } from './hooks';
NitroVersion.UI_VERSION = GetUIVersion();
const preloadUrl = async (url: string): Promise<void> =>
{
if(!url) return;
try
{
const response = await fetch(url, { cache: 'force-cache' });
await response.arrayBuffer();
}
catch {}
};
const preloadImage = (url: string): void =>
{
if(!url) return;
try
{
const image = new Image();
image.decoding = 'async';
image.src = url;
}
catch {}
};
const asStringArray = (value: unknown): string[] =>
{
if(Array.isArray(value)) return value.filter(item => typeof item === 'string');
if(typeof value === 'string' && value.length) return [ value ];
return [];
};
export const App: FC<{}> = props =>
{
const [ isReady, setIsReady ] = useState(false);
const [ errorMessage, setErrorMessage ] = useState('');
const [ homeUrl, setHomeUrl ] = useState('');
const [ showLogin, setShowLogin ] = useState(false);
const [ showLogin, setShowLogin ] = useState(() => !window.NitroConfig?.['sso.ticket']);
const [ isEnteringHotel, setIsEnteringHotel ] = useState(false);
const [ prepareTrigger, setPrepareTrigger ] = useState(0);
const warmupPromiseRef = useRef<Promise<void>>(null);
const rendererPromiseRef = useRef<Promise<any>>(null);
const tickersStartedRef = useRef(false);
const heartbeatIntervalRef = useRef<number>(null);
const showSessionExpired = useCallback(() =>
{
const baseUrl = window.location.origin + '/';
@@ -24,13 +62,15 @@ export const App: FC<{}> = props =>
setErrorMessage('Your session has expired.\nPlease log in again to enter the hotel.');
setIsReady(false);
setShowLogin(false);
setIsEnteringHotel(false);
}, []);
const handleAuthenticated = useCallback((ssoTicket: string) =>
{
if(!ssoTicket) return;
window.NitroConfig['sso.ticket'] = ssoTicket;
setShowLogin(false);
GetConfiguration().setValue('sso.ticket', ssoTicket);
setIsEnteringHotel(true);
setErrorMessage('');
setPrepareTrigger(prev => prev + 1);
}, []);
@@ -47,10 +87,89 @@ export const App: FC<{}> = props =>
LegacyExternalInterface.callGame('showGame', parser.url);
});
const startRenderer = useCallback((width: number, height: number) =>
{
if(rendererPromiseRef.current) return rendererPromiseRef.current;
const rawUseBackBuffer = window.NitroConfig?.['renderer.useBackBuffer'];
const useBackBuffer = (rawUseBackBuffer === undefined)
? true
: ((rawUseBackBuffer === true) || (rawUseBackBuffer === 'true'));
rendererPromiseRef.current = PrepareRenderer({
width: Math.floor(width),
height: Math.floor(height),
resolution: window.devicePixelRatio,
autoDensity: true,
backgroundAlpha: 0,
preference: 'webgl',
eventMode: 'none',
failIfMajorPerformanceCaveat: false,
roundPixels: true,
useBackBuffer
});
return rendererPromiseRef.current;
}, []);
const startWarmup = useCallback((width: number, height: number) =>
{
if(warmupPromiseRef.current) return warmupPromiseRef.current;
warmupPromiseRef.current = (async () =>
{
await GetConfiguration().init();
GetTicker().maxFPS = GetConfiguration().getValue<number>('system.fps.max', 24);
NitroLogger.LOG_DEBUG = GetConfiguration().getValue<boolean>('system.log.debug', true);
NitroLogger.LOG_WARN = GetConfiguration().getValue<boolean>('system.log.warn', false);
NitroLogger.LOG_ERROR = GetConfiguration().getValue<boolean>('system.log.error', false);
NitroLogger.LOG_EVENTS = GetConfiguration().getValue<boolean>('system.log.events', false);
NitroLogger.LOG_PACKETS = GetConfiguration().getValue<boolean>('system.log.packets', false);
startRenderer(width, height).catch(error => NitroLogger.error('[LoginScreen] Renderer warmup failed', error));
const interpolate = (value: string) => GetConfiguration().interpolate(value);
const assetUrls = asStringArray(GetConfiguration().getValue<unknown>('preload.assets.urls')).map(interpolate);
const gamedataUrls = [
...asStringArray(GetConfiguration().getValue<unknown>('external.texts.url')).map(interpolate),
...[
'furnidata.url',
'productdata.url',
'avatar.actions.url',
'avatar.figuredata.url',
'avatar.figuremap.url',
'avatar.effectmap.url'
].map(key => interpolate(GetConfiguration().getValue<string>(key, ''))).filter(Boolean)
];
const loginImages = ((GetConfiguration().getValue<Record<string, unknown>>('loginview', {})?.images) as Record<string, string>) ?? {};
const loginImageUrls = [
loginImages.background,
loginImages.sun,
loginImages.drape,
loginImages.left,
loginImages['right.repeat'],
loginImages.right
].filter(Boolean).map(interpolate);
loginImageUrls.forEach(preloadImage);
gamedataUrls.forEach(url => preloadUrl(url));
await Promise.all(
[
GetAssetManager().downloadAssets(assetUrls),
GetLocalizationManager().init(),
GetAvatarRenderManager().init(),
GetSoundManager().init()
]
);
})();
return warmupPromiseRef.current;
}, [ startRenderer ]);
useEffect(() =>
{
let heartbeatInterval: number = null;
const prepare = async (width: number, height: number) =>
{
try
@@ -58,6 +177,7 @@ export const App: FC<{}> = props =>
if(!window.NitroConfig) throw new Error('NitroConfig is not defined!');
const ssoTicket = window.NitroConfig['sso.ticket'];
if(ssoTicket) GetConfiguration().setValue('sso.ticket', ssoTicket);
if(!ssoTicket || ssoTicket === '')
{
@@ -79,62 +199,29 @@ export const App: FC<{}> = props =>
{
setIsReady(false);
setShowLogin(true);
startWarmup(width, height).catch(error => NitroLogger.error('[LoginScreen] Warmup failed', error));
return;
}
if(configInitError)
{
setHomeUrl(window.location.origin + '/');
setErrorMessage(`Unable to load renderer-config.json.\n${ String((configInitError as Error)?.message ?? configInitError) }`);
setIsReady(false);
setShowLogin(false);
return;
}
if(configInitError)
{
setHomeUrl(window.location.origin + '/');
setErrorMessage(`Unable to load renderer-config.json.\n${ String((configInitError as Error)?.message ?? configInitError) }`);
setIsReady(false);
setShowLogin(false);
setIsEnteringHotel(false);
return;
}
showSessionExpired();
return;
}
const rawUseBackBuffer = window.NitroConfig['renderer.useBackBuffer'];
const useBackBuffer = (rawUseBackBuffer === undefined)
? true
: ((rawUseBackBuffer === true) || (rawUseBackBuffer === 'true'));
const renderer = await PrepareRenderer({
width: Math.floor(width),
height: Math.floor(height),
resolution: window.devicePixelRatio,
autoDensity: true,
backgroundAlpha: 0,
preference: 'webgl',
eventMode: 'none',
failIfMajorPerformanceCaveat: false,
roundPixels: true,
useBackBuffer // Keep disabled by default unless explicitly enabled in NitroConfig
});
await GetConfiguration().init();
GetTicker().maxFPS = GetConfiguration().getValue<number>('system.fps.max', 24);
NitroLogger.LOG_DEBUG = GetConfiguration().getValue<boolean>('system.log.debug', true);
NitroLogger.LOG_WARN = GetConfiguration().getValue<boolean>('system.log.warn', false);
NitroLogger.LOG_ERROR = GetConfiguration().getValue<boolean>('system.log.error', false);
NitroLogger.LOG_EVENTS = GetConfiguration().getValue<boolean>('system.log.events', false);
NitroLogger.LOG_PACKETS = GetConfiguration().getValue<boolean>('system.log.packets', false);
const assetUrls = GetConfiguration().getValue<string[]>('preload.assets.urls').map(url => GetConfiguration().interpolate(url)) ?? [];
await Promise.all(
[
GetAssetManager().downloadAssets(assetUrls),
GetLocalizationManager().init(),
GetAvatarRenderManager().init(),
GetSoundManager().init(),
GetSessionDataManager().init(),
GetRoomSessionManager().init()
]
);
const renderer = await startRenderer(width, height);
await startWarmup(width, height);
await GetSessionDataManager().init();
await GetRoomSessionManager().init();
await GetRoomEngine().init();
await GetCommunication().init();
@@ -142,17 +229,25 @@ export const App: FC<{}> = props =>
HabboWebTools.sendHeartBeat();
heartbeatInterval = window.setInterval(() => HabboWebTools.sendHeartBeat(), 10000);
if(heartbeatIntervalRef.current !== null) window.clearInterval(heartbeatIntervalRef.current);
heartbeatIntervalRef.current = window.setInterval(() => HabboWebTools.sendHeartBeat(), 10000);
GetTicker().add(ticker => GetRoomEngine().update(ticker));
GetTicker().add(ticker => renderer.render(GetStage()));
GetTicker().add(ticker => GetTexturePool().run());
if(!tickersStartedRef.current)
{
tickersStartedRef.current = true;
GetTicker().add(ticker => GetRoomEngine().update(ticker));
GetTicker().add(ticker => renderer.render(GetStage()));
GetTicker().add(ticker => GetTexturePool().run());
}
setIsReady(true);
setShowLogin(false);
setIsEnteringHotel(false);
}
catch(err)
{
NitroLogger.error(err);
setIsEnteringHotel(false);
showSessionExpired();
}
};
@@ -161,15 +256,15 @@ export const App: FC<{}> = props =>
return () =>
{
if(heartbeatInterval !== null) window.clearInterval(heartbeatInterval);
if(heartbeatIntervalRef.current !== null) window.clearInterval(heartbeatIntervalRef.current);
};
}, [ prepareTrigger ]);
}, [ prepareTrigger, startWarmup, startRenderer ]);
return (
<Base fit overflow="hidden" className={ !(window.devicePixelRatio % 1) && 'image-rendering-pixelated' }>
{ !isReady && !showLogin &&
{ !isReady && !showLogin && errorMessage.length > 0 &&
<LoadingView isError={ errorMessage.length > 0 } message={ errorMessage } homeUrl={ homeUrl } /> }
{ !isReady && showLogin && <LoginView onAuthenticated={ handleAuthenticated } /> }
{ !isReady && showLogin && <LoginView onAuthenticated={ handleAuthenticated } isEntering={ isEnteringHotel } /> }
{ isReady && <MainView /> }
<ReconnectView />
<Base id="draggable-windows-container" />