Skip to content

Commit 5111740

Browse files
committed
#75 wip: xboxビルドテスト v49
1 parent 8f9f07a commit 5111740

8 files changed

Lines changed: 421 additions & 30 deletions

File tree

src/index.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 が必須。

templates/xbox/CMakeLists.txt

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,18 @@ file(GLOB_RECURSE NEXT2D_HOST_SOURCES
8989
"${CMAKE_CURRENT_SOURCE_DIR}/src/*.h"
9090
)
9191

92+
# 埋め込みアセット: builder の deployXbox が assets.pak と assets.rc を生成する。
93+
# 存在すれば RCDATA "N2DASSETS" として exe にリンクし、ゲーム JS/HTML を平文で
94+
# 隣に置かずに済む。無ければ従来どおり隣接 assets/app・js から読む。
95+
set(NEXT2D_ASSETS_RC "${CMAKE_CURRENT_SOURCE_DIR}/assets.rc")
96+
if(EXISTS "${NEXT2D_ASSETS_RC}")
97+
enable_language(RC)
98+
list(APPEND NEXT2D_HOST_SOURCES "${NEXT2D_ASSETS_RC}")
99+
message(STATUS "Next2D: embedding assets from ${NEXT2D_ASSETS_RC}")
100+
else()
101+
message(STATUS "Next2D: no assets.rc found - runtime reads adjacent files")
102+
endif()
103+
92104
add_executable(Next2DXboxHost WIN32 ${NEXT2D_HOST_SOURCES})
93105

