diff --git a/.gitignore b/.gitignore index bcb6648..0d6e042 100644 --- a/.gitignore +++ b/.gitignore @@ -64,5 +64,7 @@ __pycache__/ *.keystore local.properties # Generated by scripts/build_reflex_tauri_wasm.py. -Tauri/game-reflex-poc/frontend/game_reflex_poc_tauri.js -Tauri/game-reflex-poc/frontend/game_reflex_poc_tauri_bg.wasm +crates/apps/game-reflex-poc-tauri/frontend/wasm/game_reflex_poc_wasm.js +crates/apps/game-reflex-poc-tauri/frontend/wasm/game_reflex_poc_wasm_bg.wasm +crates/apps/game-reflex-poc-tauri/frontend/wasm/game_reflex_poc_wasm.d.ts +crates/apps/game-reflex-poc-tauri/frontend/wasm/game_reflex_poc_wasm_bg.wasm.d.ts diff --git a/Android/game-reflex-poc/build.gradle b/Android/game-reflex-poc/build.gradle index f5f4f8c..071e7e8 100644 --- a/Android/game-reflex-poc/build.gradle +++ b/Android/game-reflex-poc/build.gradle @@ -1,5 +1,5 @@ // file: Android/game-reflex-poc/build.gradle -// version: 12 +// version: 13 plugins { id 'com.android.application' @@ -18,7 +18,7 @@ android { minSdk 21 targetSdk 36 versionCode 1 - versionName '0.1.0-1-alpha.2.fix.1' + versionName '0.1.0-1-alpha.2.fix.2' } compileOptions { diff --git a/Android/game-snake-poc/build.gradle b/Android/game-snake-poc/build.gradle index a48fe79..8f2c86b 100644 --- a/Android/game-snake-poc/build.gradle +++ b/Android/game-snake-poc/build.gradle @@ -1,5 +1,5 @@ // file: Android/game-snake-poc/build.gradle -// version: 12 +// version: 13 plugins { id 'com.android.application' @@ -18,7 +18,7 @@ android { minSdk 21 targetSdk 36 versionCode 1 - versionName '0.1.0-1-alpha.2.fix.1' + versionName '0.1.0-1-alpha.2.fix.2' } compileOptions { diff --git a/Cargo.toml b/Cargo.toml index b98004d..32fe687 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ # file: Cargo.toml -# version: 29 +# version: 30 [workspace] resolver = "3" @@ -15,10 +15,11 @@ members = [ "crates/common/game-logging-lib", "crates/apps/game-android-entrypoint", "crates/apps/game-reflex-poc-tauri", + "crates/apps/game-reflex-poc-wasm", ] [workspace.package] -version = "0.1.0-1-alpha.2.fix.1" +version = "0.1.0-1-alpha.2.fix.2" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/games" @@ -32,6 +33,7 @@ tracing-appender = "0.2.5" tracing-subscriber = "0.3.23" tauri = "2" tauri-build = "2" +tauri-plugin-tracing = "^0.3" wasm-bindgen = "0.2" [workspace.lints.rust] diff --git a/Tauri/README.md b/Tauri/README.md deleted file mode 100644 index 75be8dd..0000000 --- a/Tauri/README.md +++ /dev/null @@ -1,10 +0,0 @@ - - - -# Tauri - -Cette arborescence contient les frontends statiques des variantes Desktop Tauri. - -Le backend Rust reste une crate sous `crates/apps/`, conformément à la règle du workspace Cargo unique. - -Les fichiers générés par `wasm-bindgen` ne sont pas versionnés. diff --git a/Tauri/game-reflex-poc/frontend/main.js b/Tauri/game-reflex-poc/frontend/main.js deleted file mode 100644 index 5145b36..0000000 --- a/Tauri/game-reflex-poc/frontend/main.js +++ /dev/null @@ -1,76 +0,0 @@ -// file: Tauri/game-reflex-poc/frontend/main.js -// version: 1 - -import init, { ReflexWasmGame } from "./game_reflex_poc_tauri.js"; - -const FRAME_MS = 16; - -function color(red, green, blue) { - return `rgb(${red} ${green} ${blue})`; -} - -function resizeCanvas(canvas) { - const ratio = window.devicePixelRatio || 1; - const width = Math.max(1, Math.floor(canvas.clientWidth * ratio)); - const height = Math.max(1, Math.floor(canvas.clientHeight * ratio)); - if (canvas.width !== width || canvas.height !== height) { - canvas.width = width; - canvas.height = height; - } -} - -function render(game, canvas, context, score) { - resizeCanvas(canvas); - context.fillStyle = color(game.background_red(), game.background_green(), game.background_blue()); - context.fillRect(0, 0, canvas.width, canvas.height); - - const count = game.rectangle_count(); - for (let index = 0; index < count; index += 1) { - context.fillStyle = color(game.rectangle_red(index), game.rectangle_green(index), game.rectangle_blue(index)); - context.fillRect( - game.rectangle_x(index) * canvas.width, - game.rectangle_y(index) * canvas.height, - game.rectangle_width(index) * canvas.width, - game.rectangle_height(index) * canvas.height, - ); - } - - score.value = `Score : ${game.score()}`; -} - -async function start() { - await init(); - - const canvas = document.querySelector("#game"); - const score = document.querySelector("#score"); - const context = canvas.getContext("2d"); - if (!(canvas instanceof HTMLCanvasElement) || !(score instanceof HTMLOutputElement) || context === null) { - throw new Error("Reflex Tauri frontend DOM is incomplete"); - } - - const game = new ReflexWasmGame(); - canvas.addEventListener("pointerdown", (event) => { - const bounds = canvas.getBoundingClientRect(); - const x = (event.clientX - bounds.left) / bounds.width; - const y = (event.clientY - bounds.top) / bounds.height; - game.pointer_down(x, y); - render(game, canvas, context, score); - }); - - let previous = performance.now(); - function frame(now) { - while (now - previous >= FRAME_MS) { - game.tick(); - previous += FRAME_MS; - } - render(game, canvas, context, score); - window.requestAnimationFrame(frame); - } - - render(game, canvas, context, score); - window.requestAnimationFrame(frame); -} - -start().catch((error) => { - console.error("Reflex Tauri WASM startup failed", error); -}); diff --git a/crates/apps/game-reflex-poc-tauri/Cargo.toml b/crates/apps/game-reflex-poc-tauri/Cargo.toml index 90f1115..78c4ad8 100644 --- a/crates/apps/game-reflex-poc-tauri/Cargo.toml +++ b/crates/apps/game-reflex-poc-tauri/Cargo.toml @@ -1,5 +1,5 @@ # file: crates/apps/game-reflex-poc-tauri/Cargo.toml -# version: 1 +# version: 3 [package] name = "game-reflex-poc-tauri" @@ -9,24 +9,24 @@ license.workspace = true repository.workspace = true authors.workspace = true publish.workspace = true -build = "build.rs" [lib] -crate-type = ["cdylib", "rlib"] +name = "game_reflex_poc_tauri_lib" +path = "src/lib.rs" +crate-type = ["staticlib", "cdylib", "rlib"] -[dependencies] -engine-v1-common = { path = "../../engines/engine-v1-common" } -engine-v1-platform-api = { path = "../../engines/engine-v1-platform-api" } -game-reflex-poc = { path = "../../games/game-reflex-poc" } - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -tauri.workspace = true - -[target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen.workspace = true +[[bin]] +name = "game-reflex-poc-tauri" +path = "src/main.rs" [build-dependencies] tauri-build.workspace = true +[dependencies] +engine-v1-platform-api = { path = "../../engines/engine-v1-platform-api" } +tauri.workspace = true +tauri-plugin-tracing.workspace = true +tracing.workspace = true + [lints] workspace = true diff --git a/crates/apps/game-reflex-poc-tauri/build.rs b/crates/apps/game-reflex-poc-tauri/build.rs index 24848af..fcae3bf 100644 --- a/crates/apps/game-reflex-poc-tauri/build.rs +++ b/crates/apps/game-reflex-poc-tauri/build.rs @@ -1,14 +1,9 @@ // file: crates/apps/game-reflex-poc-tauri/build.rs -// version: 2 +// version: 3 -//! Tauri build script skipped for the WebAssembly library target. +//! Native Tauri build script for the Reflex Desktop WebView runner. fn main() { - let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH"); - match target_arch { - std::result::Result::Ok(value) if value == "wasm32" => return, - _ => {}, - } tauri_build::build(); return; } diff --git a/crates/apps/game-reflex-poc-tauri/capabilities/default.json b/crates/apps/game-reflex-poc-tauri/capabilities/default.json new file mode 100644 index 0000000..5e194a2 --- /dev/null +++ b/crates/apps/game-reflex-poc-tauri/capabilities/default.json @@ -0,0 +1,7 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Default capability for Reflex Tauri POC", + "windows": ["main"], + "permissions": ["core:default", "tracing:default"] +} diff --git a/Tauri/game-reflex-poc/frontend/index.html b/crates/apps/game-reflex-poc-tauri/frontend/index.html similarity index 67% rename from Tauri/game-reflex-poc/frontend/index.html rename to crates/apps/game-reflex-poc-tauri/frontend/index.html index 513e109..7469331 100644 --- a/Tauri/game-reflex-poc/frontend/index.html +++ b/crates/apps/game-reflex-poc-tauri/frontend/index.html @@ -4,14 +4,14 @@ Reflex POC Tauri - +
Score : 0 - Tauri · WebView · WASM + Initialisation…
- + diff --git a/Tauri/game-reflex-poc/frontend/style.css b/crates/apps/game-reflex-poc-tauri/frontend/style.css similarity index 89% rename from Tauri/game-reflex-poc/frontend/style.css rename to crates/apps/game-reflex-poc-tauri/frontend/style.css index d2af18d..108ebdb 100644 --- a/Tauri/game-reflex-poc/frontend/style.css +++ b/crates/apps/game-reflex-poc-tauri/frontend/style.css @@ -1,4 +1,4 @@ -/* file: Tauri/game-reflex-poc/frontend/style.css */ +/* file: crates/apps/game-reflex-poc-tauri/frontend/style.css */ /* version: 1 */ html, diff --git a/crates/apps/game-reflex-poc-tauri/frontend/ts/bridge.ts b/crates/apps/game-reflex-poc-tauri/frontend/ts/bridge.ts new file mode 100644 index 0000000..db4ddc3 --- /dev/null +++ b/crates/apps/game-reflex-poc-tauri/frontend/ts/bridge.ts @@ -0,0 +1,12 @@ +// file: crates/apps/game-reflex-poc-tauri/frontend/ts/bridge.ts +// version: 1 + +import { invoke } from "@tauri-apps/api/core"; + +export async function getRuntimeLabel(): Promise { + return await invoke("get_runtime_label"); +} + +export async function notifyFrontendReady(): Promise { + await invoke("frontend_ready"); +} diff --git a/crates/apps/game-reflex-poc-tauri/frontend/ts/game.ts b/crates/apps/game-reflex-poc-tauri/frontend/ts/game.ts new file mode 100644 index 0000000..da999d4 --- /dev/null +++ b/crates/apps/game-reflex-poc-tauri/frontend/ts/game.ts @@ -0,0 +1,67 @@ +// file: crates/apps/game-reflex-poc-tauri/frontend/ts/game.ts +// version: 1 + +import init, { ReflexWasmGame } from "../wasm/game_reflex_poc_wasm.js"; +import { tracingDebug, tracingTrace } from "./logging"; + +const FRAME_MS = 16; + +function color(red: number, green: number, blue: number): string { + return `rgb(${red} ${green} ${blue})`; +} + +function resizeCanvas(canvas: HTMLCanvasElement): void { + const ratio = window.devicePixelRatio || 1; + const width = Math.max(1, Math.floor(canvas.clientWidth * ratio)); + const height = Math.max(1, Math.floor(canvas.clientHeight * ratio)); + if (canvas.width !== width || canvas.height !== height) { + canvas.width = width; + canvas.height = height; + } +} + +function render(game: ReflexWasmGame, canvas: HTMLCanvasElement, context: CanvasRenderingContext2D, score: HTMLOutputElement): void { + resizeCanvas(canvas); + context.fillStyle = color(game.background_red(), game.background_green(), game.background_blue()); + context.fillRect(0, 0, canvas.width, canvas.height); + const count = game.rectangle_count(); + for (let index = 0; index < count; index += 1) { + context.fillStyle = color(game.rectangle_red(index), game.rectangle_green(index), game.rectangle_blue(index)); + context.fillRect( + game.rectangle_x(index) * canvas.width, + game.rectangle_y(index) * canvas.height, + game.rectangle_width(index) * canvas.width, + game.rectangle_height(index) * canvas.height, + ); + } + score.value = `Score : ${game.score()}`; +} + +export async function startGame(canvas: HTMLCanvasElement, score: HTMLOutputElement): Promise { + await init(); + await tracingDebug("Reflex WASM module initialized"); + const context = canvas.getContext("2d"); + if (context === null) { + throw new Error("2D canvas context is unavailable"); + } + const game = new ReflexWasmGame(); + canvas.addEventListener("pointerdown", event => { + const bounds = canvas.getBoundingClientRect(); + const x = (event.clientX - bounds.left) / bounds.width; + const y = (event.clientY - bounds.top) / bounds.height; + game.pointer_down(x, y); + render(game, canvas, context, score); + void tracingTrace(`pointerdown x=${x.toFixed(4)} y=${y.toFixed(4)} score=${game.score()}`); + }); + let previous = performance.now(); + function frame(now: number): void { + while (now - previous >= FRAME_MS) { + game.tick(); + previous += FRAME_MS; + } + render(game, canvas, context, score); + window.requestAnimationFrame(frame); + } + render(game, canvas, context, score); + window.requestAnimationFrame(frame); +} diff --git a/crates/apps/game-reflex-poc-tauri/frontend/ts/logging.ts b/crates/apps/game-reflex-poc-tauri/frontend/ts/logging.ts new file mode 100644 index 0000000..14ffc95 --- /dev/null +++ b/crates/apps/game-reflex-poc-tauri/frontend/ts/logging.ts @@ -0,0 +1,38 @@ +// file: crates/apps/game-reflex-poc-tauri/frontend/ts/logging.ts +// version: 1 + +import { attachConsole, debug, error, info, trace, warn } from "@fltsci/tauri-plugin-tracing"; + +let detachRustConsole: (() => void) | null = null; + +export async function initializeTracing(): Promise { + detachRustConsole = await attachConsole(); + await info("Reflex Tauri frontend tracing initialized", "games::tauri::reflex::frontend"); +} + +export async function tracingTrace(message: string): Promise { + await trace(message, "games::tauri::reflex::frontend"); +} + +export async function tracingDebug(message: string): Promise { + await debug(message, "games::tauri::reflex::frontend"); +} + +export async function tracingInfo(message: string): Promise { + await info(message, "games::tauri::reflex::frontend"); +} + +export async function tracingWarn(message: string): Promise { + await warn(message, "games::tauri::reflex::frontend"); +} + +export async function tracingError(message: string): Promise { + await error(message, "games::tauri::reflex::frontend"); +} + +export function detachTracingConsole(): void { + if (detachRustConsole !== null) { + detachRustConsole(); + detachRustConsole = null; + } +} diff --git a/crates/apps/game-reflex-poc-tauri/frontend/ts/main.ts b/crates/apps/game-reflex-poc-tauri/frontend/ts/main.ts new file mode 100644 index 0000000..1b9e37d --- /dev/null +++ b/crates/apps/game-reflex-poc-tauri/frontend/ts/main.ts @@ -0,0 +1,25 @@ +// file: crates/apps/game-reflex-poc-tauri/frontend/ts/main.ts +// version: 1 + +import { getRuntimeLabel, notifyFrontendReady } from "./bridge"; +import { startGame } from "./game"; +import { initializeTracing, tracingError, tracingInfo } from "./logging"; + +async function main(): Promise { + await initializeTracing(); + const canvas = document.querySelector("#game"); + const score = document.querySelector("#score"); + const runtime = document.querySelector("#runtime"); + if (canvas === null || score === null || runtime === null) { + throw new Error("Reflex Tauri frontend DOM is incomplete"); + } + runtime.value = await getRuntimeLabel(); + await startGame(canvas, score); + await notifyFrontendReady(); + await tracingInfo("Reflex Tauri frontend started"); +} + +void main().catch(async caughtError => { + const message = caughtError instanceof Error ? caughtError.stack ?? caughtError.message : String(caughtError); + await tracingError(`Reflex Tauri frontend startup failed: ${message}`); +}); diff --git a/crates/apps/game-reflex-poc-tauri/package.json b/crates/apps/game-reflex-poc-tauri/package.json new file mode 100644 index 0000000..f588f97 --- /dev/null +++ b/crates/apps/game-reflex-poc-tauri/package.json @@ -0,0 +1,20 @@ +{ + "name": "game-reflex-poc-tauri", + "private": true, + "version": "0.1.0-1-alpha.2.fix.2", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build" + }, + "dependencies": { + "@fltsci/tauri-plugin-tracing": "^0.3", + "@tauri-apps/api": "^2.11" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.11", + "@types/node": "^26.1", + "typescript": "^7.0", + "vite": "^8.2" + } +} diff --git a/crates/apps/game-reflex-poc-tauri/src/lib.rs b/crates/apps/game-reflex-poc-tauri/src/lib.rs index 6ac0fdd..25bb0fd 100644 --- a/crates/apps/game-reflex-poc-tauri/src/lib.rs +++ b/crates/apps/game-reflex-poc-tauri/src/lib.rs @@ -1,15 +1,19 @@ // file: crates/apps/game-reflex-poc-tauri/src/lib.rs -// version: 1 +// version: 2 + +//! Tauri desktop application facade for the Reflex WebAssembly POC. -#![warn(missing_docs)] -#![deny(unreachable_pub)] #![forbid(unsafe_code)] +#![deny(unreachable_pub)] +#![warn(missing_docs)] -//! WebAssembly adapter for the Reflex POC when hosted by the Tauri variant. +mod runtime; +mod tauri; -#[cfg(target_arch = "wasm32")] -mod wasm_runtime; +/// Runs the Reflex Tauri desktop application. +pub use self::tauri::run; -#[cfg(target_arch = "wasm32")] -/// Re-export of the Reflex WebAssembly runtime adapter. -pub use self::wasm_runtime::ReflexWasmGame; +/// Records that the Vite/TypeScript frontend reached its ready state. +pub(crate) use self::runtime::record_frontend_ready; +/// Returns the runtime label exposed by the Tauri bridge. +pub(crate) use self::runtime::runtime_label; diff --git a/crates/apps/game-reflex-poc-tauri/src/main.rs b/crates/apps/game-reflex-poc-tauri/src/main.rs index d8b4dd5..16bfe57 100644 --- a/crates/apps/game-reflex-poc-tauri/src/main.rs +++ b/crates/apps/game-reflex-poc-tauri/src/main.rs @@ -1,25 +1,20 @@ // file: crates/apps/game-reflex-poc-tauri/src/main.rs -// version: 1 +// version: 2 + +//! Binary entry point for the Reflex Tauri desktop application. -#![warn(missing_docs)] -#![deny(unreachable_pub)] #![forbid(unsafe_code)] +#![deny(unreachable_pub)] +#![warn(missing_docs)] +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -//! Native Tauri host for the local Reflex WebAssembly POC. - -#[cfg(not(target_arch = "wasm32"))] -fn main() { - let result = tauri::Builder::default().run(tauri::generate_context!()); - match result { - std::result::Result::Ok(()) => {}, +fn main() -> std::process::ExitCode { + let result = game_reflex_poc_tauri_lib::run(); + return match result { + std::result::Result::Ok(()) => std::process::ExitCode::SUCCESS, std::result::Result::Err(error) => { - eprintln!("Reflex POC Tauri runner failed: {error}"); + eprintln!("Reflex Tauri application error: {error}"); + std::process::ExitCode::FAILURE }, - } - return; -} - -#[cfg(target_arch = "wasm32")] -fn main() { - return; + }; } diff --git a/crates/apps/game-reflex-poc-tauri/src/runtime.rs b/crates/apps/game-reflex-poc-tauri/src/runtime.rs new file mode 100644 index 0000000..a06cf46 --- /dev/null +++ b/crates/apps/game-reflex-poc-tauri/src/runtime.rs @@ -0,0 +1,18 @@ +// file: crates/apps/game-reflex-poc-tauri/src/runtime.rs +// version: 1 + +/// Stable label for the current Tauri/WASM runtime combination. +pub(crate) fn runtime_label() -> &'static str { + return "Desktop / TauriWebView / Wasm / KeyboardMouse"; +} + +/// Records that the frontend completed its startup sequence. +pub(crate) fn record_frontend_ready() { + tracing::info!( + target: "games::tauri::reflex", + action = "frontend_ready", + runtime = crate::runtime_label(), + "Reflex Tauri frontend ready" + ); + return; +} diff --git a/crates/apps/game-reflex-poc-tauri/src/tauri.rs b/crates/apps/game-reflex-poc-tauri/src/tauri.rs new file mode 100644 index 0000000..fa67764 --- /dev/null +++ b/crates/apps/game-reflex-poc-tauri/src/tauri.rs @@ -0,0 +1,39 @@ +// file: crates/apps/game-reflex-poc-tauri/src/tauri.rs +// version: 1 + +#[tauri::command] +fn get_runtime_label() -> String { + return crate::runtime_label().to_string(); +} + +#[tauri::command] +fn frontend_ready() { + crate::record_frontend_ready(); + return; +} + +fn configure_plugins(builder: tauri::Builder) -> tauri::Builder { + let tracing_plugin = tauri_plugin_tracing::Builder::new().build::(); + return builder.plugin(tracing_plugin); +} + +#[allow(clippy::question_mark_used)] // Tauri generates the question-mark operator internally for command dispatch. +fn configure_commands(builder: tauri::Builder) -> tauri::Builder { + return builder.invoke_handler(tauri::generate_handler![get_runtime_label, frontend_ready]); +} + +/// Runs the Reflex Tauri desktop application. +pub fn run() -> std::result::Result<(), String> { + tracing::info!( + target: "games::tauri::reflex", + action = "runtime_start", + "Starting Reflex Tauri runtime" + ); + let builder = tauri::Builder::default(); + let builder = configure_plugins(builder); + let builder = configure_commands(builder); + return match builder.run(tauri::generate_context!()) { + std::result::Result::Ok(()) => std::result::Result::Ok(()), + std::result::Result::Err(error) => std::result::Result::Err(error.to_string()), + }; +} diff --git a/crates/apps/game-reflex-poc-tauri/tauri.conf.json b/crates/apps/game-reflex-poc-tauri/tauri.conf.json index 800680d..686e829 100644 --- a/crates/apps/game-reflex-poc-tauri/tauri.conf.json +++ b/crates/apps/game-reflex-poc-tauri/tauri.conf.json @@ -1,14 +1,24 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Reflex POC Tauri", - "version": "0.1.0-1-alpha.2.fix.1", + "version": "0.1.0-1-alpha.2.fix.2", "identifier": "com.sasedev.games.reflex.tauri", "build": { - "frontendDist": "../../../Tauri/game-reflex-poc/frontend" + "beforeDevCommand": { + "script": "python3 ../../../scripts/build_reflex_tauri_wasm.py && npm run dev", + "cwd": "." + }, + "devUrl": "http://localhost:1432", + "beforeBuildCommand": { + "script": "python3 ../../../scripts/build_reflex_tauri_wasm.py && npm run build", + "cwd": "." + }, + "frontendDist": "../../../builds/sasedev-games/game-reflex-poc-tauri/dist" }, "app": { "windows": [ { + "label": "main", "title": "Reflex POC Tauri", "width": 405, "height": 720, @@ -16,10 +26,11 @@ } ], "security": { - "csp": "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' ipc: http://ipc.localhost" + "csp": "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' ipc: http://ipc.localhost http://localhost:1432 ws://localhost:1433" } }, "bundle": { - "active": false + "active": false, + "icon": ["icons/icon.png"] } } diff --git a/crates/apps/game-reflex-poc-tauri/tsconfig.json b/crates/apps/game-reflex-poc-tauri/tsconfig.json new file mode 100644 index 0000000..47f16ea --- /dev/null +++ b/crates/apps/game-reflex-poc-tauri/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["frontend/**/*.ts", "vite.config.ts"] +} diff --git a/crates/apps/game-reflex-poc-tauri/vite.config.ts b/crates/apps/game-reflex-poc-tauri/vite.config.ts new file mode 100644 index 0000000..862bf8a --- /dev/null +++ b/crates/apps/game-reflex-poc-tauri/vite.config.ts @@ -0,0 +1,36 @@ +// file: crates/apps/game-reflex-poc-tauri/vite.config.ts +// version: 1 + +import { fileURLToPath } from "node:url"; +import { resolve } from "node:path"; +import { defineConfig, normalizePath } from "vite"; + +const appRoot = fileURLToPath(new URL(".", import.meta.url)); +const frontendRoot = normalizePath(resolve(appRoot, "frontend")); +const frontendDist = normalizePath(resolve(appRoot, "../../../builds/sasedev-games/game-reflex-poc-tauri/dist")); +const devHost = process.env.TAURI_DEV_HOST; + +export default defineConfig({ + clearScreen: false, + root: frontendRoot, + publicDir: false, + build: { + outDir: frontendDist, + emptyOutDir: true, + minify: true, + sourcemap: false, + }, + server: { + port: 1432, + strictPort: true, + host: devHost || false, + ws: { + protocol: "ws", + host: devHost || "localhost", + port: 1433, + }, + watch: { + ignored: ["**/src/**"], + }, + }, +}); diff --git a/crates/apps/game-reflex-poc-wasm/Cargo.toml b/crates/apps/game-reflex-poc-wasm/Cargo.toml new file mode 100644 index 0000000..a0d6207 --- /dev/null +++ b/crates/apps/game-reflex-poc-wasm/Cargo.toml @@ -0,0 +1,22 @@ +# file: crates/apps/game-reflex-poc-wasm/Cargo.toml +# version: 1 + +[package] +name = "game-reflex-poc-wasm" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +authors.workspace = true +publish.workspace = true + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +engine-v1-common = { path = "../../engines/engine-v1-common" } +game-reflex-poc = { path = "../../games/game-reflex-poc" } +wasm-bindgen.workspace = true + +[lints] +workspace = true diff --git a/crates/apps/game-reflex-poc-wasm/src/lib.rs b/crates/apps/game-reflex-poc-wasm/src/lib.rs new file mode 100644 index 0000000..3ce0027 --- /dev/null +++ b/crates/apps/game-reflex-poc-wasm/src/lib.rs @@ -0,0 +1,13 @@ +// file: crates/apps/game-reflex-poc-wasm/src/lib.rs +// version: 1 + +//! WebAssembly adapter facade for the Reflex POC. + +#![forbid(unsafe_code)] +#![deny(unreachable_pub)] +#![warn(missing_docs)] + +mod runtime; + +/// Re-export of the Reflex WebAssembly runtime adapter. +pub use self::runtime::ReflexWasmGame; diff --git a/crates/apps/game-reflex-poc-tauri/src/wasm_runtime.rs b/crates/apps/game-reflex-poc-wasm/src/runtime.rs similarity index 86% rename from crates/apps/game-reflex-poc-tauri/src/wasm_runtime.rs rename to crates/apps/game-reflex-poc-wasm/src/runtime.rs index 94ebc5a..ce909fd 100644 --- a/crates/apps/game-reflex-poc-tauri/src/wasm_runtime.rs +++ b/crates/apps/game-reflex-poc-wasm/src/runtime.rs @@ -1,7 +1,7 @@ -// file: crates/apps/game-reflex-poc-tauri/src/wasm_runtime.rs +// file: crates/apps/game-reflex-poc-wasm/src/runtime.rs // version: 1 -/// WebAssembly-owned Reflex game state consumed by the local Tauri WebView. +/// WebAssembly-owned Reflex game state consumed by the Tauri WebView. #[wasm_bindgen::prelude::wasm_bindgen] pub struct ReflexWasmGame { runner: engine_v1_common::FixedStepRunner, @@ -10,7 +10,7 @@ pub struct ReflexWasmGame { #[wasm_bindgen::prelude::wasm_bindgen] impl ReflexWasmGame { - /// Creates a fresh Reflex POC session using the same gameplay crate as native Desktop. + /// Creates a fresh Reflex POC session using the shared gameplay crate. #[wasm_bindgen::prelude::wasm_bindgen(constructor)] pub fn new() -> Self { return Self { @@ -65,7 +65,7 @@ impl ReflexWasmGame { return count; } - /// Returns one rectangle's normalized left coordinate or `-1.0` if the index is absent. + /// Returns one rectangle's normalized left coordinate or `-1.0` if absent. pub fn rectangle_x(&self, index: u32) -> f32 { return match self.rectangle(index) { Some(rectangle) => rectangle.rect().x(), @@ -73,7 +73,7 @@ impl ReflexWasmGame { }; } - /// Returns one rectangle's normalized top coordinate or `-1.0` if the index is absent. + /// Returns one rectangle's normalized top coordinate or `-1.0` if absent. pub fn rectangle_y(&self, index: u32) -> f32 { return match self.rectangle(index) { Some(rectangle) => rectangle.rect().y(), @@ -81,7 +81,7 @@ impl ReflexWasmGame { }; } - /// Returns one rectangle's normalized width or `0.0` if the index is absent. + /// Returns one rectangle's normalized width or `0.0` if absent. pub fn rectangle_width(&self, index: u32) -> f32 { return match self.rectangle(index) { Some(rectangle) => rectangle.rect().width(), @@ -89,7 +89,7 @@ impl ReflexWasmGame { }; } - /// Returns one rectangle's normalized height or `0.0` if the index is absent. + /// Returns one rectangle's normalized height or `0.0` if absent. pub fn rectangle_height(&self, index: u32) -> f32 { return match self.rectangle(index) { Some(rectangle) => rectangle.rect().height(), @@ -97,7 +97,7 @@ impl ReflexWasmGame { }; } - /// Returns one rectangle's red channel or zero if the index is absent. + /// Returns one rectangle's red channel or zero if absent. pub fn rectangle_red(&self, index: u32) -> u8 { return match self.rectangle(index) { Some(rectangle) => rectangle.color().red(), @@ -105,7 +105,7 @@ impl ReflexWasmGame { }; } - /// Returns one rectangle's green channel or zero if the index is absent. + /// Returns one rectangle's green channel or zero if absent. pub fn rectangle_green(&self, index: u32) -> u8 { return match self.rectangle(index) { Some(rectangle) => rectangle.color().green(), @@ -113,7 +113,7 @@ impl ReflexWasmGame { }; } - /// Returns one rectangle's blue channel or zero if the index is absent. + /// Returns one rectangle's blue channel or zero if absent. pub fn rectangle_blue(&self, index: u32) -> u8 { return match self.rectangle(index) { Some(rectangle) => rectangle.color().blue(), diff --git a/deltas/0.1.0/1-alpha.2.fix.2.delete.txt b/deltas/0.1.0/1-alpha.2.fix.2.delete.txt new file mode 100644 index 0000000..51cab41 --- /dev/null +++ b/deltas/0.1.0/1-alpha.2.fix.2.delete.txt @@ -0,0 +1,3 @@ +Tauri/game-reflex-poc +Tauri/README.md +crates/apps/game-reflex-poc-tauri/src/wasm_runtime.rs diff --git a/deltas/0.1.0/1-alpha.2.fix.2.md b/deltas/0.1.0/1-alpha.2.fix.2.md new file mode 100644 index 0000000..683eac6 --- /dev/null +++ b/deltas/0.1.0/1-alpha.2.fix.2.md @@ -0,0 +1,154 @@ + + + +# Delta 0.1.0-1-alpha.2.fix.2 + +## Base + +Base déclarée : `0.1.0-1-alpha.2.fix.1`, techniquement compilable mais architecture Tauri/WASM rejetée avant validation fonctionnelle. + +## Réorganisation + +Le POC est réaligné sur le modèle des apps Desk KSP. + +### Tauri + +```text +crates/apps/game-reflex-poc-tauri/ +├── src/lib.rs +├── src/tauri.rs +├── src/runtime.rs +├── src/main.rs +├── frontend/ +├── capabilities/ +├── package.json +├── tsconfig.json +├── vite.config.ts +└── tauri.conf.json +``` + +`lib.rs` reste une façade. `tauri.rs` assemble Tauri et expose les commandes qui délèguent aux modules propriétaires. + +### WASM + +```text +crates/apps/game-reflex-poc-wasm/ +├── src/lib.rs +└── src/runtime.rs +``` + +La crate WASM ne dépend pas de Tauri. La crate Tauri ne compile plus elle-même en `wasm32`. + +### Frontend + +Le frontend utilise Vite + TypeScript. + +Le hook Tauri `beforeDevCommand` exécute : + +```text +python3 ../../../scripts/build_reflex_tauri_wasm.py && npm run dev +``` + +Le hook `beforeBuildCommand` exécute le build WASM puis `npm run build`. + +`npm run build` n'est donc pas une gate manuelle indépendante. + +### Logging + +Backend : + +```text +tracing +tauri-plugin-tracing +``` + +Frontend : + +```text +@fltsci/tauri-plugin-tracing +``` + +La capability inclut : + +```text +tracing:default +``` + +Le frontend trace startup, chargement WASM, pointeur, score et erreurs via le plugin. + +## Suppressions nécessaires + +Un ZIP delta ne supprime pas les anciens fichiers lors d'un simple overlay. + +Après extraction du delta, appliquer la liste : + +```bash +while IFS= read -r path; do + rm -rf -- "$path" +done < deltas/0.1.0/1-alpha.2.fix.2.delete.txt +``` + +Cette suppression retire uniquement l'ancien prototype statique et l'ancien module WASM interne à la crate Tauri. + +## Installation frontend + +Depuis : + +```bash +cd crates/apps/game-reflex-poc-tauri +npm install +cd ../../.. +``` + +## Validation Rust + +```bash +cargo fmt --all +cargo fmt --all -- --check + +python3 scripts/audit_rust_workspace_rules.py +python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates Android deltas history + +cargo check --workspace +cargo clippy --workspace --all-targets --all-features -- -D warnings + +cargo test -p engine-v1-platform-api --all-targets --all-features +cargo test -p game-reflex-poc --all-targets --all-features +cargo test -p game-reflex-poc-wasm --all-targets --all-features +``` + +## Gate WASM + +```bash +python3 scripts/build_reflex_tauri_wasm.py + +test -f crates/apps/game-reflex-poc-tauri/frontend/wasm/game_reflex_poc_wasm.js +test -f crates/apps/game-reflex-poc-tauri/frontend/wasm/game_reflex_poc_wasm_bg.wasm +``` + +## Smoke Tauri + +Depuis la crate : + +```bash +cd crates/apps/game-reflex-poc-tauri +cargo tauri dev +``` + +Tauri lance lui-même Vite et le build WASM. + +Critères : + +- fenêtre locale 405 × 720 ; +- frontend Vite/TypeScript ; +- cible Reflex visible ; +- clic comptabilisé et cible déplacée ; +- score visible ; +- traces frontend visibles via le plugin tracing ; +- aucun site distant. + +## Transition + +Si les gates et le smoke passent, `0.1.0-1-alpha.2.fix.2` valide le POC restructuré. + +En cas d'échec imputable au projet, produire `0.1.0-1-alpha.2.fix.3`. diff --git a/docs/000-README.md b/docs/000-README.md index 4b4c509..fd93b5b 100644 --- a/docs/000-README.md +++ b/docs/000-README.md @@ -1,5 +1,5 @@ - + # Documentation games.sasedev diff --git a/docs/architecture/001-WORKSPACE_ARCHITECTURE.md b/docs/architecture/001-WORKSPACE_ARCHITECTURE.md index 02b724b..7db7f7f 100644 --- a/docs/architecture/001-WORKSPACE_ARCHITECTURE.md +++ b/docs/architecture/001-WORKSPACE_ARCHITECTURE.md @@ -1,5 +1,5 @@ - + # Architecture du workspace @@ -21,13 +21,12 @@ games.sasedev/ │ └── apps/ │ ├── game-reflex-poc-desktop/ │ ├── game-snake-poc-desktop/ -│ └── game-reflex-poc-tauri/ +│ ├── game-reflex-poc-tauri/ +│ └── game-reflex-poc-wasm/ ├── assets/ │ ├── common/ │ ├── game-reflex-poc/ │ └── game-snake-poc/ -├── Tauri/ -│ └── game-reflex-poc/ ├── Android/ │ ├── common/ │ ├── game-reflex-poc/ @@ -80,3 +79,5 @@ La variante Tauri est distincte du runner SDL3 natif. - un binaire natif Tauri qui héberge la WebView locale. Le frontend statique sous `Tauri/game-reflex-poc/frontend/` pilote un Canvas et appelle l'adaptateur WASM. Les règles Reflex restent exclusivement dans `game-reflex-poc`. + +La crate Tauri suit le modèle des apps Desk KSP : façade `lib.rs`, pont `tauri.rs`, modules propriétaires séparés et frontend Vite/TypeScript local à la crate. diff --git a/docs/development/008-WASM_TAURI_POC.md b/docs/development/008-WASM_TAURI_POC.md index db9ce85..ce80b89 100644 --- a/docs/development/008-WASM_TAURI_POC.md +++ b/docs/development/008-WASM_TAURI_POC.md @@ -1,5 +1,5 @@ - + # POC WebAssembly embarqué dans Tauri @@ -111,3 +111,37 @@ crates/apps/game-reflex-poc-tauri/icons/icon.png ``` Il s'agit d'une icône minimale de développement ; l'identité graphique définitive viendra plus tard. + +## Réorganisation fix.2 + +La crate Tauri et la crate WASM sont désormais séparées. + +```text +crates/apps/game-reflex-poc-tauri/ +├── src/lib.rs +├── src/tauri.rs +├── src/runtime.rs +├── src/main.rs +├── frontend/ +├── package.json +├── tsconfig.json +├── vite.config.ts +└── tauri.conf.json + +crates/apps/game-reflex-poc-wasm/ +├── src/lib.rs +└── src/runtime.rs +``` + +`lib.rs` joue le rôle de façade. `tauri.rs` contient le pont Web/Rust et l'assemblage Tauri. Les fonctions métier/runtime sont portées par leurs modules puis appelées par les commandes Tauri. + +Le frontend utilise Vite + TypeScript. Le build WASM reste séparé et produit les bindings `wasm-bindgen` dans `frontend/wasm/`. + +Le logging est unifié avec `tracing` : + +- `tracing` côté Rust ; +- `tauri-plugin-tracing` côté Tauri ; +- `@fltsci/tauri-plugin-tracing` côté TypeScript ; +- capability `tracing:default`. + +Les événements frontend utiles sont donc remontés dans le même système de traces que le backend Rust. diff --git a/docs/rules/FILE_CONTRACTS.md b/docs/rules/FILE_CONTRACTS.md index 64b17c5..77028bb 100644 --- a/docs/rules/FILE_CONTRACTS.md +++ b/docs/rules/FILE_CONTRACTS.md @@ -1,5 +1,5 @@ - + # Contrats des fichiers principaux @@ -27,6 +27,8 @@ ## Tauri -- `Tauri//frontend/` contient le frontend Web statique versionné d'une variante Tauri. +- `crates/apps/-tauri/frontend/` contient le frontend Vite/TypeScript versionné. +- `crates/apps/-tauri/src/lib.rs` est la façade Rust de l'app Tauri. +- `crates/apps/-tauri/src/tauri.rs` assemble Tauri et porte le pont Web/Rust. +- `crates/apps/-wasm/` contient l'adaptation WebAssembly distincte. - Les bindings JavaScript et modules `.wasm` générés restent ignorés par Git. -- Le backend Rust Tauri reste sous `crates/apps/`. diff --git a/docs/rules/RULES_COMMANDS.md b/docs/rules/RULES_COMMANDS.md index 6519cf3..b0152f4 100644 --- a/docs/rules/RULES_COMMANDS.md +++ b/docs/rules/RULES_COMMANDS.md @@ -1,5 +1,5 @@ - + # Règles d'exécution des commandes @@ -53,10 +53,12 @@ - **CMD-WEB-001** — Aucun gestionnaire de paquets JavaScript ni build Web n'est exécuté tant qu'un frontend Web réel n'a pas été introduit dans le dépôt. - **CMD-WEB-002** — Lorsqu'une cible Web existe, ses commandes de build et test sont documentées avant d'être ajoutées aux gates. -- **CMD-WEB-003** — Le POC Tauri/WASM utilise `scripts/build_reflex_tauri_wasm.py`; aucun gestionnaire de paquets JavaScript ni serveur Web externe n'est requis pour cette variante. +- **CMD-WEB-003** — Le POC Tauri/WASM utilise `scripts/build_reflex_tauri_wasm.py` pour WASM et Vite/TypeScript pour le frontend local ; aucun site distant n'est requis. - **CMD-WEB-004** — Les fichiers produits par `wasm-bindgen` sont générés localement et ne sont pas commités. ## Git et fichiers générés - **CMD-GIT-001** — Les commandes Git destructives (`reset --hard`, nettoyage forcé, réécriture non demandée) ne sont jamais utilisées pour remettre artificiellement le workspace en état. - **CMD-GIT-002** — Les fichiers générés ne sont pas commités sauf contrat explicite du dépôt ou exigence de distribution. + +- **CMD-WEB-005** — `npm run dev` et `npm run build` du frontend Tauri sont pilotés par les hooks Tauri ; ils ne constituent pas des gates manuelles indépendantes. diff --git a/docs/rules/RULES_PROJECT.md b/docs/rules/RULES_PROJECT.md index 1edd020..8147c6d 100644 --- a/docs/rules/RULES_PROJECT.md +++ b/docs/rules/RULES_PROJECT.md @@ -1,5 +1,5 @@ - + # Règles spécifiques games.sasedev @@ -49,7 +49,7 @@ - **GAME-PLATFORM-004** — Ads et Billing sont des capacités optionnelles ; le gameplay reste fonctionnel lorsqu'elles sont disponibles, désactivées ou non supportées. - **GAME-PLATFORM-005** — Un backend de monétisation est spécifique à sa plateforme et à sa distribution ; une intégration Android n'est pas réutilisée implicitement sur Web ou Desktop. - **GAME-PLATFORM-006** — Le runner Desktop natif SDL3 est la forme Desktop par défaut. Une variante Tauri peut coexister uniquement lorsqu'un besoin explicite le justifie et consomme la même crate lib de jeu. -- **GAME-PLATFORM-007** — Une variante Tauri conserve son backend Rust sous `crates/apps/`; son frontend Web statique réside sous `Tauri//` et ne duplique pas le gameplay Rust. +- **GAME-PLATFORM-007** — Une variante Tauri conserve son backend Rust sous `crates/apps/`; son frontend Vite/TypeScript réside dans la même crate sous `frontend/` et ne duplique pas le gameplay Rust. - **GAME-PLATFORM-008** — Le gameplay compilé en WebAssembly est adapté par une frontière dédiée ; le code JavaScript orchestre la WebView, les événements et le rendu Canvas mais ne réimplémente pas les règles du jeu. ## POC @@ -63,3 +63,7 @@ - **GAME-TRACE-002** — `tracing-subscriber` compose les subscribers applicatifs et de test ; une librairie métier ne configure pas silencieusement le subscriber global. - **GAME-TRACE-003** — `tracing-appender` est utilisé lorsque l’écriture non bloquante ou les fichiers de logs deviennent nécessaires ; le guard associé reste vivant pendant toute la durée utile. - **GAME-TRACE-004** — La configuration de logging commune réside dans une crate transverse et ne doit pas être dupliquée par jeu. + +- **GAME-PLATFORM-009** — Une adaptation WebAssembly réutilisable est une crate dédiée distincte de la crate Tauri. +- **GAME-PLATFORM-010** — Dans une app Tauri, `lib.rs` reste une façade/reexport ; `tauri.rs` assemble Tauri et expose les commandes qui délèguent aux modules propriétaires. +- **GAME-PLATFORM-011** — Les traces frontend Tauri passent par `@fltsci/tauri-plugin-tracing` vers `tauri-plugin-tracing`/`tracing`; les `console.*` applicatifs directs sont interdits hors fallback interne. diff --git a/scripts/build_reflex_tauri_wasm.py b/scripts/build_reflex_tauri_wasm.py index 5a6f374..88c5168 100644 --- a/scripts/build_reflex_tauri_wasm.py +++ b/scripts/build_reflex_tauri_wasm.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 # file: scripts/build_reflex_tauri_wasm.py -# version: 1 +# version: 2 -"""Build the Reflex WASM adapter and generate browser bindings for the local Tauri frontend.""" +"""Build the dedicated Reflex WASM adapter for the Vite/Tauri frontend.""" from __future__ import annotations @@ -12,15 +12,15 @@ import sys def main() -> int: - """Build the WASM library and run wasm-bindgen into the committed Tauri frontend directory.""" + """Build the WASM crate and generate browser bindings inside the Tauri frontend.""" root = pathlib.Path(__file__).resolve().parent.parent target_dir = root.parent / "builds" / "sasedev-games" / "target" - wasm_path = target_dir / "wasm32-unknown-unknown" / "debug" / "game_reflex_poc_tauri.wasm" - output_dir = root / "Tauri" / "game-reflex-poc" / "frontend" + wasm_path = target_dir / "wasm32-unknown-unknown" / "debug" / "game_reflex_poc_wasm.wasm" + output_dir = root / "crates" / "apps" / "game-reflex-poc-tauri" / "frontend" / "wasm" build = subprocess.run( - ["cargo", "build", "-p", "game-reflex-poc-tauri", "--lib", "--target", "wasm32-unknown-unknown"], + ["cargo", "build", "-p", "game-reflex-poc-wasm", "--target", "wasm32-unknown-unknown"], cwd=root, check=False, ) @@ -30,6 +30,7 @@ def main() -> int: print(f"WASM artifact not found: {wasm_path}", file=sys.stderr) return 2 + output_dir.mkdir(parents=True, exist_ok=True) bindings = subprocess.run( [ "wasm-bindgen", @@ -39,8 +40,7 @@ def main() -> int: "--out-dir", str(output_dir), "--out-name", - "game_reflex_poc_tauri", - "--no-typescript", + "game_reflex_poc_wasm", ], cwd=root, check=False, @@ -48,7 +48,7 @@ def main() -> int: if bindings.returncode != 0: return bindings.returncode - print(f"Reflex Tauri WASM frontend generated in {output_dir}") + print(f"Reflex WASM bindings generated in {output_dir}") return 0