create-pixi-vn 2.1.4 → 2.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/package.json +1 -1
  2. package/template-lib/package.json +3 -0
  3. package/template-nqtr-react-vite-muijoy/package.json +2 -2
  4. package/template-nqtr-react-vite-muijoy/src/assets/index.ts +2 -11
  5. package/template-nqtr-react-vite-muijoy/src/assets/manifest.gen.json +2 -7
  6. package/template-nqtr-react-vite-muijoy/src/components/menus/settings/index.tsx +15 -6
  7. package/template-nqtr-react-vite-muijoy/src/components/menus/settings/menus/diagnostics.tsx +266 -0
  8. package/template-nqtr-react-vite-muijoy/src/components/menus/settings/quick-menus.tsx +25 -1
  9. package/template-nqtr-react-vite-muijoy/src/lib/hooks/device-diagnostics-hooks.ts +68 -0
  10. package/template-nqtr-react-vite-muijoy/src/lib/hooks/hotkeys-hooks.ts +16 -0
  11. package/template-nqtr-react-vite-muijoy/src/lib/utils/device-diagnostics-utility.ts +382 -0
  12. package/template-nqtr-react-vite-muijoy/src/locales/en.json +50 -0
  13. package/template-nqtr-react-vite-muijoy/src/pixi-vn.keys.gen.ts +4 -7
  14. package/template-nqtr-react-vite-muijoy-ink/package.json +3 -3
  15. package/template-nqtr-react-vite-muijoy-ink/src/assets/index.ts +2 -11
  16. package/template-nqtr-react-vite-muijoy-ink/src/assets/manifest.gen.json +2 -7
  17. package/template-nqtr-react-vite-muijoy-ink/src/components/menus/settings/index.tsx +15 -6
  18. package/template-nqtr-react-vite-muijoy-ink/src/components/menus/settings/menus/diagnostics.tsx +266 -0
  19. package/template-nqtr-react-vite-muijoy-ink/src/components/menus/settings/quick-menus.tsx +25 -1
  20. package/template-nqtr-react-vite-muijoy-ink/src/lib/hooks/device-diagnostics-hooks.ts +68 -0
  21. package/template-nqtr-react-vite-muijoy-ink/src/lib/hooks/hotkeys-hooks.ts +16 -0
  22. package/template-nqtr-react-vite-muijoy-ink/src/lib/utils/device-diagnostics-utility.ts +382 -0
  23. package/template-nqtr-react-vite-muijoy-ink/src/locales/en.json +50 -0
  24. package/template-nqtr-react-vite-muijoy-ink/src/pixi-vn.keys.gen.ts +4 -7
  25. package/template-nqtr-react-vite-muijoy-ink-tauri/.vscode/launch.json +1 -1
  26. package/template-nqtr-react-vite-muijoy-ink-tauri/.vscode/tasks.json +1 -35
  27. package/template-nqtr-react-vite-muijoy-ink-tauri/_github/workflows/mobile.yml +76 -13
  28. package/template-nqtr-react-vite-muijoy-ink-tauri/package.json +3 -3
  29. package/template-nqtr-react-vite-muijoy-ink-tauri/src/assets/index.ts +2 -11
  30. package/template-nqtr-react-vite-muijoy-ink-tauri/src/assets/manifest.gen.json +2 -7
  31. package/template-nqtr-react-vite-muijoy-ink-tauri/src/components/menus/settings/index.tsx +15 -6
  32. package/template-nqtr-react-vite-muijoy-ink-tauri/src/components/menus/settings/menus/diagnostics.tsx +332 -0
  33. package/template-nqtr-react-vite-muijoy-ink-tauri/src/components/menus/settings/quick-menus.tsx +32 -1
  34. package/template-nqtr-react-vite-muijoy-ink-tauri/src/content/ink/hashtag-commands.ts +17 -0
  35. package/template-nqtr-react-vite-muijoy-ink-tauri/src/lib/hooks/device-diagnostics-hooks.ts +96 -0
  36. package/template-nqtr-react-vite-muijoy-ink-tauri/src/lib/hooks/hotkeys-hooks.ts +16 -0
  37. package/template-nqtr-react-vite-muijoy-ink-tauri/src/lib/system-info.ts +54 -0
  38. package/template-nqtr-react-vite-muijoy-ink-tauri/src/lib/utils/device-diagnostics-utility.ts +511 -0
  39. package/template-nqtr-react-vite-muijoy-ink-tauri/src/locales/en.json +66 -0
  40. package/template-nqtr-react-vite-muijoy-ink-tauri/src/pixi-vn.keys.gen.ts +4 -7
  41. package/template-nqtr-react-vite-muijoy-ink-tauri/src-tauri/Cargo.toml +1 -0
  42. package/template-nqtr-react-vite-muijoy-ink-tauri/src-tauri/src/lib.rs +34 -22
  43. package/template-nqtr-react-vite-muijoy-ink-tauri/src-tauri/src/system_info.rs +33 -0
  44. package/template-nqtr-react-vite-muijoy-tauri/.vscode/launch.json +1 -1
  45. package/template-nqtr-react-vite-muijoy-tauri/.vscode/tasks.json +1 -35
  46. package/template-nqtr-react-vite-muijoy-tauri/_github/workflows/mobile.yml +76 -13
  47. package/template-nqtr-react-vite-muijoy-tauri/package.json +2 -2
  48. package/template-nqtr-react-vite-muijoy-tauri/src/assets/index.ts +2 -11
  49. package/template-nqtr-react-vite-muijoy-tauri/src/assets/manifest.gen.json +2 -7
  50. package/template-nqtr-react-vite-muijoy-tauri/src/components/menus/settings/index.tsx +15 -6
  51. package/template-nqtr-react-vite-muijoy-tauri/src/components/menus/settings/menus/diagnostics.tsx +332 -0
  52. package/template-nqtr-react-vite-muijoy-tauri/src/components/menus/settings/quick-menus.tsx +32 -1
  53. package/template-nqtr-react-vite-muijoy-tauri/src/lib/hooks/device-diagnostics-hooks.ts +96 -0
  54. package/template-nqtr-react-vite-muijoy-tauri/src/lib/hooks/hotkeys-hooks.ts +16 -0
  55. package/template-nqtr-react-vite-muijoy-tauri/src/lib/system-info.ts +54 -0
  56. package/template-nqtr-react-vite-muijoy-tauri/src/lib/utils/device-diagnostics-utility.ts +511 -0
  57. package/template-nqtr-react-vite-muijoy-tauri/src/locales/en.json +66 -0
  58. package/template-nqtr-react-vite-muijoy-tauri/src/pixi-vn.keys.gen.ts +4 -7
  59. package/template-nqtr-react-vite-muijoy-tauri/src-tauri/Cargo.toml +1 -0
  60. package/template-nqtr-react-vite-muijoy-tauri/src-tauri/src/lib.rs +32 -20
  61. package/template-nqtr-react-vite-muijoy-tauri/src-tauri/src/system_info.rs +33 -0
  62. package/template-react-vite-muijoy/package.json +1 -1
  63. package/template-react-vite-muijoy/src/assets/index.ts +55 -55
  64. package/template-react-vite-muijoy/src/assets/manifest.gen.json +2 -7
  65. package/template-react-vite-muijoy/src/components/menus/settings/index.tsx +15 -6
  66. package/template-react-vite-muijoy/src/components/menus/settings/menus/diagnostics.tsx +266 -0
  67. package/template-react-vite-muijoy/src/components/menus/settings/quick-menus.tsx +25 -1
  68. package/template-react-vite-muijoy/src/lib/hooks/device-diagnostics-hooks.ts +68 -0
  69. package/template-react-vite-muijoy/src/lib/hooks/hotkeys-hooks.ts +16 -0
  70. package/template-react-vite-muijoy/src/lib/utils/device-diagnostics-utility.ts +382 -0
  71. package/template-react-vite-muijoy/src/locales/en.json +50 -0
  72. package/template-react-vite-muijoy/src/pixi-vn.keys.gen.ts +2 -3
  73. package/template-react-vite-muijoy-ink/ink/second_part.ink +0 -1
  74. package/template-react-vite-muijoy-ink/ink/start.ink +4 -6
  75. package/template-react-vite-muijoy-ink/package.json +2 -2
  76. package/template-react-vite-muijoy-ink/src/assets/index.ts +55 -55
  77. package/template-react-vite-muijoy-ink/src/assets/manifest.gen.json +2 -7
  78. package/template-react-vite-muijoy-ink/src/components/menus/settings/index.tsx +15 -6
  79. package/template-react-vite-muijoy-ink/src/components/menus/settings/menus/diagnostics.tsx +266 -0
  80. package/template-react-vite-muijoy-ink/src/components/menus/settings/quick-menus.tsx +25 -1
  81. package/template-react-vite-muijoy-ink/src/lib/hooks/device-diagnostics-hooks.ts +68 -0
  82. package/template-react-vite-muijoy-ink/src/lib/hooks/hotkeys-hooks.ts +16 -0
  83. package/template-react-vite-muijoy-ink/src/lib/utils/device-diagnostics-utility.ts +382 -0
  84. package/template-react-vite-muijoy-ink/src/locales/en.json +50 -0
  85. package/template-react-vite-muijoy-ink/src/pixi-vn.keys.gen.ts +2 -3
  86. package/template-react-vite-muijoy-ink-tauri/.vscode/launch.json +1 -1
  87. package/template-react-vite-muijoy-ink-tauri/.vscode/tasks.json +1 -35
  88. package/template-react-vite-muijoy-ink-tauri/_github/workflows/mobile.yml +76 -13
  89. package/template-react-vite-muijoy-ink-tauri/ink/second_part.ink +0 -1
  90. package/template-react-vite-muijoy-ink-tauri/ink/start.ink +4 -6
  91. package/template-react-vite-muijoy-ink-tauri/package-lock.json +12 -12
  92. package/template-react-vite-muijoy-ink-tauri/package.json +2 -2
  93. package/template-react-vite-muijoy-ink-tauri/src/assets/index.ts +55 -55
  94. package/template-react-vite-muijoy-ink-tauri/src/assets/manifest.gen.json +2 -7
  95. package/template-react-vite-muijoy-ink-tauri/src/components/menus/settings/index.tsx +15 -6
  96. package/template-react-vite-muijoy-ink-tauri/src/components/menus/settings/menus/diagnostics.tsx +332 -0
  97. package/template-react-vite-muijoy-ink-tauri/src/components/menus/settings/quick-menus.tsx +32 -1
  98. package/template-react-vite-muijoy-ink-tauri/src/lib/hooks/device-diagnostics-hooks.ts +96 -0
  99. package/template-react-vite-muijoy-ink-tauri/src/lib/hooks/hotkeys-hooks.ts +16 -0
  100. package/template-react-vite-muijoy-ink-tauri/src/lib/system-info.ts +54 -0
  101. package/template-react-vite-muijoy-ink-tauri/src/lib/utils/device-diagnostics-utility.ts +511 -0
  102. package/template-react-vite-muijoy-ink-tauri/src/locales/en.json +66 -0
  103. package/template-react-vite-muijoy-ink-tauri/src/pixi-vn.keys.gen.ts +2 -3
  104. package/template-react-vite-muijoy-ink-tauri/src-tauri/Cargo.toml +1 -0
  105. package/template-react-vite-muijoy-ink-tauri/src-tauri/src/lib.rs +32 -20
  106. package/template-react-vite-muijoy-ink-tauri/src-tauri/src/system_info.rs +33 -0
  107. package/template-react-vite-muijoy-tauri/.vscode/launch.json +1 -1
  108. package/template-react-vite-muijoy-tauri/.vscode/tasks.json +1 -35
  109. package/template-react-vite-muijoy-tauri/_github/workflows/mobile.yml +76 -13
  110. package/template-react-vite-muijoy-tauri/package.json +1 -1
  111. package/template-react-vite-muijoy-tauri/src/assets/index.ts +55 -55
  112. package/template-react-vite-muijoy-tauri/src/assets/manifest.gen.json +2 -7
  113. package/template-react-vite-muijoy-tauri/src/components/menus/settings/index.tsx +15 -6
  114. package/template-react-vite-muijoy-tauri/src/components/menus/settings/menus/diagnostics.tsx +332 -0
  115. package/template-react-vite-muijoy-tauri/src/components/menus/settings/quick-menus.tsx +32 -1
  116. package/template-react-vite-muijoy-tauri/src/lib/hooks/device-diagnostics-hooks.ts +96 -0
  117. package/template-react-vite-muijoy-tauri/src/lib/hooks/hotkeys-hooks.ts +16 -0
  118. package/template-react-vite-muijoy-tauri/src/lib/system-info.ts +54 -0
  119. package/template-react-vite-muijoy-tauri/src/lib/utils/device-diagnostics-utility.ts +511 -0
  120. package/template-react-vite-muijoy-tauri/src/locales/en.json +66 -0
  121. package/template-react-vite-muijoy-tauri/src/pixi-vn.keys.gen.ts +2 -3
  122. package/template-react-vite-muijoy-tauri/src-tauri/Cargo.toml +1 -0
  123. package/template-react-vite-muijoy-tauri/src-tauri/src/lib.rs +32 -20
  124. package/template-react-vite-muijoy-tauri/src-tauri/src/system_info.rs +33 -0
