0.3.1-0-pre.2

This commit is contained in:
2026-09-20 21:30:12 +02:00
parent ba718ce4ec
commit 51371c1b24
29 changed files with 981 additions and 21 deletions

View File

@@ -0,0 +1,34 @@
# file: crates/apps/game-snake-poc-tauri/Cargo.toml
# version: 1
[package]
name = "game-snake-poc-tauri"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
authors.workspace = true
publish.workspace = true
[lib]
name = "game_snake_poc_tauri_lib"
path = "src/lib.rs"
crate-type = ["staticlib", "cdylib", "rlib"]
[[bin]]
name = "game-snake-poc-tauri"
path = "src/main.rs"
[build-dependencies]
tauri-build.workspace = true
[dependencies]
engine-v1-platform-api = { path = "../../engines/engine-v1-platform-api" }
game-logging-lib = { path = "../../common/game-logging-lib" }
serde.workspace = true
tauri.workspace = true
tauri-plugin-tracing.workspace = true
tracing.workspace = true
[lints]
workspace = true

View File

@@ -0,0 +1,25 @@
<!-- file: crates/apps/game-snake-poc-tauri/README.md -->
<!-- version: 1 -->
# game-snake-poc-tauri
Host Tauri Android du POC Snake. Cette application réutilise le gameplay Rust de `game-snake-poc` et l'unique adapter WebAssembly `game-snake-poc-wasm` ; elle ne possède aucune règle de jeu.
## Responsabilités
La crate possède uniquement la frontière Tauri : initialisation du logging Rust, assemblage/plugins/commandes Tauri, contrat minimal de provenance native et frontend Vite/TypeScript exécuté dans la WebView Android.
L'organisation Rust suit le modèle des applications Desk KSP :
- `src/lib.rs` reste une façade et réexporte les éléments partagés au crate-root ;
- `src/tauri.rs` assemble Tauri et fait déléguer les commandes aux modules propriétaires ;
- `src/runtime.rs` possède le petit contrat runtime/provenance ;
- `frontend/` contient le host WebView et consomme les bindings générés de `game-snake-poc-wasm`.
Le frontend minimal de `0.3.1-0-pre.2` valide uniquement le démarrage Tauri, le bridge Rust, le tracing et le chargement du même adapter WASM que le Web direct. Canvas, contrôles, assets et lifecycle complet sont prévus dans les tranches suivantes.
## Build
Les hooks `beforeDevCommand` et `beforeBuildCommand` de Tauri possèdent le build WASM et Vite. Aucun orchestrateur Python n'est utilisé. Les bindings WASM, la distribution Vite, les caches, `node_modules/` et le projet Android généré restent hors des sources livrées ou dans des chemins générés ignorés.
Voir [`USAGE.md`](USAGE.md) pour l'initialisation locale et les smokes Android.

View File

@@ -0,0 +1,42 @@
<!-- file: crates/apps/game-snake-poc-tauri/USAGE.md -->
<!-- version: 1 -->
# Utilisation — Snake Tauri Android
## Prérequis locaux
Le host utilise Cargo/Rust, `cargo tauri`, Node/npm, `wasm-bindgen`, Java et le SDK Android. Le dépôt attend `ANDROID_HOME` vers le SDK Android et des NDK side-by-side sous `$ANDROID_HOME/ndk/<version>`.
`NDK_HOME` n'est pas imposé globalement par le projet. Si une commande Tauri locale le réclame, définir ponctuellement `NDK_HOME` vers une version NDK exacte installée sous `$ANDROID_HOME/ndk/`; ne pas sélectionner implicitement une version lorsque plusieurs NDK coexistent.
`JAVA_HOME` n'est pas imposé tant que la CLI Tauri résout correctement `java` depuis l'environnement. En cas de diagnostic explicite de la CLI, fixer alors `JAVA_HOME` vers le JDK réellement choisi.
Pour les cibles actuellement disponibles, `aarch64-linux-android` permet le smoke sur l'appareil ARM64 et `x86_64-linux-android` celui des AVD x86_64. Les autres ABI ne sont pas requises pour `0-pre.2`.
## Initialisation locale
Installer les dépendances frontend dans la crate :
```bash
(cd crates/apps/game-snake-poc-tauri && npm install)
```
Initialiser une fois le projet Android généré par Tauri :
```bash
(cd crates/apps/game-snake-poc-tauri && cargo tauri android init)
```
Le répertoire `gen/` produit par Tauri est généré et ignoré ; il n'appartient pas aux deltas.
## Smoke Android de développement
Lancer ensuite :
```bash
(cd crates/apps/game-snake-poc-tauri && cargo tauri android dev)
```
Choisir l'appareil réel ou l'AVD voulu lorsque la CLI le demande. Le frontend doit afficher une provenance `android / tauri-webview / wasm / unknown / unknown` et les événements de démarrage doivent apparaître dans le tracing/logcat.
Ne pas utiliser `npm run dev` ou `npm run build` comme gate manuelle : ces scripts sont des hooks internes possédés par Tauri. `cargo tauri android build` reste réservé au jalon de packaging prévu par le plan, sauf diagnostic explicite.

