diff --git a/CHANGELOG.md b/CHANGELOG.md index 72b111f..5573cac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,15 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] +## [0.7.0] - 2026-03-06 + ### Added - `webtau/adapters/electrobun`: `bootstrapElectrobunFromWindowBridge()`, `createElectrobunWindowBridgeProvider()`, `isElectrobun()`, and `getElectrobunCapabilities()` for first-class Electrobun runtime detection and bridge bootstrap. - `webtau`: `getRuntimeInfo()` for runtime id and capability introspection across WASM, Tauri, and provider-backed runtimes. - `create-gametau`: `--desktop-shell electrobun` and `--electrobun-mode hybrid|native|dual`. -- Electrobun counter example BrowserWindow and GPUWindow build lanes. +- Electrobun counter example BrowserWindow and GPUWindow build lanes, including the hybrid embedded WGPU showcase path. +- Battlestation example BrowserWindow and GPUWindow runtime lanes with a native Three WebGPU proof path. ### Changed - Scaffolded entrypoints now auto-check for `window.__ELECTROBUN__` before falling back to Tauri or plain WASM. - Electrobun counter example and generated Electrobun shell files now target the WGPU-capable `electrobun@^1.15.1` line. +- CI and release-gate docs now treat `Electrobun Hybrid + GPU Smoke` as a required release lane. ## [0.6.0] - 2026-03-04 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..5f3c52a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,89 @@ +# gametau + +A toolkit for building games in Rust that run in the browser (WASM) and on desktop from one codebase. + +## Architecture + +Monorepo with three main packages: + +- **`packages/webtau/`** — TypeScript runtime bridge. Routes `invoke()` to Tauri IPC (desktop) or direct WASM calls (browser). Includes shims for window, fs, dialog, event, path, input, audio, assets. +- **`packages/create-gametau/`** — CLI scaffolder (`bunx create-gametau my-game`). +- **`crates/webtau/`** — Rust crate: `wasm_state!` macro + `#[webtau::command]` re-export. +- **`crates/webtau-macros/`** — Proc macro that generates both `#[tauri::command]` and `#[wasm_bindgen]` wrappers from a single function definition. + +## Key Conventions + +- **Runtime:** Bun for TypeScript, Cargo for Rust. +- **Tests:** `bun test` (TS), `cargo test --workspace` (Rust). +- **Linting:** `bunx biome check` (TS), `cargo clippy --workspace` (Rust). +- **Imports:** Always `import { invoke } from "webtau"`, never `@tauri-apps/api/core`. +- **Args:** Use snake_case keys when calling `invoke("command", { my_arg: value })`. + +## Command Pattern (`#[webtau::command]`) + +```rust +#[webtau::command] +pub fn get_score(state: &GameWorld) -> i32 { state.score } + +#[webtau::command] +pub fn set_score(state: &mut GameWorld, value: i32) { state.score = value; } +``` + +- First param must be `&T` (read) or `&mut T` (mutable). Any identifier name works. +- Additional params become named args on the JS side (snake_case). +- Return `T`, `Result`, or `()`. +- Generates: native `#[tauri::command]` with `State>` + WASM `#[wasm_bindgen]` wrapper. +- **No async**, no `self`, no tuple/struct patterns. + +## `wasm_state!(T)` Macro + +Generates thread-local state for WASM. Expands to: `set_state(val)`, `with_state(|s| ...)`, `with_state_mut(|s| ...)`, `try_with_state(|s| ...)`, `try_with_state_mut(|s| ...)`. + +## Provider Pattern + +`CoreProvider` interface enables runtime pluggability (Tauri, Electrobun, custom): +```typescript +registerProvider({ id: "my-runtime", invoke: ..., convertFileSrc: ... }); +``` + +## Error Handling + +All failures throw `WebtauError` with structured envelope: +- `code`: `NO_WASM_CONFIGURED` | `UNKNOWN_COMMAND` | `LOAD_FAILED` | `PROVIDER_ERROR` | `PROVIDER_MISSING` +- `runtime`, `command`, `message`, `hint` + +## Package Exports (`webtau`) + +`.` / `./core` — invoke, configure, isTauri, getRuntimeInfo, registerProvider +`./event` — listen, once, emit, emitTo +`./task` — startTask, pollTask, cancelTask, updateTaskProgress +`./window` — getCurrentWindow (fullscreen, size, title, etc.) +`./fs` — writeTextFile, readTextFile, writeFile, readFile, exists, mkdir, readDir, remove, copyFile, rename +`./dialog` — message, ask, open, save +`./path` — appDataDir, join, basename, dirname, etc. +`./dpi` — LogicalSize, PhysicalSize, LogicalPosition, PhysicalPosition +`./input` — createInputController (keyboard, gamepad, touch, pointer-lock) +`./audio` — resume, suspend, setMuted, setMasterVolume, playTone +`./assets` — loadText, loadJson, loadBytes, loadImage, clear +`./app` — getName, getVersion, getTauriVersion +`./provider` — CoreProvider, WindowAdapter, EventAdapter, FsAdapter, DialogAdapter +`./adapters/tauri` — bootstrapTauri, createTauriCoreProvider, createTauriEventAdapter +`./adapters/electrobun` — bootstrapElectrobun, isElectrobun, getElectrobunCapabilities + +## Project Structure (scaffolded) + +``` +src-tauri/ + core/ — Pure Rust game logic (no framework deps) + commands/ — #[webtau::command] definitions + app/ — Tauri desktop shell (generate_handler!) + wasm/ — WASM entry point (links commands crate) +src/ + services/backend.ts — Typed invoke() wrappers + game/scene.ts — Renderer (Three.js/PixiJS/Canvas2D) +vite.config.ts — webtau-vite plugin +``` + +## Full API Reference + +See `llms-full.txt` in the repo root for the complete API surface. diff --git a/Cargo.toml b/Cargo.toml index ffda315..357541d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ members = [ ] [workspace.package] -version = "0.6.0" +version = "0.7.0" edition = "2021" rust-version = "1.77" license = "Apache-2.0" diff --git a/crates/webtau/Cargo.toml b/crates/webtau/Cargo.toml index bc4e3f9..efae7c5 100644 --- a/crates/webtau/Cargo.toml +++ b/crates/webtau/Cargo.toml @@ -8,4 +8,4 @@ license.workspace = true repository.workspace = true [dependencies] -webtau-macros = { path = "../webtau-macros", version = "=0.6.0" } +webtau-macros = { path = "../webtau-macros", version = "=0.7.0" } diff --git a/llms-full.txt b/llms-full.txt new file mode 100644 index 0000000..a7b0baa --- /dev/null +++ b/llms-full.txt @@ -0,0 +1,329 @@ +# gametau — Full API Reference + +> A toolkit for building games in Rust that run in the browser (WASM) and on desktop (Tauri) from one codebase. Write Rust game logic once; gametau compiles it to both a native binary and a WASM module, routing `invoke("command")` calls to whichever is available at runtime. + +## Architecture + +Your frontend calls `invoke("command_name")` everywhere. At runtime: +- Inside Tauri → routes through Tauri IPC at native speed +- In a browser → calls the WASM export directly +- With a registered provider → routes through the provider (e.g. Electrobun) + +Three packages: +- `webtau` (npm) — TypeScript runtime bridge with Tauri API shims +- `webtau` (crate) — Rust `wasm_state!` macro + `#[webtau::command]` +- `webtau-vite` — Vite plugin: compiles Rust to WASM, watches, hot-reloads, aliases imports +- `create-gametau` — project scaffolder + +## Getting Started + +```bash +bunx create-gametau my-game # Three.js (default), -t pixi, -t vanilla +cd my-game && bun install && bun run dev +``` + +Prerequisites: Rust with wasm32-unknown-unknown target, wasm-pack, Bun (or Node 18+). + +## TypeScript API — webtau (npm) + +### webtau/core (default export) + +```typescript +import { invoke, configure, isTauri, getRuntimeInfo, registerProvider, convertFileSrc } from "webtau"; +``` + +**`invoke(command: string, args?: Record): Promise`** +Universal IPC. Routes to Tauri IPC, WASM export, or registered provider. Args use snake_case keys. + +**`configure(config: WebtauConfig): void`** +```typescript +configure({ + loadWasm: async () => { const w = await import("./wasm/my_game"); await w.default(); w.init(); return w; }, + onLoadError: (err) => console.error(err), // optional +}); +``` +Required before invoke() in web mode. No-op inside Tauri. + +**`isTauri(): boolean`** — true when `window.__TAURI_INTERNALS__` exists. + +**`getRuntimeInfo(): RuntimeInfo`** — returns `{ id, platform, capabilities }`. + +**`registerProvider(provider: CoreProvider): void`** — register a custom runtime backend. + +**`convertFileSrc(filePath: string, protocol?: string): string`** — convert file path to loadable URL. + +### webtau/event + +```typescript +import { listen, once, emit, emitTo } from "webtau/event"; +``` + +**`listen(event: string, handler: EventCallback): Promise`** +Subscribe to events. Web: CustomEvent bridge. Desktop: adapter-based. + +**`once(event: string, handler: EventCallback): Promise`** +Listen for one event then auto-unlisten. + +**`emit(event: string, payload?: T): Promise`** +Dispatch event. Web: `window.dispatchEvent(new CustomEvent(...))`. + +**`emitTo(target: string, event: string, payload?: T): Promise`** +Alias of emit in web mode. + +**Types:** +```typescript +interface Event { event: string; id: number; payload: T; } +type EventCallback = (event: Event) => void; +type UnlistenFn = () => void; +``` + +### webtau/task + +```typescript +import { startTask, pollTask, cancelTask, updateTaskProgress } from "webtau/task"; +``` + +**`startTask(command: string, args?, options?): Promise`** +Launch non-blocking backend work. Returns taskId immediately. +`options.onCancel` propagates cancellation to backend. + +**`pollTask(taskId: string): Promise>`** +Returns: `{ state: "running", progress? }` | `{ state: "completed", result }` | `{ state: "cancelled" }` | `{ state: "failed", error }`. + +**`cancelTask(taskId: string): Promise`** — idempotent cancellation. + +**`updateTaskProgress(taskId: string, progress: TaskProgress): void`** — update from provider/test. + +**Types:** +```typescript +interface TaskProgress { percent: number; message?: string; } +type TaskState = { state: "running"; progress?: TaskProgress } | { state: "completed"; result: T } | { state: "cancelled" } | { state: "failed"; error: WebtauError }; +``` + +### webtau/window + +```typescript +import { getCurrentWindow } from "webtau/window"; +const win = getCurrentWindow(); +``` + +Methods: `isFullscreen()`, `setFullscreen(bool)`, `innerSize()`, `outerSize()`, `setSize(size)`, `maximize()`, `isMaximized()`, `title()`, `setTitle(str)`, `close()`, `minimize()`, `unminimize()`, `show()`, `hide()`, `setDecorations(bool)`, `center()`, `currentMonitor()`, `scaleFactor()`. + +Web: backed by Fullscreen API, document.title, screen.*. No-ops where browser doesn't support. + +### webtau/fs + +```typescript +import { writeTextFile, readTextFile, writeFile, readFile, exists, mkdir, readDir, remove, copyFile, rename } from "webtau/fs"; +``` + +All async. Web: backed by IndexedDB with virtual filesystem. + +### webtau/dialog + +```typescript +import { message, ask, open, save } from "webtau/dialog"; +``` + +Web: HTML `` modals with alert/confirm/prompt fallbacks. + +### webtau/path + +```typescript +import { appDataDir, appConfigDir, homeDir, join, basename, dirname, extname, normalize, resolve, isAbsolute, delimiter, sep } from "webtau/path"; +``` + +Web: deterministic virtual `/app/*` paths. POSIX-style path utilities. + +### webtau/dpi + +```typescript +import { LogicalSize, PhysicalSize, LogicalPosition, PhysicalPosition } from "webtau/dpi"; +``` + +DPI-aware size/position types with `toLogical(scaleFactor)` and `toPhysical(scaleFactor)` conversion. + +### webtau/input + +```typescript +import { createInputController } from "webtau/input"; +const input = createInputController(); +``` + +**`isPressed(key: string): boolean`** — keyboard state. +**`keyAxis(negative, positive): number`** — digital axis (-1, 0, 1). +**`gamepadAxis(axisIndex, options?): number`** — analog with deadzone/invert. +**`touches(): TouchPoint[]`** — active touch positions. +**`requestPointerLock(element): Promise`** — for mouse-look. +**`consumePointerDelta(): PointerDelta`** — relative mouse delta since last frame. +**`destroy()`** — cleanup all listeners. + +### webtau/audio + +```typescript +import { resume, suspend, setMuted, setMasterVolume, playTone } from "webtau/audio"; +``` + +Minimal Web Audio wrapper. `playTone(freq, durationMs, options?)` for SFX synthesis. + +### webtau/assets + +```typescript +import { loadText, loadJson, loadBytes, loadImage, clear } from "webtau/assets"; +``` + +Cached fetch helpers. `clear()` empties the cache. + +### webtau/app + +```typescript +import { getName, getVersion, getTauriVersion } from "webtau/app"; +``` + +Web: configurable name/version, `getTauriVersion()` returns `"web"` sentinel. + +### webtau/provider + +```typescript +import type { CoreProvider, WindowAdapter, EventAdapter, FsAdapter, DialogAdapter, RuntimeCapabilities, RuntimeInfo } from "webtau/provider"; +``` + +**CoreProvider interface:** +```typescript +interface CoreProvider { + id: string; + invoke(command: string, args?: Record): Promise; + convertFileSrc(filePath: string, protocol?: string): string; + runtimeInfo?: RuntimeInfo | (() => RuntimeInfo); +} +``` + +**RuntimeCapabilities:** `events`, `fs`, `dialog`, `window`, `task`, `convertFileSrc`, `renderMode?`, `hasGpuWindow?`, `hasWgpuView?`, `hasWebGpu?`. + +### webtau/adapters/tauri + +```typescript +import { bootstrapTauri, createTauriCoreProvider, createTauriEventAdapter } from "webtau/adapters/tauri"; +bootstrapTauri(); // registers core provider + event adapter in one call +``` + +### webtau/adapters/electrobun + +```typescript +import { bootstrapElectrobun, bootstrapElectrobunFromWindowBridge, isElectrobun, getElectrobunCapabilities, createElectrobunCoreProvider, dispatchElectrobunEvent } from "webtau/adapters/electrobun"; +``` + +### webtau/diagnostics + +```typescript +import { WebtauError } from "webtau"; // re-exported from core +``` + +**DiagnosticCode:** `"NO_WASM_CONFIGURED"` | `"UNKNOWN_COMMAND"` | `"LOAD_FAILED"` | `"PROVIDER_ERROR"` | `"PROVIDER_MISSING"` + +**WebtauError properties:** `code`, `runtime`, `command`, `message`, `hint`. + +| Situation | Error Code | Fix | +|---|---|---| +| invoke() before configure() | NO_WASM_CONFIGURED | Call configure({ loadWasm: ... }) first | +| WASM export not found | UNKNOWN_COMMAND | Check the function is exported from WASM crate | +| WASM module fails to load | LOAD_FAILED | Check loadWasm returns valid module, network available | +| Command/provider throws | PROVIDER_ERROR | Check Rust implementation or provider | + +## Rust API — webtau crate + +### `wasm_state!(T)` + +Generates thread-local state for WASM (single-threaded). Replaces Tauri's `State>`. + +```rust +use webtau::wasm_state; +struct GameWorld { score: i32 } +wasm_state!(GameWorld); +``` + +Generated API: +- `set_state(val: T)` — initialize or replace +- `with_state(|state: &T| -> R) -> R` — read-only (panics if uninitialized) +- `with_state_mut(|state: &mut T| -> R) -> R` — mutable (panics if uninitialized) +- `try_with_state(|state: &T| -> R) -> Option` — read-only, None if uninitialized +- `try_with_state_mut(|state: &mut T| -> R) -> Option` — mutable, None if uninitialized + +### `#[webtau::command]` + +Write one function, get both Tauri and WASM wrappers generated. + +```rust +#[webtau::command] +pub fn get_world_view(state: &GameWorld) -> WorldView { + state.view() +} + +#[webtau::command] +pub fn tick_world(state: &mut GameWorld, speed: i32) -> TickResult { + state.tick_with_speed(speed) +} +``` + +**Contract:** +- First param: `name: &T` (read-only) or `name: &mut T` (mutable). Any identifier. +- Additional params: named typed values → JS args object (snake_case). +- Return: `T` (Serialize), `Result` (errors surface to JS), or `()`. +- **Not supported:** async, self, tuple/struct patterns, param names starting with `__webtau`. + +**Generated code:** +- `#[cfg(not(wasm32))]` — `#[tauri::command(rename_all = "snake_case")]` with `State>`. Mutex uses `unwrap_or_else(|p| p.into_inner())` (non-panicking). +- `#[cfg(wasm32)]` — `#[wasm_bindgen]` with `serde_wasm_bindgen` deserialization. Returns `Result<_, JsError>` (non-panicking). Uses `try_with_state`/`try_with_state_mut`. + +## Vite Plugin — webtau-vite + +```typescript +import webtauVite from "webtau-vite"; +export default defineConfig({ plugins: [webtauVite()] }); +``` + +Options (all optional): `wasmCrate`, `wasmOutDir`, `watchPaths`, `wasmOpt`. +Auto-detects crate paths. Aliases `@tauri-apps/api/*` → `webtau/*` in web builds. + +## Project Structure (scaffolded) + +``` +my-game/ + src-tauri/ + Cargo.toml # workspace: [core, commands, app, wasm] + core/src/lib.rs # Pure game logic (no framework deps) + commands/src/ # #[webtau::command] definitions + app/src/lib.rs # Tauri shell: generate_handler! + Mutex state + wasm/src/lib.rs # WASM entry: links commands crate + src/ + index.ts # configure() + bootstrapTauri() + services/backend.ts # Typed invoke() wrappers + game/scene.ts # Renderer + vite.config.ts # webtau-vite plugin +``` + +## Build Targets + +| Target | Command | Output | +|---|---|---| +| Dev | `bun run dev` | localhost:1420 — hot-reload, no Tauri needed | +| Web | `bun run build:web` | Static files for any host | +| Desktop | `bun run build:desktop` | Native .exe/.dmg/.AppImage via Tauri | + +## Migration from Existing Tauri Game + +1. Extract core logic into a `core/` crate (no Tauri deps) +2. Create `commands/` crate with `#[webtau::command]` functions +3. Create `wasm/` crate with `crate-type = ["cdylib"]` +4. Update `app/` to import from `commands/` +5. Replace `@tauri-apps/api/core` imports with `webtau` +6. Add `configure()` for web mode +7. Add `webtau-vite` to vite.config.ts + +## Supported Runtimes + +| Runtime | Status | +|---|---| +| Web (WASM) | Stable | +| Desktop (Tauri) | Stable | +| Desktop (Electrobun) | Supported via explicit shell selection | diff --git a/llms.txt b/llms.txt new file mode 100644 index 0000000..59be8f6 --- /dev/null +++ b/llms.txt @@ -0,0 +1,56 @@ +# gametau + +> A toolkit for building games in Rust that run in the browser (WASM) and on desktop (Tauri) from one codebase. Write Rust game logic once; gametau compiles it to both a native binary and a WASM module, routing `invoke("command")` calls to whichever is available at runtime. + +Three packages work together: +- `webtau` (npm + Rust crate) — runtime bridge routing invoke() to Tauri IPC or WASM +- `webtau-vite` — Vite plugin compiling Rust to WASM on save with hot-reload +- `create-gametau` — project scaffolder + +## Core Concepts + +- [README](https://github.com/devallibus/gametau/blob/master/README.md): Full documentation with architecture, getting started, API reference, and migration guide +- [CLAUDE.md](https://github.com/devallibus/gametau/blob/master/CLAUDE.md): Project conventions and quick reference for AI assistants + +## TypeScript API (webtau npm package) + +- [core.ts](https://github.com/devallibus/gametau/blob/master/packages/webtau/src/core.ts): invoke(), configure(), isTauri(), getRuntimeInfo(), registerProvider(), convertFileSrc() +- [event.ts](https://github.com/devallibus/gametau/blob/master/packages/webtau/src/event.ts): listen(), once(), emit(), emitTo() — CustomEvent bridge for web, adapter-based for desktop +- [task.ts](https://github.com/devallibus/gametau/blob/master/packages/webtau/src/task.ts): startTask(), pollTask(), cancelTask(), updateTaskProgress() — non-blocking task lifecycle +- [window.ts](https://github.com/devallibus/gametau/blob/master/packages/webtau/src/window.ts): getCurrentWindow() — fullscreen, size, title, minimize, maximize, close +- [fs.ts](https://github.com/devallibus/gametau/blob/master/packages/webtau/src/fs.ts): writeTextFile, readTextFile, writeFile, readFile, exists, mkdir, readDir, remove — IndexedDB-backed +- [dialog.ts](https://github.com/devallibus/gametau/blob/master/packages/webtau/src/dialog.ts): message(), ask(), open(), save() — HTML dialog fallbacks +- [path.ts](https://github.com/devallibus/gametau/blob/master/packages/webtau/src/path.ts): appDataDir, join, basename, dirname, extname, normalize — virtual POSIX paths +- [input.ts](https://github.com/devallibus/gametau/blob/master/packages/webtau/src/input.ts): createInputController() — keyboard, gamepad, touch, pointer-lock +- [audio.ts](https://github.com/devallibus/gametau/blob/master/packages/webtau/src/audio.ts): resume, suspend, setMuted, setMasterVolume, playTone — Web Audio wrapper +- [assets.ts](https://github.com/devallibus/gametau/blob/master/packages/webtau/src/assets.ts): loadText, loadJson, loadBytes, loadImage, clear — cached asset loading +- [app.ts](https://github.com/devallibus/gametau/blob/master/packages/webtau/src/app.ts): getName, getVersion, getTauriVersion — app metadata +- [dpi.ts](https://github.com/devallibus/gametau/blob/master/packages/webtau/src/dpi.ts): LogicalSize, PhysicalSize, LogicalPosition, PhysicalPosition +- [provider.ts](https://github.com/devallibus/gametau/blob/master/packages/webtau/src/provider.ts): CoreProvider, WindowAdapter, EventAdapter, FsAdapter, DialogAdapter interfaces +- [diagnostics.ts](https://github.com/devallibus/gametau/blob/master/packages/webtau/src/diagnostics.ts): WebtauError, DiagnosticCode, DiagnosticEnvelope + +## Rust API (webtau crate) + +- [lib.rs](https://github.com/devallibus/gametau/blob/master/crates/webtau/src/lib.rs): wasm_state!(T) macro — generates set_state, with_state, with_state_mut, try_with_state, try_with_state_mut +- [macros/lib.rs](https://github.com/devallibus/gametau/blob/master/crates/webtau-macros/src/lib.rs): #[webtau::command] proc macro — dual-target codegen for Tauri + WASM from one function + +## Adapters + +- [adapters/tauri.ts](https://github.com/devallibus/gametau/blob/master/packages/webtau/src/adapters/tauri.ts): bootstrapTauri(), createTauriCoreProvider(), createTauriEventAdapter() +- [adapters/electrobun.ts](https://github.com/devallibus/gametau/blob/master/packages/webtau/src/adapters/electrobun.ts): bootstrapElectrobun(), isElectrobun(), getElectrobunCapabilities() + +## Project Structure + +- [Scaffolded layout](https://github.com/devallibus/gametau/blob/master/README.md#scaffolded-project-structure): src-tauri/{core, commands, app, wasm} + src/{services, game} + +## Examples + +- [counter](https://github.com/devallibus/gametau/tree/master/examples/counter): Simplest gametau project — WASM + desktop +- [pong](https://github.com/devallibus/gametau/tree/master/examples/pong): Two-player Pong with Rust physics and PixiJS +- [battlestation](https://github.com/devallibus/gametau/tree/master/examples/battlestation): Flagship — full module surface with persistent state and events + +## Optional + +- [llms-full.txt](https://github.com/devallibus/gametau/blob/master/llms-full.txt): Expanded API reference with full signatures, code examples, and error handling details +- [CONTRIBUTING.md](https://github.com/devallibus/gametau/blob/master/CONTRIBUTING.md): Contribution guide +- [ELECTROBUN-SHOWCASE.md](https://github.com/devallibus/gametau/blob/master/ELECTROBUN-SHOWCASE.md): Electrobun integration walkthrough diff --git a/packages/create-gametau/package.json b/packages/create-gametau/package.json index 04b9674..80a35c2 100644 --- a/packages/create-gametau/package.json +++ b/packages/create-gametau/package.json @@ -1,6 +1,6 @@ { "name": "create-gametau", - "version": "0.6.0", + "version": "0.7.0", "description": "Scaffold a Tauri game that deploys to web + desktop", "license": "Apache-2.0", "repository": { diff --git a/packages/create-gametau/templates/base/package.json b/packages/create-gametau/templates/base/package.json index ce461d9..190651c 100644 --- a/packages/create-gametau/templates/base/package.json +++ b/packages/create-gametau/templates/base/package.json @@ -11,12 +11,12 @@ "preview": "vite preview" }, "dependencies": { - "webtau": "^0.6.0" + "webtau": "^0.7.0" }, "devDependencies": { "typescript": "^5.8.0", "vite": "^6.0.0", - "webtau-vite": "^0.6.0", + "webtau-vite": "^0.7.0", "@tauri-apps/cli": "^2.0.0", "@tauri-apps/api": "^2.0.0" } diff --git a/packages/create-gametau/templates/base/src-tauri/commands/Cargo.toml b/packages/create-gametau/templates/base/src-tauri/commands/Cargo.toml index b800fa7..ccb87fd 100644 --- a/packages/create-gametau/templates/base/src-tauri/commands/Cargo.toml +++ b/packages/create-gametau/templates/base/src-tauri/commands/Cargo.toml @@ -5,7 +5,7 @@ edition.workspace = true [dependencies] {{PROJECT_NAME}}-core = { path = "../core" } -webtau = "0.6.0" +webtau = "0.7.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tauri = { version = "2", features = [] } diff --git a/packages/create-gametau/templates/base/src-tauri/wasm/Cargo.toml b/packages/create-gametau/templates/base/src-tauri/wasm/Cargo.toml index 3c141a5..2da61ba 100644 --- a/packages/create-gametau/templates/base/src-tauri/wasm/Cargo.toml +++ b/packages/create-gametau/templates/base/src-tauri/wasm/Cargo.toml @@ -11,7 +11,7 @@ wasm-bindgen = "0.2" serde = { version = "1", features = ["derive"] } serde-wasm-bindgen = "0.6" getrandom = { version = "0.2", features = ["js"] } -webtau = "0.6.0" +webtau = "0.7.0" {{PROJECT_NAME}}-core = { path = "../core" } {{PROJECT_NAME}}-commands = { path = "../commands" } diff --git a/packages/create-gametau/templates/pixi/package.json b/packages/create-gametau/templates/pixi/package.json index eee534f..c4b6214 100644 --- a/packages/create-gametau/templates/pixi/package.json +++ b/packages/create-gametau/templates/pixi/package.json @@ -12,12 +12,12 @@ }, "dependencies": { "pixi.js": "^8.0.0", - "webtau": "^0.6.0" + "webtau": "^0.7.0" }, "devDependencies": { "typescript": "^5.8.0", "vite": "^6.0.0", - "webtau-vite": "^0.6.0", + "webtau-vite": "^0.7.0", "@tauri-apps/cli": "^2.0.0", "@tauri-apps/api": "^2.0.0" } diff --git a/packages/create-gametau/templates/three/package.json b/packages/create-gametau/templates/three/package.json index 5bd69c0..395e86c 100644 --- a/packages/create-gametau/templates/three/package.json +++ b/packages/create-gametau/templates/three/package.json @@ -12,13 +12,13 @@ }, "dependencies": { "three": "^0.172.0", - "webtau": "^0.6.0" + "webtau": "^0.7.0" }, "devDependencies": { "@types/three": "^0.172.0", "typescript": "^5.8.0", "vite": "^6.0.0", - "webtau-vite": "^0.6.0", + "webtau-vite": "^0.7.0", "@tauri-apps/cli": "^2.0.0", "@tauri-apps/api": "^2.0.0" } diff --git a/packages/webtau-vite/package.json b/packages/webtau-vite/package.json index 6d1da6c..b2e4889 100644 --- a/packages/webtau-vite/package.json +++ b/packages/webtau-vite/package.json @@ -1,6 +1,6 @@ { "name": "webtau-vite", - "version": "0.6.0", + "version": "0.7.0", "description": "Vite plugin for webtau — wasm-pack automation + Tauri API aliasing", "license": "Apache-2.0", "repository": { diff --git a/packages/webtau/package.json b/packages/webtau/package.json index b37d1c2..513c25d 100644 --- a/packages/webtau/package.json +++ b/packages/webtau/package.json @@ -1,6 +1,6 @@ { "name": "webtau", - "version": "0.6.0", + "version": "0.7.0", "description": "Deploy Tauri games to web + desktop from one codebase", "license": "Apache-2.0", "repository": { diff --git a/plugins/gametau-dev/.claude-plugin/plugin.json b/plugins/gametau-dev/.claude-plugin/plugin.json new file mode 100644 index 0000000..099f556 --- /dev/null +++ b/plugins/gametau-dev/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "gametau-dev", + "description": "Skills and agents for building games with gametau — the Rust WASM + Tauri game toolkit", + "version": "1.0.0", + "author": { + "name": "devallibus" + }, + "homepage": "https://github.com/devallibus/gametau", + "repository": "https://github.com/devallibus/gametau", + "license": "Apache-2.0", + "keywords": ["gametau", "webtau", "tauri", "wasm", "game-development", "rust"] +} diff --git a/plugins/gametau-dev/agents/gametau-expert.md b/plugins/gametau-dev/agents/gametau-expert.md new file mode 100644 index 0000000..a481435 --- /dev/null +++ b/plugins/gametau-dev/agents/gametau-expert.md @@ -0,0 +1,34 @@ +--- +name: gametau-expert +description: Expert in gametau game development — the Rust WASM + Tauri toolkit. Use when building games with gametau, implementing commands, configuring the runtime bridge, scaffolding projects, debugging dual-target issues, or migrating from vanilla Tauri. +--- + +You are an expert in gametau, a toolkit for building games in Rust that run in the browser (WASM) and on desktop (Tauri) from one codebase. + +## Core Knowledge + +**Architecture:** gametau routes `invoke("command")` calls to Tauri IPC on desktop or direct WASM calls in the browser, automatically. The switch is transparent to frontend code. + +**Three packages:** +- `webtau` (npm) — TypeScript runtime bridge with Tauri API shims for window, fs, dialog, event, path, input, audio, assets +- `webtau` (Rust crate) — `wasm_state!` macro for WASM thread-local state + `#[webtau::command]` proc macro for dual-target codegen +- `webtau-vite` — Vite plugin that compiles Rust to WASM, watches for changes, and aliases `@tauri-apps/api/*` imports + +**Command pattern:** `#[webtau::command]` takes a function with `state: &T` or `state: &mut T` as the first parameter and generates both `#[tauri::command]` (native) and `#[wasm_bindgen]` (WASM) wrappers. Additional params become snake_case args on the JS side. + +**State management:** `wasm_state!(T)` generates `set_state`, `with_state`, `with_state_mut`, `try_with_state`, `try_with_state_mut` for thread-local WASM state. On desktop, Tauri uses `State>`. + +**Provider pattern:** `CoreProvider` interface allows plugging in alternative runtimes (Electrobun, custom). Adapters exist for window, event, fs, and dialog. + +**Error handling:** All errors are `WebtauError` with `code`, `runtime`, `command`, `message`, `hint`. Codes: NO_WASM_CONFIGURED, UNKNOWN_COMMAND, LOAD_FAILED, PROVIDER_ERROR, PROVIDER_MISSING. + +## When Helping Users + +1. Always recommend `import { invoke } from "webtau"` not `@tauri-apps/api/core` +2. Always use snake_case for invoke() args keys +3. Remind about `configure()` before invoke() in web mode +4. For Rust commands, ensure first param is `&T` or `&mut T` +5. Place commands in a submodule, not crate root +6. Use `try_with_state` / `try_with_state_mut` for non-panicking state access +7. Separate pure game logic in `core/` crate with no framework dependencies +8. Reference `llms-full.txt` in the repo root for the complete API surface diff --git a/plugins/gametau-dev/skills/gametau-scaffold/SKILL.md b/plugins/gametau-dev/skills/gametau-scaffold/SKILL.md new file mode 100644 index 0000000..89c49d3 --- /dev/null +++ b/plugins/gametau-dev/skills/gametau-scaffold/SKILL.md @@ -0,0 +1,118 @@ +--- +name: gametau-scaffold +description: Use when creating a new gametau project, setting up the Rust workspace, configuring Vite with webtau-vite, choosing a renderer template, or understanding the scaffolded project structure. +--- + +# gametau Project Scaffolding + +## Create a New Project + +```bash +bunx create-gametau my-game # Three.js (default) +bunx create-gametau my-game -t pixi # PixiJS +bunx create-gametau my-game -t vanilla # Canvas2D +bunx create-gametau my-game --desktop-shell electrobun +``` + +Then: +```bash +cd my-game +bun install +bun run dev # Opens localhost:1420 with hot-reload +``` + +## Prerequisites + +- Rust with `wasm32-unknown-unknown` target: `rustup target add wasm32-unknown-unknown` +- wasm-pack: `cargo install wasm-pack` +- Bun (or Node 18+) +- Tauri CLI (only for desktop): `bun add -g @tauri-apps/cli` + +## Scaffolded Structure + +``` +my-game/ + src-tauri/ + Cargo.toml # Workspace: [core, commands, app, wasm] + core/ + src/lib.rs # Pure Rust game logic — NO framework deps + Cargo.toml # Only serde + commands/ + src/lib.rs # Re-exports from submodule + src/commands.rs # #[webtau::command] functions + wasm_state! + Cargo.toml # Depends on core, webtau, wasm-bindgen, serde + app/ + src/lib.rs # Tauri shell: generate_handler! + Mutex state + tauri.conf.json + Cargo.toml # Depends on tauri, commands + wasm/ + src/lib.rs # Links commands: `use my_game_commands as _;` + Cargo.toml # crate-type = ["cdylib"], depends on commands + src/ + index.ts # Entry: configure() + bootstrapTauri() + game/scene.ts # Three.js / PixiJS / Canvas2D scene + game/loop.ts # requestAnimationFrame + tick + services/backend.ts # Typed invoke() wrappers + task seams + services/settings.ts # Runtime settings (webtau/path + webtau/fs) + services/comms.ts # Event-driven comms (webtau/event) + services/contracts.ts # Shared TypeScript interfaces + package.json + vite.config.ts # webtau-vite plugin +``` + +## Vite Configuration + +```typescript +import { defineConfig } from "vite"; +import webtauVite from "webtau-vite"; + +export default defineConfig({ + plugins: [webtauVite()], +}); +``` + +Zero config for the standard layout. Options (all optional): +- `wasmCrate: "src-tauri/wasm"` — path to WASM crate +- `wasmOutDir: "src/wasm"` — wasm-pack output directory +- `watchPaths: []` — extra dirs to watch +- `wasmOpt: false` — run wasm-opt on release builds + +## Build Targets + +| Target | Command | Output | +|---|---|---| +| Dev (web) | `bun run dev` | localhost:1420, hot-reload, no Tauri needed | +| Web release | `bun run build:web` | Static files for any host | +| Desktop | `bun run build:desktop` | Native .exe/.dmg/.AppImage via Tauri | + +## Entry Point Pattern + +```typescript +// src/index.ts +import { configure, isTauri } from "webtau"; + +if (!isTauri()) { + configure({ + loadWasm: async () => { + const wasm = await import("./wasm/my_game_wasm"); + await wasm.default(); + wasm.init(); + return wasm; + }, + }); +} +// From here, invoke() works on both platforms +``` + +## WASM Optimization + +Add to `wasm/Cargo.toml`: +```toml +[profile.release] +lto = true +opt-level = "z" +codegen-units = 1 +strip = true +``` + +Expected sizes: simple game ~50-100 KB WASM (20-40 KB gzipped). diff --git a/plugins/gametau-dev/skills/gametau-troubleshoot/SKILL.md b/plugins/gametau-dev/skills/gametau-troubleshoot/SKILL.md new file mode 100644 index 0000000..dd8bff3 --- /dev/null +++ b/plugins/gametau-dev/skills/gametau-troubleshoot/SKILL.md @@ -0,0 +1,96 @@ +--- +name: gametau-troubleshoot +description: Use when encountering gametau errors like NO_WASM_CONFIGURED, UNKNOWN_COMMAND, LOAD_FAILED, PROVIDER_ERROR, or PROVIDER_MISSING. Also use when debugging runtime detection, WASM loading, Tauri IPC, or provider registration issues. +--- + +# gametau Troubleshooting + +## Error Code Reference + +All gametau errors throw `WebtauError` with a structured envelope: `{ code, runtime, command, message, hint }`. + +| Code | Runtime | Cause | Fix | +|---|---|---|---| +| `NO_WASM_CONFIGURED` | wasm | `invoke()` called before `configure()` in web mode | Call `configure({ loadWasm: ... })` before any invoke() | +| `UNKNOWN_COMMAND` | wasm | WASM module has no export matching the command name | Check the function is exported from the commands crate and re-exported from wasm/src/lib.rs | +| `LOAD_FAILED` | wasm | WASM module failed to load | Check loadWasm() returns a valid module; check network; check wasm-pack compiled successfully | +| `PROVIDER_ERROR` | varies | Command or provider threw during execution | Check the Rust implementation; check provider.invoke() logic | +| `PROVIDER_MISSING` | unknown | Expected provider not registered | Call registerProvider() or bootstrapTauri()/bootstrapElectrobun() | + +## Diagnostic Envelope + +```typescript +try { + await invoke("my_command"); +} catch (err) { + if (err instanceof WebtauError) { + console.error(`[${err.code}] ${err.message}`); + console.error(`Runtime: ${err.runtime}, Command: ${err.command}`); + console.error(`Hint: ${err.hint}`); + } +} +``` + +Import: `import { WebtauError } from "webtau";` + +## Common Issues by Runtime + +### Web (WASM) + +**"No WASM module configured"** +- Call `configure()` in your entry point before any invoke() +- Ensure the configure block runs before any component mounts + +**"WASM module has no export named X"** +- The function must be `pub` and use `#[webtau::command]` +- The commands crate must be linked from wasm/src/lib.rs: `use my_commands as _;` +- Check wasm-pack built successfully: `bun run dev` logs will show errors + +**"Failed to load WASM module"** +- Check `wasm-pack` is installed: `wasm-pack --version` +- Check the wasm crate compiles: `cd src-tauri/wasm && cargo check --target wasm32-unknown-unknown` +- Check the import path in loadWasm matches the wasm-pack output directory + +**WASM module loads but commands return wrong types** +- Ensure return types implement `Serialize` +- Check that args use snake_case keys on the JS side + +### Desktop (Tauri) + +**Commands not found in Tauri** +- Ensure commands are registered in `generate_handler![]` in app/src/lib.rs +- Check command names match exactly (snake_case) + +**State not available** +- Ensure `.manage(Mutex::new(YourState::new()))` is called in the Tauri builder +- The state type must match the first param type in `#[webtau::command]` + +**Mutex poisoned** +- The macro generates `unwrap_or_else(|p| p.into_inner())` to recover from poisoned mutexes +- If you see panics, check if a previous command panicked while holding the lock + +### Desktop (Electrobun) + +**Runtime not detected** +- Check `window.__ELECTROBUN__` exists in the webview +- Call `bootstrapElectrobun()` or `bootstrapElectrobunFromWindowBridge()` in your entry point + +**Capabilities not available** +- Use `getElectrobunCapabilities()` to check what's supported +- GPUWindow features require `hasGpuWindow: true` in capabilities + +## Build Issues + +**wasm-pack not found** +- Install: `cargo install wasm-pack` +- If prebuilt artifacts exist in wasmOutDir, webtau-vite reuses them but disables hot-reload + +**Rust compilation errors with #[webtau::command]** +- "does not support async" — commands must be synchronous; use webtau/task for async work +- "does not support methods with self" — use free functions with `state: &T` +- "first parameter must be a reference" — use `state: &T` or `state: &mut T` +- "parameters must use simple identifiers" — no destructuring in param list + +**Import aliasing not working** +- Ensure webtau-vite is in your vite.config.ts plugins array +- The plugin only aliases in web mode (not during `tauri dev`) diff --git a/plugins/gametau-dev/skills/webtau-api/SKILL.md b/plugins/gametau-dev/skills/webtau-api/SKILL.md new file mode 100644 index 0000000..f521809 --- /dev/null +++ b/plugins/gametau-dev/skills/webtau-api/SKILL.md @@ -0,0 +1,112 @@ +--- +name: webtau-api +description: Use when writing TypeScript code that imports from webtau, calling invoke(), using events, tasks, window, fs, dialog, path, input, audio, or assets APIs. Also use when configuring WASM loaders, registering providers, or working with runtime detection. +--- + +# webtau TypeScript API + +webtau is a drop-in replacement for `@tauri-apps/api`. Always import from `"webtau"`, never `"@tauri-apps/api/core"`. + +## Quick Reference + +| Import path | Key exports | +|---|---| +| `webtau` or `webtau/core` | `invoke`, `configure`, `isTauri`, `getRuntimeInfo`, `registerProvider` | +| `webtau/event` | `listen`, `once`, `emit`, `emitTo` | +| `webtau/task` | `startTask`, `pollTask`, `cancelTask`, `updateTaskProgress` | +| `webtau/window` | `getCurrentWindow()` → fullscreen, size, title, etc. | +| `webtau/fs` | `writeTextFile`, `readTextFile`, `writeFile`, `readFile`, `exists`, `mkdir`, `readDir`, `remove`, `copyFile`, `rename` | +| `webtau/dialog` | `message`, `ask`, `open`, `save` | +| `webtau/path` | `appDataDir`, `join`, `basename`, `dirname`, `extname`, `normalize` | +| `webtau/dpi` | `LogicalSize`, `PhysicalSize`, `LogicalPosition`, `PhysicalPosition` | +| `webtau/input` | `createInputController()` — keyboard, gamepad, touch, pointer-lock | +| `webtau/audio` | `resume`, `suspend`, `setMuted`, `setMasterVolume`, `playTone` | +| `webtau/assets` | `loadText`, `loadJson`, `loadBytes`, `loadImage`, `clear` | +| `webtau/app` | `getName`, `getVersion`, `getTauriVersion` | +| `webtau/provider` | `CoreProvider`, `WindowAdapter`, `EventAdapter`, `FsAdapter`, `DialogAdapter` | +| `webtau/adapters/tauri` | `bootstrapTauri()` | +| `webtau/adapters/electrobun` | `bootstrapElectrobun()`, `isElectrobun()` | + +## Core Pattern + +```typescript +import { invoke, configure, isTauri } from "webtau"; + +// Web mode requires WASM configuration before any invoke() +if (!isTauri()) { + configure({ + loadWasm: async () => { + const wasm = await import("./wasm/my_game_wasm"); + await wasm.default(); + wasm.init(); + return wasm; + }, + }); +} + +// Same call works on both platforms +const view = await invoke("get_world_view"); +const result = await invoke("tick_world", { speed: 2 }); +``` + +Args use **snake_case** keys to match Rust command parameters. + +## Events + +```typescript +import { listen, emit } from "webtau/event"; + +const unlisten = await listen<{ score: number }>("score_updated", (event) => { + console.log(event.payload.score); +}); +await emit("player_action", { type: "jump" }); +unlisten(); // cleanup +``` + +## Tasks (non-blocking backend work) + +```typescript +import { startTask, pollTask, cancelTask } from "webtau/task"; + +const taskId = await startTask("process_world", { slot: 1 }, { + onCancel: () => invoke("cancel_processing"), +}); + +const status = await pollTask(taskId); +if (status.state === "completed") console.log(status.result); +if (status.state === "failed") console.error(status.error.hint); + +await cancelTask(taskId); // idempotent +``` + +## Provider Registration + +```typescript +import { registerProvider } from "webtau"; +import type { CoreProvider } from "webtau/provider"; + +const myProvider: CoreProvider = { + id: "my-runtime", + invoke: async (cmd, args) => { /* ... */ }, + convertFileSrc: (path) => path, +}; +registerProvider(myProvider); +``` + +## Error Handling + +All failures throw `WebtauError` with: `code`, `runtime`, `command`, `message`, `hint`. + +| Code | Cause | +|---|---| +| `NO_WASM_CONFIGURED` | invoke() before configure() in web mode | +| `UNKNOWN_COMMAND` | WASM module has no matching export | +| `LOAD_FAILED` | WASM module failed to load (network, compile error) | +| `PROVIDER_ERROR` | Command or provider threw during execution | + +## Common Mistakes + +1. **Forgetting configure()** — invoke() in web mode without configure() throws NO_WASM_CONFIGURED +2. **Using @tauri-apps/api imports** — always use `"webtau"` or `"webtau/*"`; webtau-vite aliases these automatically but direct imports are preferred +3. **camelCase args** — use snake_case keys: `invoke("cmd", { my_arg: 1 })` not `{ myArg: 1 }` +4. **Not calling wasm.init()** — the WASM module needs `init()` (generated by wasm_state!) after `default()` diff --git a/plugins/gametau-dev/skills/webtau-command-macro/SKILL.md b/plugins/gametau-dev/skills/webtau-command-macro/SKILL.md new file mode 100644 index 0000000..c5995c0 --- /dev/null +++ b/plugins/gametau-dev/skills/webtau-command-macro/SKILL.md @@ -0,0 +1,129 @@ +--- +name: webtau-command-macro +description: Use when writing Rust commands for gametau, using #[webtau::command] or wasm_state! macro, defining game state, or working with dual-target WASM and Tauri codegen. +--- + +# Rust Command Macro & State Management + +## `#[webtau::command]` — Dual-Target Codegen + +Write one function, get both Tauri and WASM wrappers generated automatically. + +```rust +#[webtau::command] +pub fn get_score(state: &GameWorld) -> i32 { + state.score +} + +#[webtau::command] +pub fn set_score(state: &mut GameWorld, value: i32) { + state.score = value; +} + +#[webtau::command] +pub fn save_game(state: &GameWorld, slot: i32) -> Result { + state.save_to_slot(slot) +} +``` + +### Contract + +| Rule | Detail | +|---|---| +| First param | `name: &T` (read) or `name: &mut T` (write). Any identifier. | +| Extra params | Named, typed values → JS args in snake_case | +| Return | `T` (Serialize), `Result`, or `()` | +| Not supported | `async`, `self`, tuple/struct patterns, `__webtau` prefix names | + +### What Gets Generated + +**Native (`#[cfg(not(wasm32))]`):** +- `#[tauri::command(rename_all = "snake_case")]` wrapper +- `State>` parameter injected by Tauri +- Mutex lock uses `unwrap_or_else(|p| p.into_inner())` — non-panicking on poisoned mutex + +**WASM (`#[cfg(wasm32)]`):** +- `#[wasm_bindgen]` wrapper +- Args deserialized from `JsValue` via `serde_wasm_bindgen` +- State accessed via `try_with_state` / `try_with_state_mut` — returns `JsError` if uninitialized +- All paths return `Result<_, JsError>` — no panics + +## `wasm_state!(T)` — Thread-Local State for WASM + +```rust +use webtau::wasm_state; + +struct GameWorld { score: i32 } +wasm_state!(GameWorld); + +// In your init function: +#[wasm_bindgen] +pub fn init() { set_state(GameWorld { score: 0 }); } +``` + +### Generated Functions + +| Function | Signature | Behavior | +|---|---|---| +| `set_state` | `fn set_state(val: T)` | Initialize or replace state | +| `with_state` | `fn with_state(f: F) -> R` | Read-only access. **Panics** if uninitialized. | +| `with_state_mut` | `fn with_state_mut(f: F) -> R` | Mutable access. **Panics** if uninitialized. | +| `try_with_state` | `fn try_with_state(f: F) -> Option` | Read-only. Returns `None` if uninitialized. | +| `try_with_state_mut` | `fn try_with_state_mut(f: F) -> Option` | Mutable. Returns `None` if uninitialized. | + +The `try_*` variants are used by generated WASM wrappers to avoid panics. + +## Crate Layout + +``` +src-tauri/ + core/src/lib.rs # Pure game logic (no framework deps) + commands/src/ + lib.rs # Re-exports: pub use commands::{...} + commands.rs # #[webtau::command] + wasm_state! + init() + app/src/lib.rs # generate_handler![...] + Mutex state + wasm/src/lib.rs # use my_game_commands as _; +``` + +### commands.rs Pattern + +```rust +use my_game_core::{GameWorld, WorldView, TickResult}; + +#[cfg(target_arch = "wasm32")] +webtau::wasm_state!(GameWorld); + +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen::prelude::wasm_bindgen] +pub fn init() { set_state(GameWorld::new()); } + +#[webtau::command] +pub fn get_world_view(state: &GameWorld) -> WorldView { + state.view() +} + +#[webtau::command] +pub fn tick_world(state: &mut GameWorld) -> TickResult { + state.tick() +} +``` + +### lib.rs Re-export Pattern + +```rust +mod commands; + +#[cfg(not(target_arch = "wasm32"))] +pub use commands::{get_world_view, tick_world}; + +#[cfg(target_arch = "wasm32")] +pub use commands::{init, get_world_view, tick_world}; +``` + +## Common Mistakes + +1. **Commands at crate root** — place in a submodule to avoid conflicts with Tauri's `#[macro_export]` +2. **Missing cfg gates** — `wasm_state!` and `init()` need `#[cfg(target_arch = "wasm32")]` +3. **Forgetting re-exports** — `wasm/src/lib.rs` needs `use my_commands as _;` to link exports +4. **Using panicking state access** — WASM wrappers use `try_with_state` (the macro handles this) +5. **Async commands** — `#[webtau::command]` does not support async; compute synchronously or use tasks