@@ -0,0 +1,382 @@
1
+ import packageJson from "@/../package.json";
2
+ import { canvas, PIXIVN_VERSION } from "@drincs/pixi-vn";
3
+ import motionPackageJson from "motion/package.json";
4
+ import { VERSION as PIXIJS_VERSION } from "pixi.js";
5
+ import tonePackageJson from "tone/package.json";
6
+
7
+ export interface WebglExtensionCheck {
8
+ name: string;
9
+ supported: boolean;
10
+ }
11
+
12
+ export interface WebglDiagnostics {
13
+ webgl1Supported: boolean;
14
+ webgl2Supported: boolean;
15
+ vendor: string | null;
16
+ renderer: string | null;
17
+ unmaskedVendor: string | null;
18
+ unmaskedRenderer: string | null;
19
+ /** null when it cannot be determined (e.g. WEBGL_debug_renderer_info is blocked). */
20
+ hardwareAccelerated: boolean | null;
21
+ maxTextureSize: number | null;
22
+ floatTexturesSupported: boolean;
23
+ msaaSupported: boolean;
24
+ shadersCompile: boolean;
25
+ keyExtensions: WebglExtensionCheck[];
26
+ allExtensions: string[];
27
+ }
28
+
29
+ export interface BrowserCapabilities {
30
+ audio: boolean;
31
+ gamepad: boolean;
32
+ clipboard: boolean;
33
+ fullscreen: boolean;
34
+ localStorage: boolean;
35
+ indexedDb: boolean;
36
+ /** Best-effort heuristic only: Steam's overlay is compositor-level and generally invisible to page JS. */
37
+ steamOverlayDetected: boolean;
38
+ }
39
+
40
+ export interface ResolutionInfo {
41
+ innerWidth: number;
42
+ innerHeight: number;
43
+ screenWidth: number;
44
+ screenHeight: number;
45
+ devicePixelRatio: number;
46
+ }
47
+
48
+ export interface JsMemoryInfo {
49
+ usedJSHeapSize: number;
50
+ totalJSHeapSize: number;
51
+ jsHeapSizeLimit: number;
52
+ }
53
+
54
+ const KEY_WEBGL_EXTENSIONS = [
55
+ "WEBGL_debug_renderer_info",
56
+ "WEBGL_lose_context",
57
+ "WEBGL_compressed_texture_s3tc",
58
+ "WEBGL_compressed_texture_astc",
59
+ "WEBGL_compressed_texture_etc",
60
+ "OES_texture_float",
61
+ "OES_texture_float_linear",
62
+ "OES_texture_half_float",
63
+ "OES_element_index_uint",
64
+ "OES_standard_derivatives",
65
+ "ANGLE_instanced_arrays",
66
+ "EXT_texture_filter_anisotropic",
67
+ "EXT_color_buffer_float",
68
+ ];
69
+
70
+ const SOFTWARE_RENDERER_PATTERN =
71
+ /swiftshader|llvmpipe|software|microsoft basic render|d3d11 warp|apple software renderer/i;
72
+
73
+ function testShaderCompilation(gl: WebGLRenderingContext | WebGL2RenderingContext): boolean {
74
+ try {
75
+ const vertexShader = gl.createShader(gl.VERTEX_SHADER);
76
+ const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
77
+ if (!vertexShader || !fragmentShader) {
78
+ return false;
79
+ }
80
+
81
+ gl.shaderSource(
82
+ vertexShader,
83
+ "attribute vec2 p; void main() { gl_Position = vec4(p, 0.0, 1.0); }",
84
+ );
85
+ gl.compileShader(vertexShader);
86
+ gl.shaderSource(
87
+ fragmentShader,
88
+ "precision mediump float; void main() { gl_FragColor = vec4(1.0); }",
89
+ );
90
+ gl.compileShader(fragmentShader);
91
+ if (
92
+ !gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS) ||
93
+ !gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)
94
+ ) {
95
+ return false;
96
+ }
97
+
98
+ const program = gl.createProgram();
99
+ if (!program) {
100
+ return false;
101
+ }
102
+ gl.attachShader(program, vertexShader);
103
+ gl.attachShader(program, fragmentShader);
104
+ gl.linkProgram(program);
105
+ return !!gl.getProgramParameter(program, gl.LINK_STATUS);
106
+ } catch {
107
+ return false;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Probes a throwaway canvas instead of the live Pixi-VN canvas so this works
113
+ * regardless of which renderer Pixi actually picked (or before Pixi has initialized).
114
+ */
115
+ export function getWebglDiagnostics(): WebglDiagnostics {
116
+ const probeCanvas = document.createElement("canvas");
117
+ const gl2 = probeCanvas.getContext("webgl2") as WebGL2RenderingContext | null;
118
+ const gl1 = (probeCanvas.getContext("webgl") ??
119
+ probeCanvas.getContext("experimental-webgl")) as WebGLRenderingContext | null;
120
+ const gl = gl2 ?? gl1;
121
+
122
+ if (!gl) {
123
+ return {
124
+ webgl1Supported: false,
125
+ webgl2Supported: false,
126
+ vendor: null,
127
+ renderer: null,
128
+ unmaskedVendor: null,
129
+ unmaskedRenderer: null,
130
+ hardwareAccelerated: null,
131
+ maxTextureSize: null,
132
+ floatTexturesSupported: false,
133
+ msaaSupported: false,
134
+ shadersCompile: false,
135
+ keyExtensions: KEY_WEBGL_EXTENSIONS.map((name) => ({ name, supported: false })),
136
+ allExtensions: [],
137
+ };
138
+ }
139
+
140
+ const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
141
+ const vendor = (gl.getParameter(gl.VENDOR) as string) ?? null;
142
+ const renderer = (gl.getParameter(gl.RENDERER) as string) ?? null;
143
+ const unmaskedVendor = debugInfo
144
+ ? ((gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) as string) ?? null)
145
+ : null;
146
+ const unmaskedRenderer = debugInfo
147
+ ? ((gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) as string) ?? null)
148
+ : null;
149
+ const rendererForHeuristic = unmaskedRenderer ?? renderer;
150
+ const hardwareAccelerated = rendererForHeuristic
151
+ ? !SOFTWARE_RENDERER_PATTERN.test(rendererForHeuristic)
152
+ : null;
153
+
154
+ const allExtensions = gl.getSupportedExtensions() ?? [];
155
+ const keyExtensions = KEY_WEBGL_EXTENSIONS.map((name) => ({
156
+ name,
157
+ supported: allExtensions.includes(name),
158
+ }));
159
+
160
+ const maxTextureSize = (gl.getParameter(gl.MAX_TEXTURE_SIZE) as number) ?? null;
161
+ const floatTexturesSupported = gl2 ? true : allExtensions.includes("OES_texture_float");
162
+ const msaaSupported = gl2
163
+ ? ((gl2.getParameter(gl2.MAX_SAMPLES) as number) ?? 0) > 1
164
+ : (gl.getContextAttributes()?.antialias ?? false);
165
+ const shadersCompile = testShaderCompilation(gl);
166
+
167
+ // This is a throwaway probe context; release it instead of leaving it around.
168
+ gl.getExtension("WEBGL_lose_context")?.loseContext();
169
+
170
+ return {
171
+ webgl1Supported: !!gl1 || !!gl2,
172
+ webgl2Supported: !!gl2,
173
+ vendor,
174
+ renderer,
175
+ unmaskedVendor,
176
+ unmaskedRenderer,
177
+ hardwareAccelerated,
178
+ maxTextureSize,
179
+ floatTexturesSupported,
180
+ msaaSupported,
181
+ shadersCompile,
182
+ keyExtensions,
183
+ allExtensions,
184
+ };
185
+ }
186
+
187
+ export function detectPlatform(): string {
188
+ const ua = navigator.userAgent;
189
+ const uaData = (navigator as Navigator & { userAgentData?: { platform?: string } })
190
+ .userAgentData;
191
+ const platformHint = uaData?.platform ?? navigator.platform ?? "";
192
+
193
+ if (/android/i.test(ua)) return "Android";
194
+ if (/iphone|ipod/i.test(ua)) return "iOS";
195
+ // iPadOS reports as "MacIntel" but exposes multi-touch, unlike a real Mac.
196
+ if (/ipad/i.test(ua) || (/mac/i.test(platformHint) && navigator.maxTouchPoints > 1))
197
+ return "iOS";
198
+ if (/mac/i.test(platformHint)) return "macOS";
199
+ if (/win/i.test(platformHint)) return "Windows";
200
+ if (/linux/i.test(platformHint)) return "Linux";
201
+ return "Unknown";
202
+ }
203
+
204
+ export function detectBrowserEngine(): string {
205
+ const ua = navigator.userAgent;
206
+ if (/edg\//i.test(ua)) return "Chromium (Edge)";
207
+ if (/firefox|fxios/i.test(ua)) return "Gecko (Firefox)";
208
+ if (/crios/i.test(ua)) return "Chromium (Chrome iOS)";
209
+ if (/chrome|chromium/i.test(ua)) return "Chromium";
210
+ if (/safari/i.test(ua)) return "WebKit (Safari)";
211
+ return "Unknown";
212
+ }
213
+
214
+ export function getBrowserCapabilities(): BrowserCapabilities {
215
+ const win = window as Window & {
216
+ webkitAudioContext?: typeof AudioContext;
217
+ cefQuery?: unknown;
218
+ };
219
+
220
+ let localStorageAvailable = false;
221
+ try {
222
+ const testKey = "__diagnostics_test__";
223
+ window.localStorage.setItem(testKey, "1");
224
+ window.localStorage.removeItem(testKey);
225
+ localStorageAvailable = true;
226
+ } catch {
227
+ localStorageAvailable = false;
228
+ }
229
+
230
+ return {
231
+ audio: typeof AudioContext !== "undefined" || typeof win.webkitAudioContext !== "undefined",
232
+ gamepad: typeof navigator.getGamepads === "function",
233
+ clipboard: typeof navigator.clipboard?.writeText === "function",
234
+ fullscreen: document.fullscreenEnabled === true,
235
+ localStorage: localStorageAvailable,
236
+ indexedDb: typeof window.indexedDB !== "undefined",
237
+ steamOverlayDetected:
238
+ typeof win.cefQuery === "function" ||
239
+ /Valve Steam|SteamOverlay/i.test(navigator.userAgent),
240
+ };
241
+ }
242
+
243
+ export function getResolutionInfo(): ResolutionInfo {
244
+ return {
245
+ innerWidth: window.innerWidth,
246
+ innerHeight: window.innerHeight,
247
+ screenWidth: window.screen?.width ?? 0,
248
+ screenHeight: window.screen?.height ?? 0,
249
+ devicePixelRatio: window.devicePixelRatio || 1,
250
+ };
251
+ }
252
+
253
+ export function getJsMemoryInfo(): JsMemoryInfo | null {
254
+ const memory = (performance as Performance & { memory?: JsMemoryInfo }).memory;
255
+ if (!memory) {
256
+ return null;
257
+ }
258
+ return {
259
+ usedJSHeapSize: memory.usedJSHeapSize,
260
+ totalJSHeapSize: memory.totalJSHeapSize,
261
+ jsHeapSizeLimit: memory.jsHeapSizeLimit,
262
+ };
263
+ }
264
+
265
+ export function getCurrentFps(): number | null {
266
+ try {
267
+ return Math.round(canvas.app.ticker.FPS);
268
+ } catch {
269
+ return null;
270
+ }
271
+ }
272
+
273
+ export function getGameVersion(): string {
274
+ return packageJson.version;
275
+ }
276
+
277
+ export function getPixiJsVersion(): string {
278
+ return PIXIJS_VERSION;
279
+ }
280
+
281
+ export function getPixiVnVersion(): string {
282
+ return PIXIVN_VERSION;
283
+ }
284
+
285
+ export function getToneJsVersion(): string {
286
+ return tonePackageJson.version;
287
+ }
288
+
289
+ export function getMotionVersion(): string {
290
+ return motionPackageJson.version;
291
+ }
292
+
293
+ function formatBytes(bytes: number): string {
294
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
295
+ }
296
+
297
+ export interface DeviceDiagnosticsSnapshot {
298
+ webgl: WebglDiagnostics;
299
+ capabilities: BrowserCapabilities;
300
+ resolution: ResolutionInfo;
301
+ memory: JsMemoryInfo | null;
302
+ fps: number | null;
303
+ userAgent: string;
304
+ platform: string;
305
+ browserEngine: string;
306
+ gameVersion: string;
307
+ pixiJsVersion: string;
308
+ pixiVnVersion: string;
309
+ toneJsVersion: string;
310
+ motionVersion: string;
311
+ }
312
+
313
+ export function buildDiagnosticsReportText(snapshot: DeviceDiagnosticsSnapshot): string {
314
+ const { webgl, capabilities, resolution, memory, fps } = snapshot;
315
+ const yn = (value: boolean | null) => (value === null ? "Unknown" : value ? "Yes" : "No");
316
+
317
+ const lines = [
318
+ "=== Device Diagnostics Report ===",
319
+ `Game version: ${snapshot.gameVersion}`,
320
+ `PixiJS version: ${snapshot.pixiJsVersion}`,
321
+ `Pixi'VN version: ${snapshot.pixiVnVersion}`,
322
+ `Tone.js version: ${snapshot.toneJsVersion}`,
323
+ `Motion version: ${snapshot.motionVersion}`,
324
+ `User Agent: ${snapshot.userAgent}`,
325
+ `Platform: ${snapshot.platform}`,
326
+ `Browser/WebView: ${snapshot.browserEngine}`,
327
+ "",
328
+ "-- Graphics --",
329
+ `WebGL1 available: ${yn(webgl.webgl1Supported)}`,
330
+ `WebGL2 available: ${yn(webgl.webgl2Supported)}`,
331
+ `Hardware acceleration: ${yn(webgl.hardwareAccelerated)}`,
332
+ `GPU vendor: ${webgl.unmaskedVendor ?? webgl.vendor ?? "Unknown"}`,
333
+ `GPU renderer: ${webgl.unmaskedRenderer ?? webgl.renderer ?? "Unknown"}`,
334
+ `Max texture size: ${webgl.maxTextureSize ?? "Unknown"}`,
335
+ `Float textures: ${yn(webgl.floatTexturesSupported)}`,
336
+ `MSAA: ${yn(webgl.msaaSupported)}`,
337
+ `Shaders compile: ${yn(webgl.shadersCompile)}`,
338
+ `WebGL extensions: ${webgl.keyExtensions
339
+ .filter((extension) => extension.supported)
340
+ .map((extension) => extension.name)
341
+ .join(", ")}`,
342
+ "",
343
+ "-- Display --",
344
+ `Resolution: ${resolution.innerWidth}x${resolution.innerHeight} (screen ${resolution.screenWidth}x${resolution.screenHeight})`,
345
+ `Device pixel ratio: ${resolution.devicePixelRatio}`,
346
+ `FPS: ${fps ?? "Unknown"}`,
347
+ "",
348
+ "-- Memory --",
349
+ memory
350
+ ? `JS heap: ${formatBytes(memory.usedJSHeapSize)} / ${formatBytes(memory.totalJSHeapSize)} (limit ${formatBytes(memory.jsHeapSizeLimit)})`
351
+ : "JS heap: Not available in this browser",
352
+ "",
353
+ "-- Browser capabilities --",
354
+ `Audio (Web Audio API): ${yn(capabilities.audio)}`,
355
+ `Gamepad API: ${yn(capabilities.gamepad)}`,
356
+ `Clipboard API: ${yn(capabilities.clipboard)}`,
357
+ `Fullscreen API: ${yn(capabilities.fullscreen)}`,
358
+ `LocalStorage: ${yn(capabilities.localStorage)}`,
359
+ `IndexedDB: ${yn(capabilities.indexedDb)}`,
360
+ `Steam Overlay detected (best-effort): ${yn(capabilities.steamOverlayDetected)}`,
361
+ ];
362
+
363
+ return lines.join("\n");
364
+ }
365
+
366
+ export function buildDiagnosticsReportJson(snapshot: DeviceDiagnosticsSnapshot): object {
367
+ return {
368
+ generatedAt: new Date().toISOString(),
369
+ ...snapshot,
370
+ };
371
+ }
372
+
373
+ export function downloadDiagnosticsReportJson(snapshot: DeviceDiagnosticsSnapshot) {
374
+ const jsonString = JSON.stringify(buildDiagnosticsReportJson(snapshot), null, 2);
375
+ const blob = new Blob([jsonString], { type: "application/json" });
376
+ const url = URL.createObjectURL(blob);
377
+ const a = document.createElement("a");
378
+ a.href = url;
379
+ a.download = `${__APP_NAME__}-${__APP_VERSION__}-diagnostics-${Date.now()}.json`;
380
+ a.click();
381
+ URL.revokeObjectURL(url);
382
+ }
@@ -63,6 +63,56 @@
63
63
  "name": "Name",
64
64
  "version": "Version",
65
65
  "engine": "Engine",
66
+ "diagnostics": "Diagnostics",
67
+ "diagnostics_yes": "Yes",
68
+ "diagnostics_no": "No",
69
+ "diagnostics_unknown": "Unknown",
70
+ "diagnostics_webgl2_missing_message": "This game requires WebGL2. Please update your browser, operating system, or GPU drivers.",
71
+ "diagnostics_section_engine": "Engine",
72
+ "diagnostics_section_device": "Device",
73
+ "diagnostics_section_graphics": "Graphics",
74
+ "diagnostics_section_display": "Display",
75
+ "diagnostics_section_memory": "Memory",
76
+ "diagnostics_section_capabilities": "Browser Capabilities",
77
+ "diagnostics_section_extensions": "WebGL Extensions",
78
+ "diagnostics_game_version": "Game version",
79
+ "diagnostics_pixijs_version": "PixiJS version",
80
+ "diagnostics_pixivn_version": "Pixi'VN version",
81
+ "diagnostics_tonejs_version": "Tone.js version",
82
+ "diagnostics_motion_version": "Motion version",
83
+ "diagnostics_platform": "Platform",
84
+ "diagnostics_browser_engine": "Browser/WebView",
85
+ "diagnostics_user_agent": "User Agent",
86
+ "diagnostics_webgl1": "WebGL1 available",
87
+ "diagnostics_webgl2": "WebGL2 available",
88
+ "diagnostics_hardware_acceleration": "Hardware acceleration",
89
+ "diagnostics_gpu_vendor": "GPU vendor",
90
+ "diagnostics_gpu_renderer": "GPU renderer",
91
+ "diagnostics_max_texture_size": "Max texture size",
92
+ "diagnostics_max_texture_size_check": "Max texture size >= 8192",
93
+ "diagnostics_gpu_supported": "GPU supported",
94
+ "diagnostics_float_textures": "Float textures",
95
+ "diagnostics_msaa": "MSAA",
96
+ "diagnostics_shaders_compile": "Shaders compile",
97
+ "diagnostics_resolution": "Resolution",
98
+ "diagnostics_device_pixel_ratio": "Device pixel ratio",
99
+ "diagnostics_fps": "FPS",
100
+ "diagnostics_js_heap": "JS heap used / total",
101
+ "diagnostics_js_heap_unavailable": "Not available in this browser.",
102
+ "diagnostics_audio": "Audio (Web Audio API)",
103
+ "diagnostics_gamepad": "Gamepad API",
104
+ "diagnostics_clipboard": "Clipboard API",
105
+ "diagnostics_fullscreen": "Fullscreen API",
106
+ "diagnostics_local_storage": "LocalStorage",
107
+ "diagnostics_indexed_db": "IndexedDB",
108
+ "diagnostics_steam_overlay": "Steam Overlay",
109
+ "diagnostics_steam_overlay_hint": "Best-effort detection only; not reliable in a standard browser.",
110
+ "diagnostics_copy_report": "Copy report",
111
+ "diagnostics_report_copied": "Report copied to clipboard",
112
+ "diagnostics_report_copy_failed": "Could not copy the report",
113
+ "diagnostics_download_report": "Download JSON",
114
+ "diagnostics_report_downloaded": "Report downloaded",
115
+ "diagnostics_hotkey_description": "Open settings directly on the diagnostics page.",
66
116
  "quick_actions": "Quick Actions",
67
117
  "quick_actions_open_description": "Press Tab to open or close the quick actions wheel.",
68
118
  "quick_actions_click_hint": "Click an action to execute",
@@ -20,7 +20,6 @@ declare module "@drincs/pixi-vn/narration" {
20
20
  }
21
21
  declare module "@drincs/pixi-vn/canvas" {
22
22
  interface PixivnBundleIds {
23
- "default": never;
24
23
  "audio": never;
25
24
  "/": never;
26
25
  "start": never;
@@ -91,7 +90,7 @@ export const characterIds = ["mc","james","steph","sly"] as const;
91
90
  export const characterIdsEnum = {"mc":"mc","james":"james","steph":"steph","sly":"sly"} as const;
92
91
  export const labelIds = ["animation_01","second_part","start","start_|_c-0","start_|_c-1"] as const;
93
92
  export const labelIdsEnum = {"animation_01":"animation_01","second_part":"second_part","start":"start","start_|_c-0":"start_|_c-0","start_|_c-1":"start_|_c-1"} as const;
94
- export const bundleIds = ["default","audio","/","start","second_part","fm01","fm02","m01"] as const;
95
- export const bundleIdsEnum = {"default":"default","audio":"audio","/":"/","start":"start","second_part":"second_part","fm01":"fm01","fm02":"fm02","m01":"m01"} as const;
93
+ export const bundleIds = ["audio","/","start","second_part","fm01","fm02","m01"] as const;
94
+ export const bundleIdsEnum = {"audio":"audio","/":"/","start":"start","second_part":"second_part","fm01":"fm01","fm02":"fm02","m01":"m01"} as const;
96
95
  export const assetAliasIds = ["bgm_cheerful","sfx_whoosh","background_main_menu","bg01-hallway","bg02-dorm","fm01-body","fm01-eyes-grin","fm01-eyes-smile","fm01-eyes-soft","fm01-eyes-upset","fm01-eyes-wow","fm01-mouth-grin00","fm01-mouth-serious00","fm01-mouth-serious01","fm01-mouth-smile00","fm01-mouth-smile01","fm01-mouth-soft00","fm01-mouth-soft01","fm01-mouth-upset00","fm01-mouth-upset01","fm01-mouth-wow01","fm02-body","fm02-eyes-bawl","fm02-eyes-joy","fm02-eyes-nervous","fm02-eyes-smile","fm02-eyes-upset","fm02-eyes-wow","fm02-mouth-cry01","fm02-mouth-nervous00","fm02-mouth-nervous01","fm02-mouth-smile00","fm02-mouth-smile01","fm02-mouth-upset00","fm02-mouth-upset01","fm02-mouth-wow01","m01-body","m01-eyes-annoy","m01-eyes-concern","m01-eyes-cry","m01-eyes-grin","m01-eyes-smile","m01-eyes-wow","m01-mouth-annoy00","m01-mouth-annoy01","m01-mouth-concern00","m01-mouth-concern01","m01-mouth-cry00","m01-mouth-cry01","m01-mouth-grin00","m01-mouth-neutral00","m01-mouth-neutral01","m01-mouth-smile00","m01-mouth-smile01","m01-mouth-wow01"] as const;
97
96
  export const assetAliasIdsEnum = {"bgm_cheerful":"bgm_cheerful","sfx_whoosh":"sfx_whoosh","background_main_menu":"background_main_menu","bg01-hallway":"bg01-hallway","bg02-dorm":"bg02-dorm","fm01-body":"fm01-body","fm01-eyes-grin":"fm01-eyes-grin","fm01-eyes-smile":"fm01-eyes-smile","fm01-eyes-soft":"fm01-eyes-soft","fm01-eyes-upset":"fm01-eyes-upset","fm01-eyes-wow":"fm01-eyes-wow","fm01-mouth-grin00":"fm01-mouth-grin00","fm01-mouth-serious00":"fm01-mouth-serious00","fm01-mouth-serious01":"fm01-mouth-serious01","fm01-mouth-smile00":"fm01-mouth-smile00","fm01-mouth-smile01":"fm01-mouth-smile01","fm01-mouth-soft00":"fm01-mouth-soft00","fm01-mouth-soft01":"fm01-mouth-soft01","fm01-mouth-upset00":"fm01-mouth-upset00","fm01-mouth-upset01":"fm01-mouth-upset01","fm01-mouth-wow01":"fm01-mouth-wow01","fm02-body":"fm02-body","fm02-eyes-bawl":"fm02-eyes-bawl","fm02-eyes-joy":"fm02-eyes-joy","fm02-eyes-nervous":"fm02-eyes-nervous","fm02-eyes-smile":"fm02-eyes-smile","fm02-eyes-upset":"fm02-eyes-upset","fm02-eyes-wow":"fm02-eyes-wow","fm02-mouth-cry01":"fm02-mouth-cry01","fm02-mouth-nervous00":"fm02-mouth-nervous00","fm02-mouth-nervous01":"fm02-mouth-nervous01","fm02-mouth-smile00":"fm02-mouth-smile00","fm02-mouth-smile01":"fm02-mouth-smile01","fm02-mouth-upset00":"fm02-mouth-upset00","fm02-mouth-upset01":"fm02-mouth-upset01","fm02-mouth-wow01":"fm02-mouth-wow01","m01-body":"m01-body","m01-eyes-annoy":"m01-eyes-annoy","m01-eyes-concern":"m01-eyes-concern","m01-eyes-cry":"m01-eyes-cry","m01-eyes-grin":"m01-eyes-grin","m01-eyes-smile":"m01-eyes-smile","m01-eyes-wow":"m01-eyes-wow","m01-mouth-annoy00":"m01-mouth-annoy00","m01-mouth-annoy01":"m01-mouth-annoy01","m01-mouth-concern00":"m01-mouth-concern00","m01-mouth-concern01":"m01-mouth-concern01","m01-mouth-cry00":"m01-mouth-cry00","m01-mouth-cry01":"m01-mouth-cry01","m01-mouth-grin00":"m01-mouth-grin00","m01-mouth-neutral00":"m01-mouth-neutral00","m01-mouth-neutral01":"m01-mouth-neutral01","m01-mouth-smile00":"m01-mouth-smile00","m01-mouth-smile01":"m01-mouth-smile01","m01-mouth-wow01":"m01-mouth-wow01"} as const;
@@ -26,7 +26,7 @@
26
26
  "cwd": "${workspaceFolder}",
27
27
  "terminal": "console",
28
28
  "preLaunchTask": "Cargo Build (Desktop Dev)",
29
- "postDebugTask": "Stop Vite Desktop Server"
29
+ "postDebugTask": "Stop Vite Dev Server"
30
30
  },
31
31
  {
32
32
  "name": "Debug Game Preview",
@@ -106,7 +106,7 @@
106
106
  "label": "Cargo Build (Desktop Dev)",
107
107
  "type": "shell",
108
108
  "command": "cargo build --manifest-path=./src-tauri/Cargo.toml --no-default-features",
109
- "dependsOn": ["Start Vite Desktop Server"],
109
+ "dependsOn": ["Restart Vite Dev Server"],
110
110
  "presentation": {
111
111
  "reveal": "always",
112
112
  "panel": "dedicated",
@@ -114,40 +114,6 @@
114
114
  "close": true
115
115
  },
116
116
  "problemMatcher": ["$rustc"]
117
- },
118
- {
119
- "label": "Start Vite Desktop Server",
120
- "type": "shell",
121
- "command": "npm run dev",
122
- "isBackground": true,
123
- "dependsOn": ["Install Dependencies"],
124
- "problemMatcher": {
125
- "owner": "vite",
126
- "pattern": {
127
- "regexp": "^(?!x)x"
128
- },
129
- "background": {
130
- "activeOnStart": true,
131
- "beginsPattern": ".",
132
- "endsPattern": "localhost:5173"
133
- }
134
- },
135
- "presentation": {
136
- "reveal": "always",
137
- "panel": "dedicated",
138
- "clear": false,
139
- "close": true
140
- }
141
- },
142
- {
143
- "label": "Stop Vite Desktop Server",
144
- "type": "shell",
145
- "command": "pkill -f 'vite' 2>/dev/null; pkill -f 'npm run dev' 2>/dev/null; exit 0",
146
- "presentation": {
147
- "reveal": "never",
148
- "panel": "dedicated",
149
- "close": true
150
- }
151
117
  }
152
118
  ]
153
119
  }
