import { useEffect, useState } from "react"; import { buildWorldshaperStudioUrl, openWorldshaperStudioWindow, } from "./worldshaperStudio/windowing"; import { CHANGELOG_SECTIONS, CHANGELOG_SPLASH_VERSION } from "./worldshaperStudio/changelogData"; type WorldDefaultPayload = { worldId?: string; world?: { id?: string; }; }; type LaunchState = "resolving" | "opening" | "opened" | "blocked" | "error"; const DEFAULT_EDITOR_WORLD_ID_FALLBACK = "overworld"; let didAttemptInitialLaunch = false; let rememberedLaunchState: LaunchState | "" = ""; let rememberedStatus = ""; let rememberedError = ""; let rememberedWorldId = ""; async function resolveDefaultWorldId(): Promise { const response = await fetch("/api/world-default"); if (!response.ok) { throw new Error(`Failed to load default world (${response.status}).`); } const payload = await response.json() as WorldDefaultPayload; const resolvedWorldId = String(payload.worldId || payload.world?.id || "").trim(); return resolvedWorldId || DEFAULT_EDITOR_WORLD_ID_FALLBACK; } function openStudioPopup(worldId: string): boolean { const popup = openWorldshaperStudioWindow(worldId, window, { worldId }); return Boolean(popup); } function openStudioInCurrentTab(worldId: string): void { window.location.assign(buildWorldshaperStudioUrl(worldId, window, { worldId })); } function openRepo(): void { window.location.assign("https://repo.andraxion.net/"); } function WorldshaperLauncher() { const [launchState, setLaunchState] = useState("resolving"); const [status, setStatus] = useState("Preparing Worldshaper Studio..."); const [error, setError] = useState(""); const [worldId, setWorldId] = useState(""); useEffect(() => { let cancelled = false; async function attemptInitialLaunch() { if (didAttemptInitialLaunch) { setWorldId(rememberedWorldId); setLaunchState(rememberedLaunchState || "blocked"); setStatus(rememberedStatus || "Open Worldshaper Studio."); setError(rememberedError); return; } didAttemptInitialLaunch = true; setLaunchState("resolving"); setError(""); setStatus("Preparing Worldshaper Studio..."); try { const resolvedWorldId = await resolveDefaultWorldId().catch(() => DEFAULT_EDITOR_WORLD_ID_FALLBACK); if (cancelled) { return; } setWorldId(resolvedWorldId); rememberedWorldId = resolvedWorldId; setLaunchState("opening"); setStatus(`Opening Worldshaper Studio for ${resolvedWorldId}...`); if (openStudioPopup(resolvedWorldId)) { setLaunchState("opened"); setStatus("Worldshaper Studio opened in a separate window."); rememberedLaunchState = "opened"; rememberedStatus = "Worldshaper Studio opened in a separate window."; rememberedError = ""; return; } setLaunchState("blocked"); setStatus("Your browser blocked the studio window."); rememberedLaunchState = "blocked"; rememberedStatus = "Your browser blocked the studio window."; rememberedError = ""; } catch (nextError: unknown) { if (cancelled) { return; } const nextErrorText = String(nextError || "Failed to prepare Worldshaper Studio."); setLaunchState("error"); setError(nextErrorText); setStatus("Worldshaper Studio unavailable."); rememberedLaunchState = "error"; rememberedStatus = "Worldshaper Studio unavailable."; rememberedError = nextErrorText; } } void attemptInitialLaunch(); return () => { cancelled = true; }; }, []); useEffect(() => { if (launchState !== "blocked" || !worldId) { return; } function retryLaunch() { if (!worldId) { return; } setLaunchState("opening"); setStatus(`Opening Worldshaper Studio for ${worldId}...`); if (openStudioPopup(worldId)) { setLaunchState("opened"); setStatus("Worldshaper Studio opened in a separate window."); rememberedLaunchState = "opened"; rememberedStatus = "Worldshaper Studio opened in a separate window."; rememberedError = ""; return; } setLaunchState("blocked"); setStatus("Your browser blocked the studio window."); rememberedLaunchState = "blocked"; rememberedStatus = "Your browser blocked the studio window."; rememberedError = ""; } function handlePointerDown() { retryLaunch(); } function handleKeyDown() { retryLaunch(); } window.addEventListener("pointerdown", handlePointerDown, { once: true }); window.addEventListener("keydown", handleKeyDown, { once: true }); return () => { window.removeEventListener("pointerdown", handlePointerDown); window.removeEventListener("keydown", handleKeyDown); }; }, [launchState, worldId]); async function handleRetry(): Promise { setError(""); setLaunchState("resolving"); setStatus("Preparing Worldshaper Studio..."); try { const resolvedWorldId = worldId || await resolveDefaultWorldId().catch(() => DEFAULT_EDITOR_WORLD_ID_FALLBACK); setWorldId(resolvedWorldId); rememberedWorldId = resolvedWorldId; setLaunchState("opening"); setStatus(`Opening Worldshaper Studio for ${resolvedWorldId}...`); if (openStudioPopup(resolvedWorldId)) { setLaunchState("opened"); setStatus("Worldshaper Studio opened in a separate window."); rememberedLaunchState = "opened"; rememberedStatus = "Worldshaper Studio opened in a separate window."; rememberedError = ""; return; } setLaunchState("blocked"); setStatus("Your browser blocked the studio window."); rememberedLaunchState = "blocked"; rememberedStatus = "Your browser blocked the studio window."; rememberedError = ""; } catch (nextError: unknown) { const nextErrorText = String(nextError || "Failed to prepare Worldshaper Studio."); setLaunchState("error"); setError(nextErrorText); setStatus("Worldshaper Studio unavailable."); rememberedLaunchState = "error"; rememberedStatus = "Worldshaper Studio unavailable."; rememberedError = nextErrorText; } } function handleOpenHere(): void { const nextWorldId = worldId || DEFAULT_EDITOR_WORLD_ID_FALLBACK; openStudioInCurrentTab(nextWorldId); } const isBusy = launchState === "resolving" || launchState === "opening"; const showFallbackActions = launchState === "blocked" || launchState === "opened" || launchState === "error"; return (

New RPG

Worldshaper Studio

{status}

{launchState === "blocked" ? (

Click anywhere or use the button below if your browser asks for permission to open the studio window.

) : null} {launchState === "opened" ? (

The editor is running in its own window. You can leave this tab open as a quick relaunch point, or close it.

) : null} {error ?

{error}

: null} {showFallbackActions ? (
) : null}

What's New

{CHANGELOG_SPLASH_VERSION}

{CHANGELOG_SECTIONS.map((section) => (

{section.title}

    {section.items.map((item, index) => { const key = `${section.title}-${index}`; if (typeof item === "string") { return
  • {item}
  • ; } return (
  • {item.text}
    {item.note ?
    {item.note}
    : null}
  • ); })}
))}
); } export default WorldshaperLauncher;