mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 23:16:21 +00:00
checkpoint: secure assets and login flow baseline
This commit is contained in:
+155
-60
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user