@@ -100,26 +100,89 @@ jobs:
100
100
  with open(manifest, 'w') as f:
101
101
  f.write(content)
102
102
 
103
- # Tauri may generate styles.xml or themes.xml depending on the version
103
+ # Tauri may generate styles.xml or themes.xml, and both a light (values/)
104
+ # and dark (values-night/) variant — patch every variant we find so the
105
+ # fullscreen flag doesn't silently no-op when the device is in dark mode.
104
106
  styles_candidates = (
105
- glob.glob('src-tauri/gen/android/app/src/main/res/values/styles.xml') +
106
- glob.glob('src-tauri/gen/android/app/src/main/res/values/themes.xml')
107
+ glob.glob('src-tauri/gen/android/app/src/main/res/values*/styles.xml') +
108
+ glob.glob('src-tauri/gen/android/app/src/main/res/values*/themes.xml')
107
109
  )
108
110
  if not styles_candidates:
109
111
  print('Warning: no styles.xml/themes.xml found, skipping fullscreen patch')
110
112
  else:
111
- styles = styles_candidates[0]
112
- with open(styles) as f:
113
+ for styles in styles_candidates:
114
+ with open(styles) as f:
115
+ content = f.read()
116
+ # Hide status bar (fullscreen) — kept as a best-effort theme hint,
117
+ # but on targetSdk 35+ (edge-to-edge is enforced by the OS) this
118
+ # alone has no effect; the real fix is the MainActivity.kt patch below.
119
+ content = content.replace(
120
+ '</style>',
121
+ ' <item name="android:windowFullscreen">true</item>\n </style>',
122
+ 1
123
+ )
124
+ with open(styles, 'w') as f:
125
+ f.write(content)
126
+ print(f'Patched {styles}')
127
+
128
+ # Since this project's Android template targets SDK 35+, edge-to-edge display
129
+ # is enforced by the OS and the theme-based windowFullscreen flag above no
130
+ # longer hides the status bar (it's always drawn, transparent, over the
131
+ # content). The only reliable way to hide it is to hide the system bars at
132
+ # runtime via WindowInsetsControllerCompat in MainActivity.kt.
133
+ activity_candidates = glob.glob(
134
+ 'src-tauri/gen/android/app/src/main/**/MainActivity.kt', recursive=True
135
+ )
136
+ if not activity_candidates:
137
+ print('WARNING: MainActivity.kt not found — status bar hiding NOT patched')
138
+ else:
139
+ activity = activity_candidates[0]
140
+ with open(activity) as f:
113
141
  content = f.read()
