mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 23:16:21 +00:00
4e96355a94
preloadUrl salta le URL che finiscono con '/' (le gamedata split sono directory): prima 404avano e sprecavano connessioni all'avvio.
632 lines
25 KiB
TypeScript
632 lines
25 KiB
TypeScript
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, useEffectEvent, useRef, useState } from 'react';
|
|
import { ClearRememberLogin, GetRememberLogin, GetUIVersion, SetRememberLogin, StoreRememberLoginFromPayload, persistAccessTokenFromPayload } from './api';
|
|
import { Base } from './common';
|
|
import { LoadingView } from './components/loading/LoadingView';
|
|
import { LoginView } from './components/login/LoginView';
|
|
import { MainView } from './components/MainView';
|
|
import { ReconnectView } from './components/reconnect/ReconnectView';
|
|
import { useMessageEvent, useNitroEvent } from './hooks';
|
|
|
|
NitroVersion.UI_VERSION = GetUIVersion();
|
|
|
|
const getViewportDimensions = () =>
|
|
{
|
|
const viewport = window.visualViewport;
|
|
const width = Math.max(1, Math.floor(viewport?.width ?? window.innerWidth));
|
|
const height = Math.max(1, Math.floor(viewport?.height ?? window.innerHeight));
|
|
|
|
return { width, height };
|
|
};
|
|
|
|
const syncViewportCssVars = () =>
|
|
{
|
|
const { width, height } = getViewportDimensions();
|
|
|
|
document.documentElement.style.setProperty('--nitro-app-width', `${ width }px`);
|
|
document.documentElement.style.setProperty('--nitro-app-height', `${ height }px`);
|
|
};
|
|
|
|
const preloadUrl = async (url: string): Promise<void> =>
|
|
{
|
|
if(!url) return;
|
|
|
|
// Split gamedata URLs are directories (end with '/'); fetching them as a
|
|
// file just 404s and wastes a connection at startup. The real split loader
|
|
// handles those — only warm up actual file URLs here.
|
|
if(url.split('?')[0].split('#')[0].endsWith('/')) 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 [];
|
|
};
|
|
|
|
const hasRememberLogin = (): boolean => !!GetRememberLogin();
|
|
|
|
export const App: FC<{}> = props =>
|
|
{
|
|
const [ isReady, setIsReady ] = useState(false);
|
|
const [ errorMessage, setErrorMessage ] = useState('');
|
|
const [ homeUrl, setHomeUrl ] = useState('');
|
|
const [ showLogin, setShowLogin ] = useState(false);
|
|
const [ isEnteringHotel, setIsEnteringHotel ] = useState(() => !!window.NitroConfig?.['sso.ticket'] || hasRememberLogin());
|
|
const [ prepareTrigger, setPrepareTrigger ] = useState(0);
|
|
const [ loadingProgress, setLoadingProgress ] = useState(0);
|
|
const [ loadingTask, setLoadingTask ] = useState('');
|
|
const taskLabel = useCallback((key: string, fallback: string): string =>
|
|
{
|
|
try
|
|
{
|
|
const locManager = GetLocalizationManager();
|
|
if(locManager && typeof locManager.getValue === 'function')
|
|
{
|
|
const fromLoc = locManager.getValue(key, false);
|
|
|
|
if(typeof fromLoc === 'string' && fromLoc.length && fromLoc !== key) return fromLoc;
|
|
}
|
|
}
|
|
catch
|
|
{ }
|
|
|
|
try
|
|
{
|
|
const fromConfig = GetConfiguration().getValue<string>(key, '');
|
|
if(typeof fromConfig === 'string' && fromConfig.length) return fromConfig;
|
|
}
|
|
catch
|
|
{ }
|
|
|
|
return fallback;
|
|
}, []);
|
|
const bumpProgress = useCallback((value: number, task?: string) =>
|
|
{
|
|
setLoadingProgress(prev => (value > prev ? value : prev));
|
|
if(task !== undefined) setLoadingTask(task);
|
|
}, []);
|
|
const warmupPromiseRef = useRef<Promise<void>>(null);
|
|
const rendererPromiseRef = useRef<Promise<any>>(null);
|
|
const gameInitPromiseRef = useRef<Promise<void> | null>(null);
|
|
const bootstrapDoneRef = useRef(false);
|
|
const lastPrepareTriggerRef = useRef<number | null>(null);
|
|
const tickersStartedRef = useRef(false);
|
|
const heartbeatIntervalRef = useRef<number>(null);
|
|
const rememberRotateIntervalRef = useRef<number>(null);
|
|
const isReadyRef = useRef(false);
|
|
const reconnectInProgressRef = useRef(false);
|
|
|
|
const clearStoredCredentials = useCallback(() =>
|
|
{
|
|
ClearRememberLogin();
|
|
try { delete (window as any).NitroConfig?.['sso.ticket']; } catch {}
|
|
try { GetConfiguration().setValue('sso.ticket', ''); } catch {}
|
|
try
|
|
{
|
|
const url = new URL(window.location.href);
|
|
|
|
if(url.searchParams.has('sso'))
|
|
{
|
|
url.searchParams.delete('sso');
|
|
window.history.replaceState({}, '', url.toString());
|
|
}
|
|
}
|
|
catch {}
|
|
}, []);
|
|
|
|
const showSessionExpired = useCallback(() =>
|
|
{
|
|
console.warn('[App] showSessionExpired — diagnostic shown (mid-game close)');
|
|
clearStoredCredentials();
|
|
|
|
const baseUrl = window.location.origin + '/';
|
|
setHomeUrl(baseUrl);
|
|
setErrorMessage('Your session has expired.\nPlease log in again to enter the hotel.');
|
|
setIsReady(false);
|
|
setShowLogin(false);
|
|
setIsEnteringHotel(false);
|
|
}, [ clearStoredCredentials ]);
|
|
|
|
const fallbackToLogin = useCallback(() =>
|
|
{
|
|
const rawLoginEnabled = GetConfiguration().getValue<unknown>('login.screen.enabled', false);
|
|
const loginScreenEnabled = rawLoginEnabled === true || rawLoginEnabled === 'true' || rawLoginEnabled === 1;
|
|
|
|
if(!loginScreenEnabled)
|
|
{
|
|
console.warn('[App] fallbackToLogin — login.screen.enabled=false, redirecting to home instead');
|
|
showSessionExpired();
|
|
return;
|
|
}
|
|
console.warn('[App] fallbackToLogin — surfacing login form, credentials cleared');
|
|
clearStoredCredentials();
|
|
setHomeUrl('');
|
|
setErrorMessage('');
|
|
setIsReady(false);
|
|
setShowLogin(true);
|
|
setIsEnteringHotel(false);
|
|
}, [ clearStoredCredentials, showSessionExpired ]);
|
|
|
|
const applySsoTicket = useCallback((ssoTicket: string) =>
|
|
{
|
|
if(!ssoTicket) return;
|
|
window.NitroConfig['sso.ticket'] = ssoTicket;
|
|
GetConfiguration().setValue('sso.ticket', ssoTicket);
|
|
}, []);
|
|
|
|
const handleAuthenticated = useCallback((ssoTicket: string) =>
|
|
{
|
|
if(!ssoTicket) return;
|
|
applySsoTicket(ssoTicket);
|
|
setIsEnteringHotel(true);
|
|
setErrorMessage('');
|
|
setPrepareTrigger(prev => prev + 1);
|
|
}, [ applySsoTicket ]);
|
|
|
|
const tryRememberLogin = useCallback(async (): Promise<string> =>
|
|
{
|
|
const remembered = GetRememberLogin();
|
|
|
|
console.warn('[App] tryRememberLogin start', {
|
|
hasRemembered: !!remembered,
|
|
hasToken: !!remembered?.token?.length,
|
|
hasStoredSso: !!remembered?.ssoTicket?.length
|
|
});
|
|
|
|
if(!remembered?.token?.length)
|
|
{
|
|
if(remembered) ClearRememberLogin();
|
|
console.warn('[App] tryRememberLogin → no token, returning empty');
|
|
return '';
|
|
}
|
|
|
|
try
|
|
{
|
|
const rawEndpoint = GetConfiguration().getValue<string>('login.remember.endpoint', '${api.url}/api/auth/remember');
|
|
const endpoint = GetConfiguration().interpolate(rawEndpoint);
|
|
const response = await fetch(endpoint, {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
'X-Requested-With': 'NitroRememberLogin'
|
|
},
|
|
body: JSON.stringify({ rememberToken: remembered.token })
|
|
});
|
|
|
|
let payload: Record<string, unknown> = {};
|
|
try
|
|
{
|
|
payload = await response.json();
|
|
}
|
|
catch
|
|
{}
|
|
|
|
const ssoTicket = typeof payload.ssoTicket === 'string' ? payload.ssoTicket : (typeof payload.sso === 'string' ? payload.sso : '');
|
|
|
|
console.warn('[App] tryRememberLogin → remember endpoint replied', {
|
|
status: response.status,
|
|
ok: response.ok,
|
|
gotSsoTicket: !!ssoTicket
|
|
});
|
|
|
|
if(response.ok && ssoTicket)
|
|
{
|
|
persistAccessTokenFromPayload(payload);
|
|
StoreRememberLoginFromPayload(payload, typeof payload.username === 'string' ? payload.username : remembered.username, ssoTicket);
|
|
return ssoTicket;
|
|
}
|
|
}
|
|
catch(error)
|
|
{
|
|
console.warn('[App] tryRememberLogin → fetch threw', error);
|
|
}
|
|
|
|
ClearRememberLogin();
|
|
console.warn('[App] tryRememberLogin → cleared remember, returning empty');
|
|
|
|
return '';
|
|
}, []);
|
|
|
|
const rotateRememberLogin = useCallback(async (): Promise<void> =>
|
|
{
|
|
const remembered = GetRememberLogin();
|
|
|
|
if(!remembered?.token?.length) return;
|
|
|
|
try
|
|
{
|
|
const rawEndpoint = GetConfiguration().getValue<string>('login.refresh.endpoint', '${api.url}/api/auth/refresh');
|
|
const endpoint = GetConfiguration().interpolate(rawEndpoint);
|
|
const response = await fetch(endpoint, {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
'X-Requested-With': 'NitroRememberRotate'
|
|
},
|
|
body: JSON.stringify({ rememberToken: remembered.token })
|
|
});
|
|
|
|
let payload: Record<string, unknown> = {};
|
|
try
|
|
{
|
|
payload = await response.json();
|
|
}
|
|
catch
|
|
{}
|
|
|
|
if(response.ok)
|
|
{
|
|
persistAccessTokenFromPayload(payload);
|
|
StoreRememberLoginFromPayload(payload, remembered.username, remembered.ssoTicket);
|
|
return;
|
|
}
|
|
|
|
if(response.status === 400 || response.status === 401 || response.status === 403) ClearRememberLogin();
|
|
}
|
|
catch(error)
|
|
{
|
|
NitroLogger.error('[LoginScreen] Remember rotation failed', error);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => { isReadyRef.current = isReady; }, [ isReady ]);
|
|
useNitroEvent(NitroEventType.SOCKET_RECONNECTING, () => { reconnectInProgressRef.current = true; });
|
|
useNitroEvent(NitroEventType.SOCKET_REAUTHENTICATED, () => { reconnectInProgressRef.current = false; });
|
|
|
|
useNitroEvent(NitroEventType.SOCKET_CLOSED, () =>
|
|
{
|
|
console.warn('[App] SOCKET_CLOSED fired', {
|
|
isReady: isReadyRef.current,
|
|
reconnectInProgress: reconnectInProgressRef.current
|
|
});
|
|
|
|
if(!isReadyRef.current)
|
|
{
|
|
console.warn('[App] Socket closed before authentication completed — falling back to login');
|
|
fallbackToLogin();
|
|
return;
|
|
}
|
|
|
|
if(reconnectInProgressRef.current) return;
|
|
|
|
showSessionExpired();
|
|
});
|
|
|
|
useMessageEvent<LoadGameUrlEvent>(LoadGameUrlEvent, event =>
|
|
{
|
|
const parser = event.getParser();
|
|
|
|
if(!parser) return;
|
|
|
|
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();
|
|
bumpProgress(25, taskLabel('loader.waiting', 'Loading content...'));
|
|
|
|
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));
|
|
|
|
const warmupTasks: { promise: Promise<any>; label: string }[] = [
|
|
{ promise: GetAssetManager().downloadAssets(assetUrls), label: taskLabel('loading.task.assets', 'Loading game assets...') },
|
|
{ promise: GetLocalizationManager().init(), label: taskLabel('loading.task.localization', 'Loading translations...') },
|
|
{ promise: GetAvatarRenderManager().init(), label: taskLabel('loading.task.avatar', 'Loading wardrobe...') },
|
|
{ promise: GetSoundManager().init(), label: taskLabel('loading.task.sounds', 'Loading sounds...') }
|
|
];
|
|
let warmupDone = 0;
|
|
const warmupStart = 25;
|
|
const warmupSpan = 45;
|
|
await Promise.all(warmupTasks.map(t => t.promise.then(value =>
|
|
{
|
|
warmupDone++;
|
|
bumpProgress(warmupStart + Math.round((warmupSpan * warmupDone) / warmupTasks.length), t.label);
|
|
return value;
|
|
})));
|
|
})();
|
|
|
|
return warmupPromiseRef.current;
|
|
}, [ startRenderer, bumpProgress, taskLabel ]);
|
|
|
|
useEffect(() =>
|
|
{
|
|
syncViewportCssVars();
|
|
|
|
const handleViewportResize = () => syncViewportCssVars();
|
|
const viewport = window.visualViewport;
|
|
|
|
window.addEventListener('resize', handleViewportResize);
|
|
viewport?.addEventListener('resize', handleViewportResize);
|
|
viewport?.addEventListener('scroll', handleViewportResize);
|
|
|
|
return () =>
|
|
{
|
|
window.removeEventListener('resize', handleViewportResize);
|
|
viewport?.removeEventListener('resize', handleViewportResize);
|
|
viewport?.removeEventListener('scroll', handleViewportResize);
|
|
};
|
|
}, []);
|
|
|
|
const onSessionExpired = useEffectEvent(() => showSessionExpired());
|
|
const onInitFailure = useEffectEvent(() => fallbackToLogin());
|
|
|
|
useEffect(() =>
|
|
{
|
|
const prepare = async (width: number, height: number) =>
|
|
{
|
|
console.warn('[App] prepare() start', {
|
|
hasNitroConfig: !!window.NitroConfig,
|
|
ssoTicketInConfig: !!window.NitroConfig?.['sso.ticket'],
|
|
hasRememberLocal: !!GetRememberLogin(),
|
|
hasUrlSso: !!new URLSearchParams(window.location.search).get('sso')
|
|
});
|
|
|
|
const bootLabel = taskLabel('loader', 'Booting...');
|
|
setLoadingProgress(0);
|
|
setLoadingTask(bootLabel);
|
|
bumpProgress(5, bootLabel);
|
|
|
|
try
|
|
{
|
|
if(!window.NitroConfig) throw new Error('NitroConfig is not defined!');
|
|
|
|
let ssoTicket = window.NitroConfig['sso.ticket'];
|
|
if(ssoTicket) GetConfiguration().setValue('sso.ticket', ssoTicket);
|
|
|
|
try
|
|
{
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const tokenParam = urlParams.get('token');
|
|
const tokenExpParam = urlParams.get('token_exp');
|
|
if(tokenParam && !GetRememberLogin())
|
|
{
|
|
const parsedExpiry = Number(tokenExpParam || 0);
|
|
const expiresAt = (Number.isFinite(parsedExpiry) && parsedExpiry > 0)
|
|
? parsedExpiry
|
|
: Math.floor(Date.now() / 1000) + (30 * 24 * 60 * 60);
|
|
SetRememberLogin({ token: tokenParam, expiresAt });
|
|
}
|
|
}
|
|
catch(e)
|
|
{
|
|
console.warn('[App] failed to persist remember token from URL', e);
|
|
}
|
|
|
|
bumpProgress(10, taskLabel('loading.task.session', 'Verifying session...'));
|
|
|
|
if(!ssoTicket || ssoTicket === '')
|
|
{
|
|
let configInitError: unknown = null;
|
|
try
|
|
{
|
|
await GetConfiguration().init();
|
|
}
|
|
catch(e)
|
|
{
|
|
configInitError = e;
|
|
}
|
|
|
|
const rawLoginEnabled = GetConfiguration().getValue<unknown>('login.screen.enabled', false);
|
|
const loginScreenEnabled = rawLoginEnabled === true || rawLoginEnabled === 'true' || rawLoginEnabled === 1;
|
|
|
|
console.warn('[App] no SSO path — login gate', {
|
|
configInitError: configInitError ? String((configInitError as Error)?.message ?? configInitError) : null,
|
|
rawLoginEnabled,
|
|
rawLoginEnabledType: typeof rawLoginEnabled,
|
|
loginScreenEnabled
|
|
});
|
|
|
|
if(configInitError)
|
|
{
|
|
NitroLogger.error('[LoginScreen] Failed to load renderer-config.json — cannot resolve login.screen.enabled', configInitError);
|
|
}
|
|
|
|
if(loginScreenEnabled)
|
|
{
|
|
const rememberedSsoTicket = await tryRememberLogin();
|
|
|
|
if(rememberedSsoTicket)
|
|
{
|
|
ssoTicket = rememberedSsoTicket;
|
|
applySsoTicket(rememberedSsoTicket);
|
|
setShowLogin(false);
|
|
}
|
|
else
|
|
{
|
|
setIsReady(false);
|
|
setShowLogin(true);
|
|
startWarmup(width, height).catch(error => NitroLogger.error('[LoginScreen] Warmup failed', error));
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
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;
|
|
}
|
|
|
|
onSessionExpired();
|
|
return;
|
|
}
|
|
}
|
|
|
|
const renderer = await startRenderer(width, height);
|
|
bumpProgress(20, taskLabel('loading.task.renderer', 'Initializing renderer...'));
|
|
|
|
await startWarmup(width, height);
|
|
bumpProgress(70, taskLabel('loading.task.startsession', 'Starting session...'));
|
|
|
|
if(!gameInitPromiseRef.current)
|
|
{
|
|
gameInitPromiseRef.current = (async () =>
|
|
{
|
|
await GetSessionDataManager().init();
|
|
bumpProgress(78, taskLabel('loading.task.userdata', 'Loading user data...'));
|
|
await GetRoomSessionManager().init();
|
|
bumpProgress(85, taskLabel('loading.task.rooms', 'Loading rooms...'));
|
|
await GetRoomEngine().init();
|
|
bumpProgress(92, taskLabel('loading.task.engine', 'Loading graphics engine...'));
|
|
await GetCommunication().init();
|
|
bumpProgress(98, taskLabel('generic.reconnecting', 'Connecting to server...'));
|
|
})();
|
|
}
|
|
|
|
await gameInitPromiseRef.current;
|
|
|
|
if(!bootstrapDoneRef.current)
|
|
{
|
|
bootstrapDoneRef.current = true;
|
|
if(LegacyExternalInterface.available) LegacyExternalInterface.call('legacyTrack', 'authentication', 'authok', []);
|
|
HabboWebTools.sendHeartBeat();
|
|
}
|
|
|
|
if(heartbeatIntervalRef.current !== null) window.clearInterval(heartbeatIntervalRef.current);
|
|
heartbeatIntervalRef.current = window.setInterval(() => HabboWebTools.sendHeartBeat(), 10000);
|
|
|
|
if(rememberRotateIntervalRef.current !== null) window.clearInterval(rememberRotateIntervalRef.current);
|
|
|
|
const rotateMinutes = Math.max(1, Number(GetConfiguration().getValue<unknown>('login.remember.rotate.interval.minutes', 15)) || 15);
|
|
if(GetRememberLogin()?.token?.length) rememberRotateIntervalRef.current = window.setInterval(() => rotateRememberLogin(), rotateMinutes * 60 * 1000);
|
|
|
|
if(!tickersStartedRef.current)
|
|
{
|
|
tickersStartedRef.current = true;
|
|
GetTicker().add(ticker => GetRoomEngine().update(ticker));
|
|
GetTicker().add(ticker => renderer.render(GetStage()));
|
|
GetTicker().add(ticker => GetTexturePool().run());
|
|
}
|
|
|
|
bumpProgress(100, taskLabel('onboarding.button.ready', 'Ready!'));
|
|
setIsReady(true);
|
|
setShowLogin(false);
|
|
setIsEnteringHotel(false);
|
|
}
|
|
catch(err)
|
|
{
|
|
NitroLogger.error('[App] Initialization failed — falling back to login', err);
|
|
onInitFailure();
|
|
}
|
|
};
|
|
|
|
if(lastPrepareTriggerRef.current === prepareTrigger) return;
|
|
lastPrepareTriggerRef.current = prepareTrigger;
|
|
|
|
const { width, height } = getViewportDimensions();
|
|
|
|
prepare(width, height);
|
|
|
|
return () =>
|
|
{
|
|
if(heartbeatIntervalRef.current !== null) window.clearInterval(heartbeatIntervalRef.current);
|
|
if(rememberRotateIntervalRef.current !== null) window.clearInterval(rememberRotateIntervalRef.current);
|
|
};
|
|
}, [ prepareTrigger, startWarmup, startRenderer, tryRememberLogin, applySsoTicket, rotateRememberLogin, bumpProgress, taskLabel ]);
|
|
|
|
return (
|
|
<Base fit overflow="hidden" className={ `nitro-app-root ${ !(window.devicePixelRatio % 1) ? 'image-rendering-pixelated' : '' }` }>
|
|
{ !isReady && !showLogin &&
|
|
<LoadingView isError={ errorMessage.length > 0 } message={ errorMessage } homeUrl={ homeUrl } progress={ loadingProgress } currentTask={ loadingTask } /> }
|
|
{ !isReady && showLogin && <LoginView onAuthenticated={ handleAuthenticated } isEntering={ isEnteringHotel } /> }
|
|
{ isReady && <MainView /> }
|
|
{ isReady && <ReconnectView /> }
|
|
<Base id="draggable-windows-container" />
|
|
</Base>
|
|
);
|
|
};
|