View File

@@ -0,0 +1,7 @@
// file: crates/apps/game-snake-poc-tauri/build.rs
// version: 1
fn main() {
tauri_build::build();
return;
}

View File

@@ -0,0 +1,12 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Default capability for the Snake Tauri Android POC",
"windows": [
"main"
],
"permissions": [
"core:default",
"tracing:default"
]
}

View File

@@ -0,0 +1,58 @@
/* file: crates/apps/game-snake-poc-tauri/frontend/css/main.css */
/* version: 1 */
:root {
font-family: system-ui, sans-serif;
color-scheme: dark;
background: #101418;
color: #f4f7f8;
}
* {
box-sizing: border-box;
}
html,
body {
min-height: 100%;
margin: 0;
}
body {
min-height: 100vh;
display: grid;
place-items: center;
padding: max(1.5rem, env(safe-area-inset-top)) max(1.5rem, env(safe-area-inset-right)) max(1.5rem, env(safe-area-inset-bottom))
max(1.5rem, env(safe-area-inset-left));
}
.bootstrap-panel {
width: min(100%, 34rem);
padding: 1.5rem;
border: 1px solid currentColor;
border-radius: 0.75rem;
}
.eyebrow {
margin: 0 0 0.5rem;
opacity: 0.7;
}
h1 {
margin: 0 0 1rem;
font-size: clamp(1.6rem, 7vw, 2.4rem);
}
#runtime-status {
margin: 0 0 0.75rem;
}
#runtime-provenance {
display: block;
overflow-wrap: anywhere;
font-family: ui-monospace, monospace;
}
body[data-runtime-state="error"] #runtime-status {
font-weight: 700;
}

View File

@@ -0,0 +1,17 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>Snake POC Tauri</title>
</head>
<body>
<main class="bootstrap-panel" aria-labelledby="runtime-title">
<p class="eyebrow">games.sasedev</p>
<h1 id="runtime-title">Snake — Tauri Android</h1>
<p id="runtime-status">Initialisation du runtime…</p>
<output id="runtime-provenance" aria-live="polite">provenance en attente</output>
</main>
<script type="module" src="/ts/main.ts"></script>
</body>
</html>

View File

@@ -0,0 +1,20 @@
// file: crates/apps/game-snake-poc-tauri/frontend/ts/bridge.ts
// version: 1
import { invoke } from "@tauri-apps/api/core";
export interface RuntimeDescriptor {
deviceClass: string;
executionModel: string;
inputProfile: string;
platformFamily: string;
runtimeHost: string;
}
export async function getRuntimeDescriptor(): Promise<RuntimeDescriptor> {
return await invoke<RuntimeDescriptor>("get_runtime_descriptor");
}
export async function reportFrontendReady(): Promise<void> {
await invoke("frontend_ready");
}

View File

@@ -0,0 +1,16 @@
// file: crates/apps/game-snake-poc-tauri/frontend/ts/logging.ts
// version: 1
import { attachConsole, error, info } from "@fltsci/tauri-plugin-tracing";
export async function initializeFrontendTracing(): Promise<void> {
await attachConsole();
}
export function frontendInfo(message: string): void {
info(message);
}
export function frontendError(message: string): void {
error(message);
}

View File

