mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 07:26:19 +00:00
feat(loading): redesigned loader with progress bar, task labels, configurable assets
Loading screen overhaul:
- LoadingView: Nitro V3 logo flush top-left, loading.gif at viewport
centre, large progress bar (max 900px / 90vw, h-8, gradient + glow)
anchored bottom-centre with the percentage rendered inside the bar in
Poppins, plus a friendly stage label underneath. Logo + background +
progress bar colour overridable via renderer-config keys
(loading.logo.url, loading.background, loading.progress.color).
- App.tsx: wired a real loadingProgress (0->100) + loadingTask driven by
the boot pipeline: config init (10), renderer (20), per-warmup-task
bumps for AssetManager/Localization/AvatarRender/SoundManager (25->70),
session managers (78/85/92), Communication (98), ready (100). Each bump
carries a task label looked up via a new taskLabel(key, fallback)
helper so the Italian baseline ("Sto caricando il guardaroba",
"Connessione al server", ...) can be translated by editing
renderer-config; fallback keeps current strings if the key is missing.
- AvatarEffectsView: replace raw fetch(url).json() with
loadGamedata(url) so the effectmap root manifest (JSON5 with
// comments) parses correctly and supports the core/custom/seasonal
tier merge.
- fallbackToLogin: respect login.screen.enabled=false. When login is
disabled (SSO-only deployments), init failures now route to
showSessionExpired() (home + diagnostic) instead of rendering an empty
LoginView placeholder.
- scripts/write-asset-loader.mjs: the pre-React shell rendered into
#root before the JS bundle takes over was a light-blue login skeleton
(linear gradient + two grey rectangles) producing a visible flash
before the real loader appeared. Replaced with the same
radial-gradient the LoadingView paints — the handoff is now invisible.
- renderer-config.example: document the 13 loader keys so operators can
copy & translate.
This commit is contained in:
+88
-25
@@ -72,6 +72,29 @@ export const App: FC<{}> = props =>
|
||||
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('');
|
||||
// Look up a loader-stage label from renderer-config so the strings the user
|
||||
// sees during the boot ("Sto caricando il guardaroba", "Connessione…") can
|
||||
// be translated by editing the JSON/JSON5 config — fallback keeps the
|
||||
// Italian baseline shipped with the client.
|
||||
const taskLabel = useCallback((key: string, fallback: string): string =>
|
||||
{
|
||||
try
|
||||
{
|
||||
const raw = GetConfiguration().getValue<string>(key, '');
|
||||
return (typeof raw === 'string' && raw.length) ? raw : fallback;
|
||||
}
|
||||
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);
|
||||
@@ -104,8 +127,36 @@ export const App: FC<{}> = props =>
|
||||
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(() =>
|
||||
{
|
||||
// When login.screen.enabled is false this hotel uses SSO-only auth
|
||||
// (CMS issues the ticket and redirects here). Surfacing a login form
|
||||
// on init failure would just dump an empty/broken placeholder, since
|
||||
// the form's backgrounds and Turnstile aren't even configured. Send
|
||||
// the user back to the hotel home page instead.
|
||||
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;
|
||||
}
|
||||
|
||||
// Using console.warn (not NitroLogger.log) on purpose: NitroLogger
|
||||
// is gated on LOG_DEBUG, which only flips to true once startWarmup's
|
||||
// GetConfiguration().init() completes. Auth-failure paths fire before
|
||||
@@ -119,20 +170,7 @@ export const App: FC<{}> = props =>
|
||||
setIsReady(false);
|
||||
setShowLogin(true);
|
||||
setIsEnteringHotel(false);
|
||||
}, [ clearStoredCredentials ]);
|
||||
|
||||
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 ]);
|
||||
}, [ clearStoredCredentials, showSessionExpired ]);
|
||||
|
||||
const applySsoTicket = useCallback((ssoTicket: string) =>
|
||||
{
|
||||
@@ -352,6 +390,7 @@ export const App: FC<{}> = props =>
|
||||
warmupPromiseRef.current = (async () =>
|
||||
{
|
||||
await GetConfiguration().init();
|
||||
bumpProgress(25, taskLabel('loading.task.warmup', 'Caricamento contenuti...'));
|
||||
|
||||
GetTicker().maxFPS = GetConfiguration().getValue<number>('system.fps.max', 24);
|
||||
NitroLogger.LOG_DEBUG = GetConfiguration().getValue<boolean>('system.log.debug', true);
|
||||
@@ -388,18 +427,29 @@ export const App: FC<{}> = props =>
|
||||
loginImageUrls.forEach(preloadImage);
|
||||
gamedataUrls.forEach(url => preloadUrl(url));
|
||||
|
||||
await Promise.all(
|
||||
[
|
||||
GetAssetManager().downloadAssets(assetUrls),
|
||||
GetLocalizationManager().init(),
|
||||
GetAvatarRenderManager().init(),
|
||||
GetSoundManager().init()
|
||||
]
|
||||
);
|
||||
// Wire each warmup task to a progress bump so the bar reflects
|
||||
// real subsystem-init completion, not a fake timer. Range 25→70.
|
||||
// Each task carries a friendly label so the user sees what is
|
||||
// currently being prepared instead of raw file names.
|
||||
const warmupTasks: { promise: Promise<any>; label: string }[] = [
|
||||
{ promise: GetAssetManager().downloadAssets(assetUrls), label: taskLabel('loading.task.assets', 'Sto caricando gli asset di gioco') },
|
||||
{ promise: GetLocalizationManager().init(), label: taskLabel('loading.task.localization', 'Sto caricando le traduzioni') },
|
||||
{ promise: GetAvatarRenderManager().init(), label: taskLabel('loading.task.avatar', 'Sto caricando il guardaroba') },
|
||||
{ promise: GetSoundManager().init(), label: taskLabel('loading.task.sounds', 'Sto caricando i suoni') }
|
||||
];
|
||||
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 ]);
|
||||
}, [ startRenderer, bumpProgress, taskLabel ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
@@ -434,12 +484,18 @@ export const App: FC<{}> = props =>
|
||||
urlSso: new URLSearchParams(window.location.search).get('sso')
|
||||
});
|
||||
|
||||
const bootLabel = taskLabel('loading.task.boot', 'Avvio in corso...');
|
||||
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);
|
||||
bumpProgress(10, taskLabel('loading.task.session', 'Verifica sessione'));
|
||||
|
||||
if(!ssoTicket || ssoTicket === '')
|
||||
{
|
||||
@@ -506,17 +562,23 @@ export const App: FC<{}> = props =>
|
||||
}
|
||||
|
||||
const renderer = await startRenderer(width, height);
|
||||
bumpProgress(20, taskLabel('loading.task.renderer', 'Inizializzazione renderer'));
|
||||
|
||||
await startWarmup(width, height);
|
||||
bumpProgress(70, taskLabel('loading.task.startsession', 'Avvio sessione'));
|
||||
|
||||
if(!gameInitPromiseRef.current)
|
||||
{
|
||||
gameInitPromiseRef.current = (async () =>
|
||||
{
|
||||
await GetSessionDataManager().init();
|
||||
bumpProgress(78, taskLabel('loading.task.userdata', 'Caricamento dati utente'));
|
||||
await GetRoomSessionManager().init();
|
||||
bumpProgress(85, taskLabel('loading.task.rooms', 'Caricamento stanze'));
|
||||
await GetRoomEngine().init();
|
||||
bumpProgress(92, taskLabel('loading.task.engine', 'Caricamento engine grafico'));
|
||||
await GetCommunication().init();
|
||||
bumpProgress(98, taskLabel('loading.task.connect', 'Connessione al server'));
|
||||
})();
|
||||
}
|
||||
|
||||
@@ -545,6 +607,7 @@ export const App: FC<{}> = props =>
|
||||
GetTicker().add(ticker => GetTexturePool().run());
|
||||
}
|
||||
|
||||
bumpProgress(100, taskLabel('loading.task.ready', 'Pronto!'));
|
||||
setIsReady(true);
|
||||
setShowLogin(false);
|
||||
setIsEnteringHotel(false);
|
||||
@@ -581,12 +644,12 @@ export const App: FC<{}> = props =>
|
||||
if(heartbeatIntervalRef.current !== null) window.clearInterval(heartbeatIntervalRef.current);
|
||||
if(rememberRotateIntervalRef.current !== null) window.clearInterval(rememberRotateIntervalRef.current);
|
||||
};
|
||||
}, [ prepareTrigger, startWarmup, startRenderer, tryRememberLogin, applySsoTicket, rotateRememberLogin ]);
|
||||
}, [ 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 } /> }
|
||||
<LoadingView isError={ errorMessage.length > 0 } message={ errorMessage } homeUrl={ homeUrl } progress={ loadingProgress } currentTask={ loadingTask } /> }
|
||||
{ !isReady && showLogin && <LoginView onAuthenticated={ handleAuthenticated } isEntering={ isEnteringHotel } /> }
|
||||
{ isReady && <MainView /> }
|
||||
{ /* Reconnect overlay must NOT render before we've actually entered
|
||||
|
||||
Reference in New Issue
Block a user