114
- # Hide status bar (fullscreen)
115
- content = content.replace(
116
- '</style>',
117
- ' <item name="android:windowFullscreen">true</item>\n </style>',
118
- 1
119
- )
120
- with open(styles, 'w') as f:
142
+
143
+ new_imports = [
144
+ 'androidx.core.view.WindowCompat',
145
+ 'androidx.core.view.WindowInsetsCompat',
146
+ 'androidx.core.view.WindowInsetsControllerCompat',
147
+ ]
148
+ import_lines = list(re.finditer(r'^import .+$', content, re.MULTILINE))
149
+ if import_lines:
150
+ insert_at = import_lines[-1].end()
151
+ addition = ''.join(
152
+ f'\nimport {imp}' for imp in new_imports if imp not in content
153
+ )
154
+ content = content[:insert_at] + addition + content[insert_at:]
155
+
156
+ if 'hideSystemBars' not in content:
157
+ content = content.replace(
158
+ 'super.onCreate(savedInstanceState)',
159
+ 'super.onCreate(savedInstanceState)\n hideSystemBars()',
160
+ 1
161
+ )
162
+ content = re.sub(
163
+ r'\n}\s*$',
164
+ '\n\n'
165
+ ' override fun onWindowFocusChanged(hasFocus: Boolean) {\n'
166
+ ' super.onWindowFocusChanged(hasFocus)\n'
167
+ ' if (hasFocus) {\n'
168
+ ' hideSystemBars()\n'
169
+ ' }\n'
170
+ ' }\n'
171
+ '\n'
172
+ ' private fun hideSystemBars() {\n'
173
+ ' WindowCompat.setDecorFitsSystemWindows(window, false)\n'
174
+ ' val controller = WindowInsetsControllerCompat(window, window.decorView)\n'
175
+ ' controller.hide(WindowInsetsCompat.Type.systemBars())\n'
176
+ ' controller.systemBarsBehavior =\n'
177
+ ' WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE\n'
178
+ ' }\n'
179
+ '}\n',
180
+ content,
181
+ count=1
182
+ )
183
+ with open(activity, 'w') as f:
121
184
  f.write(content)