94106
target_include_directories(Next2DXboxHost PRIVATE
@@ -178,16 +190,29 @@ target_compile_definitions(Next2DXboxHost PRIVATE
178190
# ランタイム資材のステージング
179191
# builder が assets/app に JS/HTML/アセットを配置する。
180192
# js/bootstrap.js はブラウザ相当グローバルを整える起動スクリプト。
193+
#
194+
# 埋め込みモード (assets.rc あり) では assets/ と js/ は exe 内 pak に入るため
195+
# exe 隣へは一切コピーしない (平文 JS/HTML を配布物に残さないのが目的)。
196+
# MicrosoftGame.config だけは常にステージングする。
181197
# -----------------------------------------------------------------------------
182-
add_custom_command(TARGET Next2DXboxHost POST_BUILD
183-
COMMAND ${CMAKE_COMMAND} -E copy_directory
184-
"${CMAKE_CURRENT_SOURCE_DIR}/assets"
185-
"$<TARGET_FILE_DIR:Next2DXboxHost>/assets"
186-
COMMAND ${CMAKE_COMMAND} -E copy_directory
187-
"${CMAKE_CURRENT_SOURCE_DIR}/js"
188-
"$<TARGET_FILE_DIR:Next2DXboxHost>/js"
189-
COMMAND ${CMAKE_COMMAND} -E copy_if_different
190-
"${CMAKE_CURRENT_SOURCE_DIR}/MicrosoftGame.config"
191-
"$<TARGET_FILE_DIR:Next2DXboxHost>/MicrosoftGame.config"
192-
COMMENT "Staging Next2D assets, bootstrap JS and MicrosoftGame.config"
193-
)
198+
if(EXISTS "${NEXT2D_ASSETS_RC}")
199+
add_custom_command(TARGET Next2DXboxHost POST_BUILD
200+
COMMAND ${CMAKE_COMMAND} -E copy_if_different
201+
"${CMAKE_CURRENT_SOURCE_DIR}/MicrosoftGame.config"
202+
"$<TARGET_FILE_DIR:Next2DXboxHost>/MicrosoftGame.config"
203+
COMMENT "Staging MicrosoftGame.config (assets/JS embedded in exe)"
204+
)
205+
else()
206+
add_custom_command(TARGET Next2DXboxHost POST_BUILD
207+
COMMAND ${CMAKE_COMMAND} -E copy_directory
208+
"${CMAKE_CURRENT_SOURCE_DIR}/assets"
209+
"$<TARGET_FILE_DIR:Next2DXboxHost>/assets"
210+
COMMAND ${CMAKE_COMMAND} -E copy_directory
211+
"${CMAKE_CURRENT_SOURCE_DIR}/js"
212+
"$<TARGET_FILE_DIR:Next2DXboxHost>/js"
213+
COMMAND ${CMAKE_COMMAND} -E copy_if_different
214+
"${CMAKE_CURRENT_SOURCE_DIR}/MicrosoftGame.config"
215+
"$<TARGET_FILE_DIR:Next2DXboxHost>/MicrosoftGame.config"
216+
COMMENT "Staging Next2D assets, bootstrap JS and MicrosoftGame.config"
217+
)
218+
endif()

templates/xbox/src/AssetLoader.cpp

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "AssetLoader.h"
2+
#include "EmbeddedAssets.h"
23

34
#include <filesystem>
45
#include <fstream>
@@ -14,9 +15,10 @@ AssetLoader::AssetLoader(std::string root)
1415
{
1516
}
1617

17-
std::string AssetLoader::Resolve(const std::string& url) const
18+
std::string AssetLoader::RelativeKey(const std::string& url) const
1819
{
19-
// 先頭の "/" や "./"、絶対 URL のオリジンを剥がして assets/app 基準に寄せる。
20+
// 先頭の "/" や "./"、絶対 URL のオリジンを剥がして assets/app 基準の相対パスにする。
21+
// 埋め込み pak のキー(フォワードスラッシュ相対)と一致させるためのもの。
2022
std::string path = url;
2123

2224
// スキーム付き (file://, http://localhost/...) はパス部だけ取り出す
@@ -39,7 +41,16 @@ std::string AssetLoader::Resolve(const std::string& url) const
3941
path = path.substr(2);
4042
}
4143

42-
return (fs::path(root_) / path).string();
44+
// バックスラッシュはフォワードスラッシュへ正規化 (pak キーと一致させる)。
45+
for (char& c : path) {
46+
if (c == '\\') c = '/';
47+
}
48+
return path;
49+
}
50+
51+
std::string AssetLoader::Resolve(const std::string& url) const
52+
{
53+
return (fs::path(root_) / RelativeKey(url)).string();
4354
}
4455

4556
// base64 デコード (data: URI 用)。Image.cpp の同等実装と同じ仕様。
@@ -120,6 +131,11 @@ std::optional<std::vector<uint8_t>> AssetLoader::ReadBinary(const std::string& u
120131
return PercentDecode(payload);
121132
}
122133

134+
// 埋め込みモード優先: exe 内 pak に同じ相対キーがあれば実ファイルより先に返す。
135+
if (const auto* embedded = GetEmbeddedAsset(RelativeKey(url))) {
136+
return *embedded;
137+
}
138+
123139
const std::string path = Resolve(url);
124140
std::ifstream ifs(path, std::ios::binary | std::ios::ate);
125141
if (!ifs) {
@@ -137,6 +153,10 @@ std::optional<std::vector<uint8_t>> AssetLoader::ReadBinary(const std::string& u
137153

138154
std::optional<std::string> AssetLoader::ReadText(const std::string& url) const
139155
{
156+
if (const auto* embedded = GetEmbeddedAsset(RelativeKey(url))) {
157+
return std::string(embedded->begin(), embedded->end());
158+
}
159+
140160
const std::string path = Resolve(url);
141161
std::ifstream ifs(path, std::ios::binary);
142162
if (!ifs) {
@@ -150,30 +170,37 @@ std::optional<std::string> AssetLoader::ReadText(const std::string& url) const
150170
std::optional<std::string> AssetLoader::ResolveEntryModule() const
151171
{
152172
// vite が出力する index.html から <script type="module" src="..."> を抽出する。
153-
const fs::path index = fs::path(root_) / "index.html";
154-
std::ifstream ifs(index, std::ios::binary);
155-
if (!ifs) {
156-
return std::nullopt;
173+
// 埋め込みモードでは pak から、無ければ実ファイルから読む。
174+
std::string html;
175+
if (const auto* embedded = GetEmbeddedAsset("index.html")) {
176+
html.assign(embedded->begin(), embedded->end());
177+
} else {
178+
const fs::path index = fs::path(root_) / "index.html";
179+
std::ifstream ifs(index, std::ios::binary);
180+
if (!ifs) {
181+
return std::nullopt;
182+
}
183+
std::ostringstream oss;
184+
oss << ifs.rdbuf();
185+
html = oss.str();
157186
}
158-
std::ostringstream oss;
159-
oss << ifs.rdbuf();
160-
const std::string html = oss.str();
161187

162188
// type="module" を含む <script ... src="..."> を優先して拾う。
189+
// 返す値は assets/app 基準の絶対パス。モジュールローダ側で埋め込み/実ファイルを解決する。
163190
std::regex module_re(
164191
R"(<script[^>]*type=["']module["'][^>]*src=["']([^"']+)["'])",
165192
std::regex::icase);
166193
std::smatch m;
167194
if (std::regex_search(html, m, module_re)) {
168-
return (fs::path(root_) / Resolve(m[1].str()).substr(root_.size() + 1)).string();
195+
return Resolve(m[1].str());
169196
}
170197

171198
// 逆順 (src が先) も試す
172199
std::regex module_re2(
173200
R"(<script[^>]*src=["']([^"']+)["'][^>]*type=["']module["'])",
174201
std::regex::icase);
175202
if (std::regex_search(html, m, module_re2)) {
176-
return (fs::path(root_) / Resolve(m[1].str()).substr(root_.size() + 1)).string();
203+
return Resolve(m[1].str());
177204
}
178205

179206
return std::nullopt;

templates/xbox/src/AssetLoader.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ class AssetLoader {
2020
// 相対 URL/パスを assets/app 基準の絶対パスへ解決する。
2121
std::string Resolve(const std::string& url) const;
2222

23+
// URL を assets/app 基準の相対キー(フォワードスラッシュ)へ正規化する。
24+
// 埋め込み pak のキーと一致させるために使う。
25+
std::string RelativeKey(const std::string& url) const;
26+
2327
// バイナリ読み込み。存在しなければ nullopt。
2428
std::optional<std::vector<uint8_t>> ReadBinary(const std::string& url) const;
2529

0 commit comments

Comments
 (0)