@@ -0,0 +1,56 @@
// file: crates/apps/game-snake-poc-tauri/frontend/ts/main.ts
// version: 1
import init, { SnakeWasmGame } from "@snake-wasm";
import "../css/main.css";
import { getRuntimeDescriptor, reportFrontendReady } from "./bridge";
import { frontendError, frontendInfo, initializeFrontendTracing } from "./logging";
function requiredElement<T extends HTMLElement>(identifier: string): T {
const element = document.getElementById(identifier);
if (element === null) {
throw new Error(`Élément frontend requis absent : ${identifier}`);
}
return element as T;
}
async function bootstrap(): Promise<void> {
await initializeFrontendTracing();
frontendInfo("Snake Tauri frontend bootstrap started");
const descriptor = await getRuntimeDescriptor();
await init();
const game = new SnakeWasmGame();
const configured = game.configure_runtime_provenance(
descriptor.platformFamily,
descriptor.runtimeHost,
descriptor.deviceClass,
descriptor.inputProfile,
);
if (!configured) {
game.free();
throw new Error("La provenance Tauri Android n'a pas été acceptée par l'adapter Snake WASM.");
}
const status = requiredElement<HTMLParagraphElement>("runtime-status");
const provenance = requiredElement<HTMLOutputElement>("runtime-provenance");
status.textContent = "Runtime minimal initialisé. Le Canvas et les contrôles arrivent dans 0-pre.3.";
provenance.value = [
game.provenance_platform_family(),
game.provenance_runtime_host(),
game.provenance_execution_model(),
game.provenance_device_class(),
game.provenance_input_profile(),
].join(" / ");
window.addEventListener("pagehide", () => game.free(), { once: true });
await reportFrontendReady();
frontendInfo(`Snake Tauri frontend ready: ${provenance.value}`);
}
void bootstrap().catch(reason => {
const message = reason instanceof Error ? reason.message : String(reason);
document.body.dataset.runtimeState = "error";
const status = document.getElementById("runtime-status");
if (status !== null) {
status.textContent = `Échec d'initialisation : ${message}`;
}
frontendError(`Snake Tauri frontend bootstrap failed: ${message}`);
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 B

View File

@@ -0,0 +1,21 @@
{
"name": "game-snake-poc-tauri",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"wasm:dev": "cargo build -p game-snake-poc-wasm --target wasm32-unknown-unknown && wasm-bindgen ../../../../builds/sasedev-games/target/wasm32-unknown-unknown/debug/game_snake_poc_wasm.wasm --target web --out-dir ../../../../builds/sasedev-games/game-snake-poc-tauri/wasm --out-name game_snake_poc_wasm",
"wasm:build": "cargo build -p game-snake-poc-wasm --release --target wasm32-unknown-unknown && wasm-bindgen ../../../../builds/sasedev-games/target/wasm32-unknown-unknown/release/game_snake_poc_wasm.wasm --target web --out-dir ../../../../builds/sasedev-games/game-snake-poc-tauri/wasm --out-name game_snake_poc_wasm",
"dev": "npm run wasm:dev && vite",
"build": "npm run wasm:build && tsc && vite build"
},
"dependencies": {
"@fltsci/tauri-plugin-tracing": "^0.3",
"@tauri-apps/api": "^2.11"
},
"devDependencies": {
"@types/node": "^26.1",
"typescript": "^7.0",
"vite": "^8.2"
}
}

View File

@@ -0,0 +1,21 @@
// file: crates/apps/game-snake-poc-tauri/src/lib.rs
// version: 1
//! Tauri Android application facade for the Snake WebAssembly POC.
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
#![warn(missing_docs)]
mod runtime;
mod tauri;
/// Runs the Snake Tauri application.
pub use self::tauri::run;
/// Runtime descriptor exposed by the native Tauri bridge.
pub(crate) use self::runtime::RuntimeDescriptorDto;
/// Records that the Tauri frontend completed its minimal startup sequence.
pub(crate) use self::runtime::record_frontend_ready;
/// Returns the canonical runtime descriptor for the Android WebView host.
pub(crate) use self::runtime::runtime_descriptor;

View File

@@ -0,0 +1,20 @@
// file: crates/apps/game-snake-poc-tauri/src/main.rs
// version: 1
//! Binary entry point for host-side Snake Tauri checks.
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
#![warn(missing_docs)]
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() -> std::process::ExitCode {
let result = game_snake_poc_tauri_lib::run();
return match result {
std::result::Result::Ok(()) => std::process::ExitCode::SUCCESS,
std::result::Result::Err(error) => {
eprintln!("Snake Tauri application error: {error}");
std::process::ExitCode::FAILURE
},
};
}

View File

@@ -0,0 +1,87 @@
// file: crates/apps/game-snake-poc-tauri/src/runtime.rs
// version: 1
/// Stable runtime dimensions exposed to the Tauri frontend.
#[derive(Clone, Debug, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct RuntimeDescriptorDto {
device_class: String,
execution_model: String,
input_profile: String,
platform_family: String,
runtime_host: String,
}
/// Returns the canonical runtime descriptor for the Android WebView host.
pub(crate) fn runtime_descriptor() -> RuntimeDescriptorDto {
let provenance = engine_v1_platform_api::RuntimeProvenance::new(
engine_v1_platform_api::DeviceClass::Unknown,
engine_v1_platform_api::ExecutionModel::Wasm,
engine_v1_platform_api::InputProfile::Unknown,
engine_v1_platform_api::PlatformFamily::Android,
engine_v1_platform_api::RuntimeHost::TauriWebView,
);
return RuntimeDescriptorDto {
device_class: device_class_label(provenance.device_class()).to_string(),
execution_model: execution_model_label(provenance.execution_model()).to_string(),
input_profile: input_profile_label(provenance.input_profile()).to_string(),
platform_family: platform_family_label(provenance.platform_family()).to_string(),
runtime_host: runtime_host_label(provenance.runtime_host()).to_string(),
};
}
/// Records that the Tauri frontend completed its minimal startup sequence.
pub(crate) fn record_frontend_ready() {
let descriptor = crate::runtime_descriptor();
tracing::info!(
target: "games::tauri::snake",
action = "frontend_ready",
platform_family = descriptor.platform_family,
runtime_host = descriptor.runtime_host,
execution_model = descriptor.execution_model,
"Snake Tauri frontend ready"
);
return;
}
fn device_class_label(value: engine_v1_platform_api::DeviceClass) -> &'static str {
return match value {
engine_v1_platform_api::DeviceClass::Desktop => "desktop",
engine_v1_platform_api::DeviceClass::Phone => "phone",
engine_v1_platform_api::DeviceClass::Tablet => "tablet",
engine_v1_platform_api::DeviceClass::Unknown => "unknown",
};
}
fn execution_model_label(value: engine_v1_platform_api::ExecutionModel) -> &'static str {
return match value {
engine_v1_platform_api::ExecutionModel::Native => "native",
engine_v1_platform_api::ExecutionModel::Wasm => "wasm",
};
}
fn input_profile_label(value: engine_v1_platform_api::InputProfile) -> &'static str {
return match value {
engine_v1_platform_api::InputProfile::Gamepad => "gamepad",
engine_v1_platform_api::InputProfile::KeyboardMouse => "keyboard-mouse",
engine_v1_platform_api::InputProfile::Mixed => "mixed",
engine_v1_platform_api::InputProfile::Touch => "touch",
engine_v1_platform_api::InputProfile::Unknown => "unknown",
};
}
fn platform_family_label(value: engine_v1_platform_api::PlatformFamily) -> &'static str {
return match value {
engine_v1_platform_api::PlatformFamily::Android => "android",
engine_v1_platform_api::PlatformFamily::Desktop => "desktop",
engine_v1_platform_api::PlatformFamily::Web => "web",
};
}
fn runtime_host_label(value: engine_v1_platform_api::RuntimeHost) -> &'static str {
return match value {
engine_v1_platform_api::RuntimeHost::Browser => "browser",
engine_v1_platform_api::RuntimeHost::Native => "native",
engine_v1_platform_api::RuntimeHost::TauriWebView => "tauri-webview",
};
}

