@@ -97,8 +97,8 @@ const refs = {
9797} ;
9898
9999const manifest = createToolHostManifest ( ) ;
100- const toolIds = manifest . tools . map ( ( tool ) => tool . id ) ;
101- const hasAvailableTools = toolIds . length > 0 ;
100+ const allToolIds = manifest . tools . map ( ( tool ) => tool . id ) ;
101+ let toolIds = [ ... allToolIds ] ;
102102let currentGameFrame = null ;
103103let currentGameHostContextId = "" ;
104104const TOOL_LAUNCH_PARAM_PREFIXES = Object . freeze ( {
@@ -112,6 +112,27 @@ function normalizeTextParam(value) {
112112 return typeof value === "string" ? value . trim ( ) : "" ;
113113}
114114
115+ function normalizeToken ( value ) {
116+ return normalizeTextParam ( value ) . toLowerCase ( ) ;
117+ }
118+
119+ function normalizeToolHintList ( value ) {
120+ if ( ! Array . isArray ( value ) ) {
121+ return [ ] ;
122+ }
123+ const seen = new Set ( ) ;
124+ const output = [ ] ;
125+ value . forEach ( ( entry ) => {
126+ const token = normalizeToken ( entry ) ;
127+ if ( ! token || seen . has ( token ) ) {
128+ return ;
129+ }
130+ seen . add ( token ) ;
131+ output . push ( token ) ;
132+ } ) ;
133+ return output ;
134+ }
135+
115136function normalizeLocalHrefParam ( value , allowedPrefixes = [ ] ) {
116137 const normalized = normalizeTextParam ( value ) . replace ( / \\ / g, "/" ) ;
117138 if ( ! normalized || ! normalized . startsWith ( "/" ) || normalized . includes ( ".." ) ) {
@@ -216,7 +237,7 @@ function updateStandaloneHref(toolId) {
216237 return ;
217238 }
218239 const entry = getToolHostEntryById ( manifest , toolId ) ;
219- const enabled = ! ! entry ;
240+ const enabled = ! ! entry && toolIds . includes ( toolId ) ;
220241 refs . standaloneLink . href = enabled ? entry . launchPath : "#" ;
221242 refs . standaloneLink . setAttribute ( "aria-disabled" , enabled ? "false" : "true" ) ;
222243 refs . standaloneLink . tabIndex = enabled ? 0 : - 1 ;
@@ -243,16 +264,16 @@ function writeQueryToolId(toolId, replace = false) {
243264function readInitialToolId ( ) {
244265 const url = new URL ( window . location . href ) ;
245266 const fromQuery = url . searchParams . get ( "tool" ) ;
246- if ( fromQuery && getToolHostEntryById ( manifest , fromQuery ) ) {
267+ if ( fromQuery && getToolHostEntryById ( manifest , fromQuery ) && toolIds . includes ( fromQuery ) ) {
247268 return fromQuery ;
248269 }
249- return manifest . tools [ 0 ] ?. id || "" ;
270+ return toolIds [ 0 ] || "" ;
250271}
251272
252273function readRequestedToolIdFromQuery ( ) {
253274 const url = new URL ( window . location . href ) ;
254275 const requested = ( url . searchParams . get ( "tool" ) || "" ) . trim ( ) ;
255- if ( ! requested || ! getToolHostEntryById ( manifest , requested ) ) {
276+ if ( ! requested || ! getToolHostEntryById ( manifest , requested ) || ! toolIds . includes ( requested ) ) {
256277 return "" ;
257278 }
258279 return requested ;
@@ -307,6 +328,7 @@ async function readGameEntryById(gameId) {
307328 const tags = Array . isArray ( entry . tags )
308329 ? entry . tags . map ( ( value ) => String ( value || "" ) . trim ( ) ) . filter ( Boolean )
309330 : [ ] ;
331+ const toolHints = normalizeToolHintList ( entry . toolHints ) ;
310332 return {
311333 id : String ( entry . id || "" ) . trim ( ) ,
312334 title : String ( entry . title || entry . id || "Game" ) . trim ( ) ,
@@ -317,6 +339,7 @@ async function readGameEntryById(gameId) {
317339 description : String ( entry . description || "" ) . trim ( ) ,
318340 classValues,
319341 tags,
342+ toolHints,
320343 sampleTrack : entry . sampleTrack === true ,
321344 debugShowcase : entry . debugShowcase === true ,
322345 requiresService : entry . requiresService === true
@@ -410,17 +433,17 @@ async function mountGameFrame(gameEntry) {
410433
411434function syncControlState ( ) {
412435 const selectedToolId = readSelectedToolId ( ) ;
413- const hasSelection = ! ! selectedToolId && ! ! getToolHostEntryById ( manifest , selectedToolId ) ;
436+ const hasSelection = ! ! selectedToolId && toolIds . includes ( selectedToolId ) && ! ! getToolHostEntryById ( manifest , selectedToolId ) ;
414437 const hasMount = ! ! runtime . getCurrentMount ( ) ;
415438
416439 if ( refs . mountButton instanceof HTMLButtonElement ) {
417440 refs . mountButton . disabled = ! hasSelection ;
418441 }
419442 if ( refs . prevButton instanceof HTMLButtonElement ) {
420- refs . prevButton . disabled = ! hasAvailableTools ;
443+ refs . prevButton . disabled = toolIds . length === 0 ;
421444 }
422445 if ( refs . nextButton instanceof HTMLButtonElement ) {
423- refs . nextButton . disabled = ! hasAvailableTools ;
446+ refs . nextButton . disabled = toolIds . length === 0 ;
424447 }
425448 if ( refs . unmountButton instanceof HTMLButtonElement ) {
426449 refs . unmountButton . disabled = ! hasMount ;
@@ -432,13 +455,29 @@ function populateToolSelect(initialToolId) {
432455 return ;
433456 }
434457
435- refs . toolSelect . innerHTML = manifest . tools
458+ refs . toolSelect . innerHTML = toolIds
459+ . map ( ( toolId ) => getToolHostEntryById ( manifest , toolId ) )
460+ . filter ( Boolean )
436461 . map ( ( tool ) => `<option value="${ tool . id } ">${ tool . displayName } </option>` )
437462 . join ( "" ) ;
438- refs . toolSelect . value = getToolHostEntryById ( manifest , initialToolId ) ? initialToolId : ( manifest . tools [ 0 ] ?. id || "" ) ;
463+ refs . toolSelect . value = toolIds . includes ( initialToolId ) ? initialToolId : ( toolIds [ 0 ] || "" ) ;
439464 updateSwitchMeta ( ) ;
440465}
441466
467+ function applyToolHintsFilterForGame ( gameEntry , preferredToolId = "" ) {
468+ if ( ! gameEntry ) {
469+ toolIds = [ ...allToolIds ] ;
470+ } else {
471+ const allowed = normalizeToolHintList ( gameEntry . toolHints )
472+ . filter ( ( toolId ) => ! ! getToolHostEntryById ( manifest , toolId ) ) ;
473+ toolIds = [ ...allowed ] ;
474+ }
475+ const initialToolId = toolIds . includes ( preferredToolId ) ? preferredToolId : ( toolIds [ 0 ] || "" ) ;
476+ populateToolSelect ( initialToolId ) ;
477+ updateStandaloneHref ( initialToolId ) ;
478+ syncControlState ( ) ;
479+ }
480+
442481const runtime = createToolHostRuntime ( {
443482 manifest,
444483 mountContainer : refs . mountContainer ,
@@ -535,17 +574,28 @@ function bindEvents() {
535574
536575 window . addEventListener ( "popstate" , ( ) => {
537576 const gameId = readInitialGameId ( ) ;
538- const requestedToolId = readRequestedToolIdFromQuery ( ) ;
539- if ( gameId && ! requestedToolId && shouldMountGameFrameFromQuery ( ) ) {
577+ if ( gameId ) {
540578 void readGameEntryById ( gameId ) . then ( ( gameEntry ) => {
541579 if ( ! gameEntry ) {
542580 writeStatus ( `Game "${ gameId } " is not available for Workspace Manager launch.` ) ;
581+ applyToolHintsFilterForGame ( null ) ;
582+ return ;
583+ }
584+ const requestedToolId = ( ( ) => {
585+ const url = new URL ( window . location . href ) ;
586+ return ( url . searchParams . get ( "tool" ) || "" ) . trim ( ) ;
587+ } ) ( ) ;
588+ applyToolHintsFilterForGame ( gameEntry , requestedToolId ) ;
589+ if ( ! requestedToolId && shouldMountGameFrameFromQuery ( ) ) {
590+ void mountGameFrame ( gameEntry ) ;
543591 return ;
544592 }
545- void mountGameFrame ( gameEntry ) ;
593+ mountSelectedTool ( "popstate" ) ;
546594 } ) ;
547595 return ;
548596 }
597+ applyToolHintsFilterForGame ( null ) ;
598+ const requestedToolId = readRequestedToolIdFromQuery ( ) ;
549599 const toolId = requestedToolId || readInitialToolId ( ) ;
550600 if ( refs . toolSelect instanceof HTMLSelectElement ) {
551601 refs . toolSelect . value = toolId ;
@@ -562,29 +612,33 @@ function bindEvents() {
562612}
563613
564614async function init ( ) {
565- const requestedToolId = readRequestedToolIdFromQuery ( ) ;
566- const initialToolId = requestedToolId || readInitialToolId ( ) ;
567- populateToolSelect ( initialToolId ) ;
568- updateStandaloneHref ( initialToolId ) ;
569- syncControlState ( ) ;
570- bindEvents ( ) ;
571-
572615 const initialGameId = readInitialGameId ( ) ;
573- if ( initialGameId && ! requestedToolId && shouldMountGameFrameFromQuery ( ) ) {
574- const gameEntry = await readGameEntryById ( initialGameId ) ;
575- if ( gameEntry ) {
576- await mountGameFrame ( gameEntry ) ;
577- return ;
616+ let initialGameEntry = null ;
617+ if ( initialGameId ) {
618+ initialGameEntry = await readGameEntryById ( initialGameId ) ;
619+ if ( ! initialGameEntry ) {
620+ writeStatus ( `Game " ${ initialGameId } " is not available for Workspace Manager launch.` ) ;
578621 }
579- writeStatus ( `Game "${ initialGameId } " is not available for Workspace Manager launch.` ) ;
580622 }
581623
582- if ( ! hasAvailableTools ) {
583- writeStatus ( "No active tools are currently available for Workspace Manager." ) ;
624+ const rawRequestedToolId = ( ( ) => {
625+ const url = new URL ( window . location . href ) ;
626+ return ( url . searchParams . get ( "tool" ) || "" ) . trim ( ) ;
627+ } ) ( ) ;
628+ applyToolHintsFilterForGame ( initialGameEntry , rawRequestedToolId ) ;
629+ bindEvents ( ) ;
630+
631+ const requestedToolId = readRequestedToolIdFromQuery ( ) ;
632+ if ( initialGameEntry && ! requestedToolId && shouldMountGameFrameFromQuery ( ) ) {
633+ await mountGameFrame ( initialGameEntry ) ;
634+ return ;
584635 }
585- if ( hasAvailableTools ) {
586- mountSelectedTool ( "init" ) ;
636+
637+ if ( toolIds . length === 0 ) {
638+ writeStatus ( "No active tools are currently available for Workspace Manager." ) ;
639+ return ;
587640 }
641+ mountSelectedTool ( "init" ) ;
588642}
589643
590644void init ( ) ;
0 commit comments