@@ -785,6 +785,124 @@ const copyXboxResources = (): Promise<void> =>
785785 } ) ;
786786} ;
787787
788+ /**
789+ * @description ディレクトリを再帰的に走査し、各ファイルの
790+ * [posix 相対キー, 絶対パス] を集める。
791+ * Walk a directory recursively, collecting [posixRelKey, absPath] pairs.
792+ *
793+ * @param {string } rootDir 走査ルート
794+ * @param {string } [prefix] キーへ付与する接頭辞 (例 "" / "assets/")
795+ * @return {Array<[string, string]> }
796+ * @method
797+ * @public
798+ */
799+ const walkFiles = ( rootDir : string , prefix : string = "") : [ string , string ] [ ] =>
800+ {
801+ const out : [ string , string ] [ ] = [ ] ;
802+ if ( ! fs . existsSync ( rootDir ) ) {
803+ return out ;
804+ }
805+ for ( const entry of fs . readdirSync ( rootDir , { "withFileTypes" : true } ) ) {
806+ const abs : string = path . join ( rootDir , entry . name ) ;
807+ const key : string = prefix ? `${ prefix } ${ entry . name } ` : entry . name ;
808+ if ( entry . isDirectory ( ) ) {
809+ out . push ( ...walkFiles ( abs , `${ key } /` ) ) ;
810+ } else if ( entry . isFile ( ) ) {
811+ out . push ( [ key , abs ] ) ;
812+ }
813+ }
814+ return out ;
815+ } ;
816+
817+ /**
818+ * @description Xbox ホストの assets/app とホストスクリプト(js/bootstrap.js 等)を
819+ * 単一の pak バイナリへまとめ、`assets.pak` と RCDATA 参照用の
820+ * `assets.rc` を xbox/ 直下へ生成する。CMake が assets.rc を検出すると
821+ * これらを exe 内リソースへ埋め込み、平文 JS/HTML を配布物に残さない。
822+ * 環境変数 `NEXT2D_XBOX_NO_EMBED` が設定されている場合は埋め込みを無効化し、
823+ * 既存の生成物を削除して隣接ファイル読み込みへ戻す。
824+ *
825+ * pak フォーマット (リトルエンディアン uint32):
826+ * magic "N2DA" / version(=1) / count / [keyLen,key,dataLen,data]...
827+ * キーは assets/app 基準の posix 相対パス、または "js/bootstrap.js"。
828+ * host 側 EmbeddedAssets.cpp の ParseEmbeddedPak と対になる。
829+ *
830+ * @return {Promise }
831+ * @method
832+ * @public
833+ */
834+ const embedXboxAssets = ( ) : Promise < void > =>
835+ {
836+ return new Promise < void > ((resolve, reject): void =>
837+ {
838+ const xboxDir : string = `${ process . cwd ( ) } /${ XBOX_DIR_NAME } ` ;
839+ const pakPath : string = `${ xboxDir } /assets.pak` ;
840+ const rcPath : string = `${ xboxDir } /assets.rc` ;
841+
842+ // 明示的に無効化された場合は生成物を消してフォールバックへ戻す。
843+ if ( process . env . NEXT2D_XBOX_NO_EMBED ) {
844+ try {
845+ fs . rmSync ( pakPath , { "force" : true } ) ;
846+ fs . rmSync ( rcPath , { "force" : true } ) ;
847+ } catch { /* ignore */ }
848+ console . log ( pc . yellow ( "Xbox asset embedding disabled (NEXT2D_XBOX_NO_EMBED)." ) ) ;
849+ return resolve ( ) ;
850+ }
851+
852+ try {
853+ // 収集: assets/app 一式 (キーは app 基準) + host スクリプト (js/ 基準)。
854+ const entries : [ string , string ] [ ] = [ ] ;
855+ entries . push ( ...walkFiles ( `${ xboxDir } /assets/app` , "" ) ) ;
856+ const bootstrap : string = `${ xboxDir } /js/bootstrap.js` ;
857+ if ( fs . existsSync ( bootstrap ) ) {
858+ entries . push ( [ "js/bootstrap.js" , bootstrap ] ) ;
859+ }
860+ const selftest : string = `${ xboxDir } /js/selftest.js` ;
861+ if ( fs . existsSync ( selftest ) ) {
862+ entries . push ( [ "js/selftest.js" , selftest ] ) ;
863+ }
864+
865+ if ( entries . length === 0 ) {
866+ return reject ( "No Xbox assets found to embed (assets/app is empty)." ) ;
867+ }
868+
869+ // pak バイナリを組み立てる。
870+ const chunks : Buffer [ ] = [ ] ;
871+ const header : Buffer = Buffer . alloc ( 12 ) ;
872+ header . write ( "N2DA" , 0 , "ascii" ) ;
873+ header . writeUInt32LE ( 1 , 4 ) ; // version
874+ header . writeUInt32LE ( entries . length , 8 ) ; // count
875+ chunks . push ( header ) ;
876+
877+ for ( const [ key , abs ] of entries ) {
878+ const keyBuf : Buffer = Buffer . from ( key , "utf8" ) ;
879+ const dataBuf : Buffer = fs . readFileSync ( abs ) ;
880+ const meta: Buffer = Buffer . alloc ( 4 ) ;
881+ meta. writeUInt32LE ( keyBuf . length , 0 ) ;
882+ chunks . push ( meta , keyBuf ) ;
883+ const meta2 : Buffer = Buffer . alloc ( 4 ) ;
884+ meta2 . writeUInt32LE ( dataBuf . length , 0 ) ;
885+ chunks . push ( meta2 , dataBuf ) ;
886+ }
887+
888+ fs . writeFileSync ( pakPath , Buffer . concat ( chunks ) ) ;
889+
890+ // rc.exe が assets.pak を RCDATA "N2DASSETS" として取り込む。
891+ // パスは rc ファイル位置 (xbox/) からの相対。
892+ fs . writeFileSync ( rcPath , 'N2DASSETS RCDATA "assets.pak"\n' , "utf8" ) ;
893+
894+ const total : number = chunks . reduce ( ( n , b ) : number => n + b . length , 0 ) ;
895+ console . log ( pc . green (
896+ `Embedded ${ entries . length } Xbox asset file(s) into assets.pak `
897+ + `(${ ( total / 1024 / 1024 ) . toFixed ( 2 ) } MB).`
898+ ) ) ;
899+ resolve ( ) ;
900+ } catch ( error ) {
901+ reject ( `Failed to embed Xbox assets. ${ error } ` ) ;
902+ }
903+ } ) ;
904+ } ;
905+
788906/**
789907 * @description Xbox ビルドに使う V8 のパスを解決する。優先順:
790908 * 1. `--v8-root` 引数
@@ -866,6 +984,8 @@ const buildXbox = async (): Promise<void> =>
866984 await injectGameConfig ( ) ;
867985 // ビルド済みWeb資材を配置
868986 await copyXboxResources ( ) ;
987+ // assets/app + host スクリプトを exe 埋め込み用 pak (+ rc) へまとめる
988+ await embedXboxAssets ( ) ;
869989
870990 /**
871991 * GDK ビルドは Windows + Visual Studio + Microsoft GDK が必須。
0 commit comments