View File

@@ -0,0 +1,65 @@
// file: crates/apps/game-snake-poc-tauri/src/tauri.rs
// version: 1
//! Tauri runtime assembly and Web/Rust commands for the Snake Android POC.
#[tauri::command]
fn get_runtime_descriptor() -> crate::RuntimeDescriptorDto {
return crate::runtime_descriptor();
}
#[tauri::command]
fn frontend_ready() {
crate::record_frontend_ready();
return;
}
fn configure_plugins(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
let tracing_plugin = tauri_plugin_tracing::Builder::new().build::<tauri::Wry>();
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::Wry>) -> tauri::Builder<tauri::Wry> {
return builder.invoke_handler(tauri::generate_handler![get_runtime_descriptor, frontend_ready]);
}
/// Runs the Snake Tauri application.
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() -> std::result::Result<(), String> {
let logging_guard = game_logging_lib::init_console_tracing();
let _logging_guard = match logging_guard {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
tracing::info!(
target: "games::tauri::snake",
action = "runtime_start",
platform_family = "android",
runtime_host = "tauri-webview",
execution_model = "wasm",
"Starting Snake Tauri Android 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(()) => {
tracing::info!(
target: "games::tauri::snake",
action = "runtime_stop",
"Snake Tauri runtime stopped"
);
std::result::Result::Ok(())
},
std::result::Result::Err(error) => {
tracing::error!(
target: "games::tauri::snake",
action = "runtime_error",
error = error.to_string(),
"Snake Tauri runtime failed"
);
std::result::Result::Err(error.to_string())
},
};
}