122
- print(f'Patched {styles}')
185
+ print(f'Patched {activity} (hide system bars at runtime)')
123
186
  PYEOF
124
187
 
125
188
  - name: build Android app
@@ -1,4 +1,3 @@
1
- -> second_part
2
1
  === second_part ===
3
2
 
4
3
  # show text bg "(A few minutes later...)" style \{ fontFamily: "Arial", dropShadow: \{ alpha: 0.8, angle: 2.1, blur: 4, color: "0x111111", distance: 10, \}, fill: "\#ffffff", stroke: \{ color: "\#004620", width: 12, join: "round" \}, fontSize: 60, fontWeight: "lighter" \} with fade
@@ -1,6 +1,4 @@
1
1
  -> start
2
- INCLUDE second_part.ink
3
-
4
2
  === start ===
5
3
  # lazyload bundle m01 fm01 fm02
6
4
 
@@ -46,7 +44,7 @@ james: I know you're both watching, come on out already!
46
44
  # show imagecontainer steph [fm02-body fm02-eyes-nervous fm02-mouth-nervous00] xAlign 0.8 yAlign 1 with movein direction left ease easeInOut
47
45
  sly: I just wanted to see what the new guy was like.
48
46
  # show imagecontainer sly [fm01-body fm01-eyes-upset fm01-mouth-smile01]
