0.1.0-1-alpha.2-fix.3

This commit is contained in:
2026-09-17 00:29:56 +02:00
parent 37a00b3c2b
commit b31ffa74f5
17 changed files with 349 additions and 57 deletions

View File

@@ -1,5 +1,5 @@
# file: crates/apps/game-reflex-poc-tauri/Cargo.toml
# version: 3
# version: 4
[package]
name = "game-reflex-poc-tauri"
@@ -23,6 +23,7 @@ path = "src/main.rs"
tauri-build.workspace = true
[dependencies]
game-logging-lib = { path = "../../common/game-logging-lib" }
engine-v1-platform-api = { path = "../../engines/engine-v1-platform-api" }
tauri.workspace = true
tauri-plugin-tracing.workspace = true

View File

@@ -1,5 +1,5 @@
// file: crates/apps/game-reflex-poc-tauri/frontend/ts/bridge.ts
// version: 1
// version: 2
import { invoke } from "@tauri-apps/api/core";
@@ -10,3 +10,7 @@ export async function getRuntimeLabel(): Promise<string> {
export async function notifyFrontendReady(): Promise<void> {
await invoke("frontend_ready");
}
export async function closeMainWindow(): Promise<void> {
await invoke("close_main_window");
}

View File

@@ -1,5 +1,5 @@
// file: crates/apps/game-reflex-poc-tauri/frontend/ts/game.ts
// version: 1
// version: 2
import init, { ReflexWasmGame } from "../wasm/game_reflex_poc_wasm.js";
import { tracingDebug, tracingTrace } from "./logging";
@@ -37,7 +37,7 @@ function render(game: ReflexWasmGame, canvas: HTMLCanvasElement, context: Canvas
score.value = `Score : ${game.score()}`;
}
export async function startGame(canvas: HTMLCanvasElement, score: HTMLOutputElement): Promise<void> {
export async function startGame(canvas: HTMLCanvasElement, score: HTMLOutputElement): Promise<ReflexWasmGame> {
await init();
await tracingDebug("Reflex WASM module initialized");
const context = canvas.getContext("2d");
@@ -64,4 +64,5 @@ export async function startGame(canvas: HTMLCanvasElement, score: HTMLOutputElem
}
render(game, canvas, context, score);
window.requestAnimationFrame(frame);
return game;
}

View File

@@ -1,38 +1,98 @@
// file: crates/apps/game-reflex-poc-tauri/frontend/ts/logging.ts
// version: 1
// version: 2
import { attachConsole, debug, error, info, trace, warn } from "@fltsci/tauri-plugin-tracing";
import { attachConsole } from "@fltsci/tauri-plugin-tracing";
import { invoke } from "@tauri-apps/api/core";
let detachRustConsole: (() => void) | null = null;
export type FrontendLogLevel = "trace" | "debug" | "info" | "warn" | "error";
export type FrontendLogTarget = "frontend" | "main";
export async function initializeTracing(): Promise<void> {
detachRustConsole = await attachConsole();
await info("Reflex Tauri frontend tracing initialized", "games::tauri::reflex::frontend");
}
type ConsoleMethod = (...items: unknown[]) => void;
export async function tracingTrace(message: string): Promise<void> {
await trace(message, "games::tauri::reflex::frontend");
}
const originalConsole = {
trace: console.trace.bind(console),
debug: console.debug.bind(console),
info: console.info.bind(console),
warn: console.warn.bind(console),
error: console.error.bind(console),
};
export async function tracingDebug(message: string): Promise<void> {
await debug(message, "games::tauri::reflex::frontend");
}
export async function tracingInfo(message: string): Promise<void> {
await info(message, "games::tauri::reflex::frontend");
}
export async function tracingWarn(message: string): Promise<void> {
await warn(message, "games::tauri::reflex::frontend");
}
export async function tracingError(message: string): Promise<void> {
await error(message, "games::tauri::reflex::frontend");
}
export function detachTracingConsole(): void {
if (detachRustConsole !== null) {
detachRustConsole();
detachRustConsole = null;
function stringifyItem(item: unknown): string {
if (item instanceof Error) {
return item.stack ?? item.message;
}
if (typeof item === "string") {
return item;
}
try {
const serialized = JSON.stringify(item);
return serialized ?? String(item);
} catch {
return String(item);
}
}
function formatMessage(items: unknown[]): string {
return items.map(item => stringifyItem(item)).join(" ");
}
async function sendFrontendLog(level: FrontendLogLevel, targetId: FrontendLogTarget, message: string): Promise<void> {
await invoke("emit_frontend_log", {
payload: {
level,
targetId,
message,
},
});
}
function originalConsoleFor(level: FrontendLogLevel): ConsoleMethod {
return level === "trace"
? originalConsole.trace
: level === "debug"
? originalConsole.debug
: level === "info"
? originalConsole.info
: level === "warn"
? originalConsole.warn
: originalConsole.error;
}
export async function initializeTracing(): Promise<void> {
await attachConsole();
await emitFrontendLog("info", "main", "Reflex Tauri frontend tracing initialized");
}
export async function emitFrontendLog(level: FrontendLogLevel, targetId: FrontendLogTarget, ...items: unknown[]): Promise<void> {
const message = formatMessage(items);
originalConsoleFor(level)(message);
await sendFrontendLog(level, targetId, message);
}
function emitDetached(level: FrontendLogLevel, targetId: FrontendLogTarget, items: unknown[]): void {
const message = formatMessage(items);
originalConsoleFor(level)(message);
void sendFrontendLog(level, targetId, message).catch(caughtError => {
originalConsole.error("Reflex frontend logging bridge failed", caughtError);
});
}
export function tracingTrace(...items: unknown[]): void {
emitDetached("trace", "frontend", items);
}
export function tracingDebug(...items: unknown[]): void {
emitDetached("debug", "frontend", items);
}
export function tracingInfo(...items: unknown[]): void {
emitDetached("info", "frontend", items);
}
export function tracingWarn(...items: unknown[]): void {
emitDetached("warn", "frontend", items);
}
export function tracingError(...items: unknown[]): void {
emitDetached("error", "frontend", items);
}

View File

@@ -1,7 +1,7 @@
// file: crates/apps/game-reflex-poc-tauri/frontend/ts/main.ts
// version: 1
// version: 2
import { getRuntimeLabel, notifyFrontendReady } from "./bridge";
import { closeMainWindow, getRuntimeLabel, notifyFrontendReady } from "./bridge";
import { startGame } from "./game";
import { initializeTracing, tracingError, tracingInfo } from "./logging";
@@ -14,12 +14,23 @@ async function main(): Promise<void> {
throw new Error("Reflex Tauri frontend DOM is incomplete");
}
runtime.value = await getRuntimeLabel();
await startGame(canvas, score);
const game = await startGame(canvas, score);
window.addEventListener("keydown", event => {
if (event.key !== "Escape" || event.repeat) {
return;
}
event.preventDefault();
const shouldExit = game.quit_requested_escape();
tracingInfo(`quit requested source=escape decision=${shouldExit ? "exit" : "continue"}`);
if (shouldExit) {
void closeMainWindow().catch(caughtError => tracingError("Tauri close command failed", caughtError));
}
});
await notifyFrontendReady();
await tracingInfo("Reflex Tauri frontend started");
tracingInfo("Reflex Tauri frontend started");
}
void main().catch(async caughtError => {
void main().catch(caughtError => {
const message = caughtError instanceof Error ? caughtError.stack ?? caughtError.message : String(caughtError);
await tracingError(`Reflex Tauri frontend startup failed: ${message}`);
tracingError(`Reflex Tauri frontend startup failed: ${message}`);
});

View File

@@ -1,7 +1,7 @@
{
"name": "game-reflex-poc-tauri",
"private": true,
"version": "0.1.0-1-alpha.2.fix.2",
"version": "0.1.0-1-alpha.2.fix.3",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,5 @@
// file: crates/apps/game-reflex-poc-tauri/src/lib.rs
// version: 2
// version: 3
//! Tauri desktop application facade for the Reflex WebAssembly POC.
@@ -15,5 +15,7 @@ pub use self::tauri::run;
/// Records that the Vite/TypeScript frontend reached its ready state.
pub(crate) use self::runtime::record_frontend_ready;
/// Records one accepted native window close request.
pub(crate) use self::runtime::record_window_close;
/// Returns the runtime label exposed by the Tauri bridge.
pub(crate) use self::runtime::runtime_label;

View File

@@ -1,5 +1,5 @@
// file: crates/apps/game-reflex-poc-tauri/src/runtime.rs
// version: 1
// version: 2
/// Stable label for the current Tauri/WASM runtime combination.
pub(crate) fn runtime_label() -> &'static str {
@@ -16,3 +16,14 @@ pub(crate) fn record_frontend_ready() {
);
return;
}
/// Records one accepted native window close request.
pub(crate) fn record_window_close(source: &str) {
tracing::info!(
target: "games::tauri::reflex",
action = "window_close",
source = source,
"Reflex Tauri window close accepted"
);
return;
}

View File

@@ -1,5 +1,15 @@
// file: crates/apps/game-reflex-poc-tauri/src/tauri.rs
// version: 1
// version: 2
//! Tauri runtime assembly and Web/Rust commands for the Reflex POC.
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct FrontendLogPayload {
level: String,
target_id: String,
message: String,
}
#[tauri::command]
fn get_runtime_label() -> String {
@@ -12,6 +22,47 @@ fn frontend_ready() {
return;
}
#[tauri::command]
fn emit_frontend_log(payload: FrontendLogPayload) -> std::result::Result<(), String> {
let target_id = payload.target_id.trim();
if target_id != "frontend" && target_id != "main" {
return std::result::Result::Err("unsupported frontend log target".to_string());
}
let message = payload.message.as_str();
return match payload.level.trim().to_ascii_lowercase().as_str() {
"trace" => {
tracing::trace!(target: "games::tauri::reflex::frontend", action = "frontend_log", target_id = target_id, "{message}");
std::result::Result::Ok(())
},
"debug" => {
tracing::debug!(target: "games::tauri::reflex::frontend", action = "frontend_log", target_id = target_id, "{message}");
std::result::Result::Ok(())
},
"info" => {
tracing::info!(target: "games::tauri::reflex::frontend", action = "frontend_log", target_id = target_id, "{message}");
std::result::Result::Ok(())
},
"warn" => {
tracing::warn!(target: "games::tauri::reflex::frontend", action = "frontend_log", target_id = target_id, "{message}");
std::result::Result::Ok(())
},
"error" => {
tracing::error!(target: "games::tauri::reflex::frontend", action = "frontend_log", target_id = target_id, "{message}");
std::result::Result::Ok(())
},
_ => std::result::Result::Err("unsupported frontend log level".to_string()),
};
}
#[tauri::command]
fn close_main_window(window: tauri::WebviewWindow) -> std::result::Result<(), String> {
crate::record_window_close("game_quit_decision");
return match window.close() {
std::result::Result::Ok(()) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(error.to_string()),
};
}
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);
@@ -19,11 +70,16 @@ fn configure_plugins(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<taur
#[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_label, frontend_ready]);
return builder.invoke_handler(tauri::generate_handler![get_runtime_label, frontend_ready, emit_frontend_log, close_main_window]);
}
/// Runs the Reflex Tauri desktop application.
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::reflex",
action = "runtime_start",
@@ -33,7 +89,22 @@ pub fn run() -> std::result::Result<(), String> {
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()),
std::result::Result::Ok(()) => {
tracing::info!(
target: "games::tauri::reflex",
action = "runtime_stop",
"Reflex Tauri runtime stopped"
);
std::result::Result::Ok(())
},
std::result::Result::Err(error) => {
tracing::error!(
target: "games::tauri::reflex",
action = "runtime_error",
error = error.to_string(),
"Reflex Tauri runtime failed"
);
std::result::Result::Err(error.to_string())
},
};
}

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Reflex POC Tauri",
"version": "0.1.0-1-alpha.2.fix.2",
"version": "0.1.0-1-alpha.2.fix.3",
"identifier": "com.sasedev.games.reflex.tauri",
"build": {
"beforeDevCommand": {

View File

@@ -1,5 +1,5 @@
// file: crates/apps/game-reflex-poc-wasm/src/runtime.rs
// version: 1
// version: 2
/// WebAssembly-owned Reflex game state consumed by the Tauri WebView.
#[wasm_bindgen::prelude::wasm_bindgen]
@@ -33,6 +33,12 @@ impl ReflexWasmGame {
return;
}
/// Routes the Escape key through the shared game's quit policy.
pub fn quit_requested_escape(&mut self) -> bool {
let request = engine_v1_common::QuitRequest::new(engine_v1_common::QuitSource::Escape);
return engine_v1_common::EngineGame::quit_requested(&mut self.state, request) == engine_v1_common::QuitDecision::Exit;
}
/// Returns the current gameplay score.
pub fn score(&self) -> u32 {
return self.state.score().min(u32::MAX as u64) as u32;