View File

@@ -0,0 +1,41 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Snake POC Tauri",
"identifier": "com.sasedev.games.snake.tauri",
"build": {
"beforeDevCommand": {
"script": "npm run dev",
"cwd": "."
},
"devUrl": "http://localhost:1436",
"beforeBuildCommand": {
"script": "npm run build",
"cwd": "."
},
"frontendDist": "../../../../builds/sasedev-games/game-snake-poc-tauri/dist"
},
"app": {
"windows": [
{
"label": "main",
"url": "main.html",
"title": "Snake POC Tauri",
"width": 405,
"height": 720,
"minWidth": 320,
"minHeight": 480,
"resizable": true,
"center": true
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": false,
"icon": [
"icons/icon.png"
]
}
}

View File

@@ -0,0 +1,36 @@
{
"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,
"allowSyntheticDefaultImports": true,
"types": [
"vite/client",
"node"
],
"paths": {
"@snake-wasm": [
"../../../../builds/sasedev-games/game-snake-poc-tauri/wasm/game_snake_poc_wasm.d.ts"
]
}
},
"include": [
"frontend",
"vite.config.ts"
]
}

View File

@@ -0,0 +1,72 @@
// file: crates/apps/game-snake-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 repositoryRoot = normalizePath(resolve(appRoot, "../../.."));
const frontendRoot = normalizePath(resolve(appRoot, "frontend"));
const externalBuildRoot = normalizePath(resolve(repositoryRoot, "../builds/sasedev-games/game-snake-poc-tauri"));
const wasmRoot = normalizePath(resolve(externalBuildRoot, "wasm"));
const wasmModule = normalizePath(resolve(wasmRoot, "game_snake_poc_wasm.js"));
const frontendDist = normalizePath(resolve(externalBuildRoot, "dist"));
const viteCacheDir = normalizePath(resolve(externalBuildRoot, "vite-cache"));
const devHost = process.env.TAURI_DEV_HOST;
export default defineConfig({
base: "./",
cacheDir: viteCacheDir,
clearScreen: false,
root: frontendRoot,
publicDir: false,
input: {
main: normalizePath(resolve(frontendRoot, "main.html")),
},
resolve: {
alias: {
"@snake-wasm": wasmModule,
},
},
build: {
outDir: frontendDist,
emptyOutDir: true,
minify: true,
sourcemap: false,
cssCodeSplit: true,
rolldownOptions: {
output: {
entryFileNames: "js/[name]-[hash].js",
chunkFileNames: "js/chunks/[name]-[hash].js",
assetFileNames: assetInfo => {
const originalName = assetInfo.names[0] ?? "";
const extension = originalName.substring(originalName.lastIndexOf(".") + 1).toLowerCase();
if (extension === "css") {
return "css/[name]-[hash][extname]";
}
if (extension === "wasm") {
return "wasm/[name]-[hash][extname]";
}
return "otherassets/[name][extname]";
},
},
},
},
server: {
port: 1436,
strictPort: true,
host: devHost || false,
fs: {
allow: [appRoot, repositoryRoot, externalBuildRoot],
},
ws: {
protocol: "ws",
host: devHost || "localhost",
port: 1437,
},
watch: {
ignored: ["**/src/**"],
},
},
});