49
- <><span className="inline-block motion-translate-y-loop-25">Hey</span>, you, [mc]- be nice to our little brother,
47
+ <><span class="inline-block animate-bounce animate-duration-700">Hey</span>, you, [mc]- be nice to our little brother,
50
48
  # show imagecontainer sly [fm01-body fm01-eyes-smile fm01-mouth-grin00]
51
49
  <>or you'll have to deal with *us*.
52
50
  # show imagecontainer james [m01-body m01-eyes-smile m01-mouth-neutral00]
@@ -103,7 +101,7 @@ sly: It's just a handshake...
103
101
  # show imagecontainer james [m01-body m01-eyes-smile m01-mouth-grin00]
104
102
  # show imagecontainer sly [fm01-body fm01-eyes-upset fm01-mouth-serious00]
105
103
  # show imagecontainer steph [fm02-body fm02-eyes-upset fm02-mouth-upset01]
106
- steph: <span className="inline-block animate-wave">Then just give it to him!</span>
104
+ steph: <span class="inline-block animate-wave">Then just give it to him!</span>
107
105
  # show imagecontainer james [m01-body m01-eyes-concern m01-mouth-smile01]
108
106
  # show imagecontainer sly [fm01-body fm01-eyes-smile fm01-mouth-serious00]
109
107
  # show imagecontainer steph [fm02-body fm02-eyes-upset fm02-mouth-upset00]
@@ -208,7 +206,7 @@ steph: Oh! You gotta take in your luggage!
208
206
  # remove image steph with moveout direction left ease easeInOut duration 0.5 delay 0.1
209
207
 
210
208
  You want continue to the next part?<># continue
211
- * Yes, I want to continue
209
+ * [Yes, I want to continue]
212
210
  -> second_part
213
- * No, I want to stop here
211
+ * [No, I want to stop here]
214
212
  -> END