You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@@ -0,0 +1,421 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Sour
@@ -8,3 +8,7 @@ documents the reviewed direct dependencies and policy.
Source copies of licenses required by a distributed binary belong in this directory.
No dependency may be added merely by copying a license here: its source, version,
features, engine-owned boundary, and failure behavior still require review.
+
+The bundled Gradle 8.13 wrapper (`assets/android/gradle-wrapper.jar`) is covered by
+[Apache License 2.0](Gradle-Apache-2.0.txt). Its verified SHA-256 is
+`81a82aaea5abcc8ff68b3dfcb58b3c3c429378efd98e7433460610fecd7ae45f`.
@@ -0,0 +1,96 @@
+-------------------------------
+UBUNTU FONT LICENCE Version 1.0
+-------------------------------
+
+PREAMBLE
+This licence allows the licensed fonts to be used, studied, modified and
+redistributed freely. The fonts, including any derivative works, can be
+bundled, embedded, and redistributed provided the terms of this licence
+are met. The fonts and derivatives, however, cannot be released under
+any other licence. The requirement for fonts to remain under this
+licence does not require any document created using the fonts or their
+derivatives to be published under this licence, as long as the primary
+purpose of the document is not to be a vehicle for the distribution of
+the fonts.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this licence and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Original Version" refers to the collection of Font Software components
+as received under this licence.
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by
@@ -24,6 +24,8 @@ Electron is not used. The desktop applications are native Rust executables rende
with stable script identities, generated VS Code
workspace/API annotations, versioned manifests/components, isolated execution,
deterministic input actions, clickable diagnostics, and last-known-good hot reload.
+- File → Build Project packaging for Windows games and custom installers, playable websites,
+ and generated Android apps. See [Build and distribute a project](docs/PROJECT_BUILDS.md).
- Strict locked workspace gates, dependency/license/advisory policy, and Windows/Linux CI.
The architectural baseline and staged roadmap live in
@@ -30,8 +30,29 @@ license policy on the complete transitive graph.
| Material Design Icons (Pictogrammers) | Embedded upstream SVG script-language icons | Apache-2.0 | Icons are compiled into the editor, so rendering never depends on network availability |
| Lucide Icons | Embedded SVG interface, file-type, transform, playback, and window-control icons | ISC AND MIT | Icons are compiled into the desktop applications and tinted locally; no runtime network access |
-Development-only `tempfile` is used for isolated persistence and corruption fixtures
-and is not an engine runtime API.
+`tempfile` manages private build staging and standalone-session scratch directories,
+as well as isolated persistence and corruption fixtures. Its handles are not part of
+the public game API.
+## Direct game export tools
+
+- **Emscripten SDK 6.0.13** compiles the existing game session, Lua/QuickJS and GLES
+ renderer to WebAssembly. The installer builder can provision it into `.tools`.
+ Compiler tools are build dependencies; the generated runtime is bundled beside
+ the editor. Browser initialization/graphics/script failures stop the player and
+ display a diagnostic instead of silently subs
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Add game builds for Windows, playable websites and Android
Download RusticGameEngine-Setup-0.1.0-x64-20261101-083733.exe
Release · All changed files and full diff · Download patch
Files changed
.github/workflows/quality.yml— modified, +31 / -1Diff preview
@@ -97,6 +97,36 @@ jobs: exit 1 fi + browser-export: + name: Direct browser game export + runs-on: ubuntu-latest + timeout-minutes: 30 + defaults: + run: + working-directory: Engine + shell: bash + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + persist-credentials: false + - name: Browser build dependencies + run: | + sudo apt-get update + sudo apt-get install --yes libclang-dev libwayland-dev libxkbcommon-dev libx11-dev libxcursor-dev libxi-dev libxrandr-dev libegl1 libgl1-mesa-dri + - name: Build the shared WebAssembly game runtime + run: | + python3 -m unittest discover -s tools/tests -v + python3 tools/build-browser-runtime.py --install-sdk --profile dev-fast + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version: '24' + - name: Install browser test tools + run: | + npm ci --prefix tools/browser-tests + npm exec --prefix tools/browser-tests -- playwright install --with-deps chromium + - name: Play aSee the file link for the complete diff.
Engine/.gitignore— modified, +3 / -0Diff preview
See the file link for the complete diff.
Engine/Cargo.lock— modified, +179 / -5Diff preview
See the file link for the complete diff.
Engine/Cargo.toml— modified, +2 / -1Diff preview
@@ -5,6 +5,7 @@ members = [ "apps/editor", "apps/project-manager", "apps/runtime", + "apps/browser", "apps/setup-helper", "crates/engine-assets", "crates/engine-core", @@ -65,7 +66,7 @@ toml_edit = "0.25.13" time = { version = "0.3.55", features = ["formatting", "local-offset", "macros"] } tobj = "4.0.3" uuid = { version = "1.26.0", features = ["serde", "v4"] } -wgpu = { version = "30.0.1", default-features = false, features = ["dx12", "gles", "metal", "parking_lot", "std", "vulkan", "wgsl"] } +wgpu = { version = "30.0.1", default-features = false, features = ["dx12", "gles", "metal", "parking_lot", "std", "vulkan", "wgsl", "fragile-send-sync-non-atomic-wasm"] } winit = { version = "0.30.13", default-features = false, features = ["rwh_06", "x11"] } [workspace.lints.rust]See the file link for the complete diff.
Engine/LICENSES/Gradle-Apache-2.0.txt— added, +421 / -0Diff preview
See the file link for the complete diff.
Engine/LICENSES/README.md— modified, +4 / -0Diff preview
See the file link for the complete diff.
Engine/LICENSES/Ubuntu-Font-Licence.txt— added, +96 / -0Diff preview
See the file link for the complete diff.
Engine/README.md— modified, +2 / -0Diff preview
See the file link for the complete diff.
Engine/THIRD_PARTY.md— modified, +23 / -2Diff preview
See the file link for the complete diff.
Engine/apps/browser/Cargo.toml— added, +33 / -0Diff preview
@@ -0,0 +1,33 @@ +[package] +name = "rustic-browser" +description = "Direct browser game runtime using the shared Rustic simulation and renderer" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +publish = false + +[dependencies] +engine-core = { path = "../../crates/engine-core", version = "0.1.0" } +engine-play = { path = "../../crates/engine-play", version = "0.1.0", default-features = false } +engine-assets = { path = "../../crates/engine-assets", version = "0.1.0" } +renderer-wgpu = { path = "../../crates/renderer-wgpu", version = "0.1.0" } +serde.workspace = true +serde_json.workspace = true +resvg.workspace = true +epaint_default_fonts = "0.36.1" +engine-scripting = { path = "../../crates/engine-scripting", version = "0.1.0" } + +# Exported Emscripten entry points require no_mangle. Only those attributes are +# allowed below; the runtime contains no unsafe operations or raw-pointer API. +[lints.rust] +unsafe_code = "deny" +unused_must_use = "deny" + +[dev-dependencies] +tempfile.workspace = true + +[target.'cfg(target_os = "emscripten")'.dependencies] +rquickjs = { workspace = true, features = ["bindgen"] } + +[build-dependencies] +cc = "1.4See the file link for the complete diff.
Engine/apps/browser/build.rs— added, +8 / -0Diff preview
@@ -0,0 +1,8 @@ +fn main() { + println!("cargo:rerun-if-changed=src/egl-compat.c"); + if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("emscripten") { + cc::Build::new() + .file("src/egl-compat.c") + .compile("rustic_egl_compat"); + } +}See the file link for the complete diff.
Engine/apps/browser/src/egl-compat.c— added, +38 / -0Diff preview
@@ -0,0 +1,38 @@ +/* wgpu links the EGL 1.5 entry points, while Emscripten exposes EGL 1.4. + * Browser/offscreen contexts use the default display and the equivalent 1.4 + * window-surface operation. Reject attributes that cannot be represented. */ +#include <EGL/egl.h> +#include <stdint.h> +#include <limits.h> + +EGLDisplay eglGetPlatformDisplay(EGLenum platform, void *display, const EGLAttrib *attributes) { + (void)platform; + if (display != EGL_DEFAULT_DISPLAY || (attributes && attributes[0] != EGL_NONE)) return EGL_NO_DISPLAY; + return eglGetDisplay(EGL_DEFAULT_DISPLAY); +} +EGLSurface eglCreatePlatformWindowSurface(EGLDisplay display, EGLConfig config, void *window, const EGLAttrib *attributes) { + EGLint converted[64]; + if (attributes) { + unsigned i; + for (i = 0; i < 63 && attributes[i] != EGL_NONE; ++i) { + if (attributes[i] < INT_MIN || attributes[i] > INT_MAX) return EGL_NO_SURFACE; + converted[i] = (EGLint)attributes[i]; + } + if (i == 63) return EGL_NO_SURFACE; + converted[i] = EGL_NONE; + } + return eglCreateWindowSurface(display, config, (EGLNativeWindowType)(intptr_t)window, attributes ?See the file link for the complete diff.
Engine/apps/browser/src/main.rs— added, +109 / -0Diff preview
@@ -0,0 +1,109 @@ +use engine_play::game_session::GameSession; +use engine_play::pointer_input::PointerEvent; +use renderer_wgpu::{BackendRequest, SceneViewportRenderer}; +use serde::Deserialize; +use std::cell::RefCell; +use std::path::Path; +#[path = "../../runtime/src/game_ui.rs"] +mod game_ui; + +struct BrowserGame { + session: GameSession, + renderer: SceneViewportRenderer, + models: std::collections::BTreeMap<engine_core::AssetId, engine_assets::ModelArtifact>, + ui: game_ui::GameUi, +} +thread_local! { static GAME: RefCell<Option<BrowserGame>> = const { RefCell::new(None) }; } + +#[derive(Default, Deserialize)] +#[serde(default)] +struct Input { + keys: Vec<String>, + pointers: Vec<PointerEvent>, + reset: bool, +} + +fn initialize() -> Result<(), String> { + let root = Path::new("/game"); + let session = GameSession::load(root)?; + let models = engine_assets::load_model_library(root) + .map_err(|e| e.to_string())? + .into_iter() + .map(|(id, (_, model))| (id, model)) + .collect(); + let mut renderer = + SceneViewportRenderer::new(BackendRequest::OpenGl).map_err(|e| e.to_string())?; + renderer.set_environmSee the file link for the complete diff.
Engine/apps/editor/Cargo.toml— modified, +4 / -2Diff preview
See the file link for the complete diff.
Engine/apps/editor/src/main.rs— modified, +9 / -0Diff preview
@@ -1,6 +1,7 @@ mod explorer_selection; mod play_session; mod pointer_input; +mod project_build; mod scene_graph; mod script_links; mod transform_widget; @@ -128,6 +129,7 @@ fn app_icon() -> egui::IconData { reason = "independent editor panel and interaction flags are not mutually exclusive states" )] struct EditorApp { + project_build: project_build::ProjectBuild, updates: engine_desktop::DesktopUpdates, update_close_requested: bool, document: AuthoringDocument, @@ -238,6 +240,7 @@ impl EditorApp { let missing_dependencies = missing_project_dependencies(document.project().root()); let show_dependency_prompt = !missing_dependencies.is_empty(); let mut app = Self { + project_build: project_build::ProjectBuild::default(), updates: engine_desktop::DesktopUpdates::default(), update_close_requested: false, document, @@ -1194,6 +1197,11 @@ impl EditorApp { fn top_bar(&mut self, ui: &mut egui::Ui) { egui::MenuBar::new().ui(ui, |ui| { ui.menu_button("File", |ui| { + if ui.button("Build Project…").clicked() { + self.project_See the file link for the complete diff.
Engine/apps/editor/src/play_session.rs— modified, +77 / -57Diff preview
@@ -27,69 +27,15 @@ pub struct EditorPlaySession { impl EditorPlaySession { pub fn start(document: &AuthoringDocument, mode: PlayMode) -> Result<Self, String> { - // Startup scripts may create or select the current camera. Let the live - // runtime choose it after those scripts run, rather than rejecting the - // authoring snapshot before initialization. - let scene = document - .snapshot_bytes() - .map_err(|error| error.to_string())?; let temporary = document .project() .directory(VirtualDirectory::Temp) - .map_err(|error| error.to_string())?; + .map_err(|e| e.to_string())?; let logs = document .project() .directory(VirtualDirectory::Logs) - .map_err(|error| error.to_string())?; - let mut programming = Vec::new(); - GameSettings::load(document.project().root())?; - let settings_bytes = std::fs::read(document.project().root().join(GAME_SETTINGS_FILE)) - .map_err(|error| error.to_string())?; - programming.push(SnapshotInput::new(GAME_SETTINGS_FILE, settings_bytes)); - let manifesSee the file link for the complete diff.
Engine/apps/editor/src/project_build.rs— added, +826 / -0Diff preview
@@ -0,0 +1,826 @@ +//! Project distribution UI and shell-free packaging jobs. +mod portable; +use eframe::egui; +use engine_editor::AuthoringDocument; +use engine_platform::AtomicFileService; +use engine_play::PlayMode; +use engine_project::VirtualDirectory; +use serde::{Deserialize, Serialize}; +use std::fmt::Write as _; +use std::fs; +use std::path::{Component, Path, PathBuf}; +use std::process::Command; +use std::sync::mpsc::{self, Receiver}; + +#[derive(Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +enum Target { + #[default] + Windows, + Website, + Apk, + Aab, +} +impl Target { + fn label(self) -> &'static str { + match self { + Self::Windows => "Windows game (.exe)", + Self::Website => "Website (playable game)", + Self::Apk => "Android APK (debug)", + Self::Aab => "Android App Bundle (release)", + } + } +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(default)] +struct Settings { + target: Target, + name: String, + version: String, + publisher: String, + installer: bool, + welcome: String, + license: String, + icon: String, + banner: String, + deSee the file link for the complete diff.
Engine/apps/editor/src/project_build/GameActivity.java— added, +43 / -0Diff preview
@@ -0,0 +1,43 @@ +package org.rustic.player; + +import android.app.Activity; +import android.os.Bundle; +import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; +import android.webkit.WebView; +import android.webkit.WebChromeClient; +import androidx.webkit.WebViewAssetLoader; +import androidx.webkit.WebViewClientCompat; + +/** Runs the compiled game from packaged assets without a network server. */ +public final class GameActivity extends Activity { + private WebView game; + @Override public void onCreate(Bundle state) { + super.onCreate(state); + game = new WebView(this); + game.getSettings().setJavaScriptEnabled(true); + game.getSettings().setDomStorageEnabled(true); + game.getSettings().setAllowFileAccess(false); + game.getSettings().setAllowContentAccess(false); + game.setWebChromeClient(new WebChromeClient()); + final WebViewAssetLoader assets = new WebViewAssetLoader.Builder() + .addPathHandler("/assets/", new WebViewAssetLoader.AssetsPathHandler(this)) + .build(); + game.setWebViewClient(new WebViewClientCompat() { + @Override public WebResourceSee the file link for the complete diff.
Engine/apps/editor/src/project_build/android-app.gradle— added, +35 / -0Diff preview
@@ -0,0 +1,35 @@ +plugins { id 'com.android.application' version '8.13.1' } +android { + namespace 'org.rustic.player' + compileSdk 36 + buildToolsVersion '36.0.0' + defaultConfig { + applicationId '@APPLICATION_ID@' + minSdk 26 + targetSdk 36 + versionCode @VERSION_CODE@ + versionName '@VERSION_NAME@' + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + def keystore = System.getenv('RUSTIC_ANDROID_KEYSTORE') + if (keystore) { + signingConfigs { + release { + storeFile file(keystore) + storePassword System.getenv('RUSTIC_ANDROID_STORE_PASSWORD') + keyAlias System.getenv('RUSTIC_ANDROID_KEY_ALIAS') + keyPassword System.getenv('RUSTIC_ANDROID_KEY_PASSWORD') + } + } + } + buildTypes { + release { + minifyEnabled false + if (keystore) signingConfig signingConfigs.release + } + } +} +dependencies { implementation 'androidx.webkit:webkit:1.14.0' }See the file link for the complete diff.
Engine/apps/editor/src/project_build/android-settings.gradle— added, +7 / -0Diff preview
@@ -0,0 +1,7 @@ +pluginManagement { repositories { google(); mavenCentral(); gradlePluginPortal() } } +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { google(); mavenCentral() } +} +rootProject.name = 'RusticGame' +include ':app'See the file link for the complete diff.
Engine/apps/editor/src/project_build/player.html— added, +4 / -0Diff preview
@@ -0,0 +1,4 @@ +<!doctype html> +<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1"><title>Rustic Game</title> +<style>html,body{margin:0;width:100%;height:100%;overflow:hidden}body{display:grid;place-items:center}#game{width:100vw;height:100vh}canvas{max-width:100%;max-height:100%;object-fit:contain;touch-action:none}#gpu{position:absolute;visibility:hidden}#status{position:absolute;top:0;left:0;white-space:pre-wrap}#start{position:absolute}</style></head> +<body><canvas id="game" width="640" height="360" tabindex="0" aria-label="Game"></canvas><canvas id="gpu" width="640" height="360" aria-hidden="true"></canvas><p id="status">Ready</p><button id="start">Play game</button><script src="player.js"></script></body></html>See the file link for the complete diff.
Engine/apps/editor/src/project_build/player.js— added, +142 / -0Diff preview
@@ -0,0 +1,142 @@ +/* The exported game runs locally in WebAssembly; only static files are fetched. */ +"use strict"; +const canvas = document.getElementById("game"); +const status = document.getElementById("status"); +const start = document.getElementById("start"); +fetch("game-info.json").then(response => response.json()).then(info => { document.title = info.name; }).catch(() => {}); +const keys = new Set(); +const pointers = []; +let reset = false; +let running = false; +let previous = 0; +let context; +let audio; +let nextAudioTime = 0; +function playAudio(bytes) { + if (!audio || audio.state !== "running" || bytes.length < 8) return; + if (nextAudioTime > audio.currentTime + 0.3) return; + const samples = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + const count = Math.floor(bytes.length / 8); + const buffer = audio.createBuffer(2, count, 48000); + for (let channel = 0; channel < 2; channel++) { + const destination = buffer.getChannelData(channel); + for (let i = 0; i < count; i++) destination[i] = samples.getFloat32((i * 2 + channel) * 4, true); + } + const source = audio.createBufferSource(); + source.buffer = buffer; + source.connect(audiSee the file link for the complete diff.
Engine/apps/editor/src/project_build/portable.rs— added, +396 / -0Diff preview
@@ -0,0 +1,396 @@ +//! Generated exports share one compiled game runtime; projects supply no web/app templates. +use super::{Settings, copy_tree}; +use engine_play::PlaySnapshot; +use engine_scripting::{ScriptLanguage, load_manifest}; +use std::fs; +use std::path::{Path, PathBuf}; + +pub(super) fn runtime_directory(executable: &Path) -> PathBuf { + let directory = executable.parent().unwrap_or_else(|| Path::new(".")); + let directory = if directory.file_name().is_some_and(|name| name == "deps") { + directory.parent().unwrap_or(directory) + } else { + directory + }; + directory.join("browser-runtime") +} + +pub(super) fn runtime_available(directory: &Path) -> bool { + directory.join("rustic-browser.js").is_file() && directory.join("rustic-browser.wasm").is_file() +} + +pub(super) fn validate_scripts(root: &Path) -> Result<(), String> { + let path = root.join("config/scripts.ron"); + if !path.is_file() { + return Ok(()); + } + let loaded = + load_manifest(&fs::read(path).map_err(|e| e.to_string())?).map_err(|e| e.to_string())?; + for entry in loaded.manifest.scripts { + if !matches!( + entry.language, +See the file link for the complete diff.
Engine/apps/runtime/Cargo.toml— modified, +1 / -1Diff preview
@@ -15,10 +15,10 @@ renderer-wgpu = { path = "../../crates/renderer-wgpu", version = "0.1.0" } glam.workspace = true resvg.workspace = true winit.workspace = true +tempfile.workspace = true [dev-dependencies] engine-world = { path = "../../crates/engine-world", version = "0.1.0" } -tempfile.workspace = true [lints] workspace = trueSee the file link for the complete diff.
Engine/apps/runtime/src/game_ui.rs— modified, +8 / -0Diff preview
@@ -44,7 +44,15 @@ impl GameUi { let svg = html_to_svg(&html, &css); let mut options = usvg::Options::default(); let mut fonts = usvg::fontdb::Database::new(); + #[cfg(not(target_os = "emscripten"))] fonts.load_system_fonts(); + #[cfg(target_os = "emscripten")] + { + fonts.load_font_data(epaint_default_fonts::UBUNTU_LIGHT.to_vec()); + fonts.set_serif_family("Ubuntu"); + fonts.set_sans_serif_family("Ubuntu"); + options.font_family = "Ubuntu".into(); + } options.fontdb = std::sync::Arc::new(fonts); let tree = usvg::Tree::from_str(&svg, &options) .map_err(|e| format!("could not layout game UI: {e}"))?;See the file link for the complete diff.
Engine/apps/runtime/src/main.rs— modified, +50 / -4Diff preview
@@ -23,6 +23,7 @@ use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; use winit::window::{Window, WindowId}; mod game_ui; +mod standalone; const TOKEN_ENVIRONMENT_VARIABLE: &str = "RUSTIC_RUNTIME_IPC_TOKEN"; @@ -72,6 +73,10 @@ fn main() -> ExitCode { } fn run(arguments: &[OsString]) -> Result<(), String> { + if arguments.is_empty() { + let executable = std::env::current_exe().map_err(|e| e.to_string())?; + return standalone::run(&executable); + } if !arguments.iter().any(|argument| argument == "--ipc-server") { return Err(usage()); } @@ -183,7 +188,8 @@ fn run_windowed_runtime(config: RuntimeServerConfig) -> Result<(), String> { struct RuntimeWindowApp { finished: Arc<AtomicBool>, - title: &'static str, + title: String, + standalone: Option<standalone::Standalone>, initial_size: [f64; 2], window: Option<Arc<Window>>, renderer: Option<SurfaceRenderer>, @@ -194,10 +200,11 @@ struct RuntimeWindowApp { } impl RuntimeWindowApp { - fn new(finished: Arc<AtomicBool>, title: &'static str, initial_size: [f64; 2]) -> Self { + fn new(finished: Arc<AtomicBool>, title: impl Into<String>, iSee the file link for the complete diff.
Engine/apps/runtime/src/standalone.rs— added, +268 / -0Diff preview
@@ -0,0 +1,268 @@ +//! A distributable game uses this executable as its window and an isolated child simulation. +use super::RuntimeWindowApp; +use engine_play::pointer_input::{PointerDevice, PointerEvent, PointerPhase}; +use engine_play::{ControlRequest, PlayMode, PlaySnapshot, RuntimeLaunch, SupervisedRuntime}; +use std::collections::BTreeSet; +use std::path::Path; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::Duration; +use winit::event::{ElementState, MouseButton, TouchPhase, WindowEvent}; +use winit::event_loop::{ControlFlow, EventLoop}; +use winit::keyboard::{Key, NamedKey}; + +pub(super) struct Standalone { + runtime: SupervisedRuntime, + _session: tempfile::TempDir, + keys: BTreeSet<String>, + cursor: [f64; 2], + mouse_down: bool, +} + +pub(super) fn run(executable: &Path) -> Result<(), String> { + let directory = executable + .parent() + .ok_or("game executable has no parent directory")?; + let data = directory.join("data"); + PlaySnapshot::open(&data).map_err(|e| format!("Could not load packaged game data: {e}"))?; + let session = tempfile::Builder::new() + .prefix("rustic-game-") +See the file link for the complete diff.
Engine/assets/android/gradle-wrapper.jar— added, +0 / -0Diff preview unavailable (binary or omitted by GitHub); see the file link.
Engine/crates/engine-play/Cargo.toml— modified, +5 / -1Diff preview
@@ -6,6 +6,10 @@ edition.workspace = true rust-version.workspace = true publish = false +[features] +default = ["native"] +native = ["dep:interprocess"] + [dependencies] serde_json.workspace = true hound.workspace = true @@ -14,7 +18,7 @@ engine-core = { path = "../engine-core", version = "0.1.0" } engine-scripting = { path = "../engine-scripting", version = "0.1.0" } engine-world = { path = "../engine-world", version = "0.1.0" } glam.workspace = true -interprocess.workspace = true +interprocess = { workspace = true, optional = true } ron.workspace = true serde.workspace = true sha2.workspace = trueSee the file link for the complete diff.
Engine/crates/engine-play/src/audio_output.rs— modified, +18 / -1Diff preview
@@ -87,7 +87,16 @@ impl AudioOutput { if let Some(sender) = &self.sender { let _ = sender.try_send(Command::Samples(samples)); } - #[cfg(not(windows))] + #[cfg(target_os = "emscripten")] + BROWSER_AUDIO.with(|queue| { + let mut queue = queue.borrow_mut(); + // Keep no more than half a second of interleaved stereo audio. + if queue.len() + samples.len() > 48_000 { + queue.clear(); + } + queue.extend(samples.into_iter().take(48_000)); + }); + #[cfg(all(not(windows), not(target_os = "emscripten")))] drop(samples); } } @@ -209,3 +218,11 @@ mod native { } } } + +#[cfg(target_os = "emscripten")] +thread_local! { static BROWSER_AUDIO: std::cell::RefCell<Vec<f32>> = const { std::cell::RefCell::new(Vec::new()) }; } + +#[cfg(target_os = "emscripten")] +pub(crate) fn take_browser_audio() -> Vec<f32> { + BROWSER_AUDIO.with(|queue| std::mem::take(&mut *queue.borrow_mut())) +}See the file link for the complete diff.
Engine/crates/engine-play/src/game_session.rs— added, +70 / -0Diff preview
@@ -0,0 +1,70 @@ +//! Single-threaded game execution shared by browser and embedded runtimes. +use crate::PlaySnapshot; +use crate::pointer_input::PointerEvent; +use crate::script_runtime::RuntimeScripts; +use engine_world::SceneWorld; +use std::path::Path; + +pub struct GameSession { + scripts: RuntimeScripts, + accumulator: f64, +} +impl GameSession { + /// Load verified packaged data and initialize the same gameplay callbacks as local play. + /// # Errors + /// Returns snapshot, scene, settings or script initialization errors. + pub fn load(root: &Path) -> Result<Self, String> { + let snapshot = PlaySnapshot::open(root).map_err(|e| e.to_string())?; + let source = snapshot.scene_bytes().map_err(|e| e.to_string())?; + Ok(Self { + scripts: RuntimeScripts::load(root, &source)?, + accumulator: 0.0, + }) + } + /// Advance bounded real time without launching threads or child processes. + /// # Errors + /// Rejects non-finite or negative frame intervals. + pub fn step( + &mut self, + seconds: f64, + keys: Vec<String>, + pointers: Vec<PointerEvent>, + reset: bool, + )See the file link for the complete diff.
Engine/crates/engine-play/src/lib.rs— modified, +8 / -0Diff preview
@@ -7,14 +7,17 @@ pub mod changes; mod entity_services; pub mod frame_ring; +#[cfg(feature = "native")] pub mod local_ipc; mod physics; pub mod pointer_input; pub mod protocol; +#[cfg(feature = "native")] pub mod runtime_server; mod script_runtime; pub mod simulation; pub mod snapshot; +#[cfg(feature = "native")] pub mod supervisor; pub mod viewport; pub use viewport::{MAX_GAME_DIMENSION, game_view_extent}; @@ -24,6 +27,7 @@ pub use changes::{ RuntimeChangeSet, RuntimeChangeStore, RuntimeChangeTarget, RuntimeValue, }; pub use frame_ring::{BgraFrame, FrameRing, FrameRingError, FrameRingLimits, FrameRingStats}; +#[cfg(feature = "native")] pub use local_ipc::{ AuthenticatedConnection, LocalEndpoint, LocalIpcListener, connect_authenticated, }; @@ -32,6 +36,7 @@ pub use protocol::{ PROTOCOL_VERSION, ProcessDescriptor, ProcessRole, ProtocolError, ProtocolMessage, ProtocolVersion, }; +#[cfg(feature = "native")] pub use runtime_server::{ RuntimeServerConfig, RuntimeServerError, record_runtime_crash, run_runtime_server, run_runtime_server_with_renderer, run_runtime_server_with_viewport_renderer, @@ -43,10 +48,13 @@ pub use snapshot::{ PSee the file link for the complete diff.
Engine/crates/engine-play/src/snapshot.rs— modified, +49 / -4Diff preview
@@ -11,7 +11,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use thiserror::Error; use uuid::Uuid; -pub const SNAPSHOT_FORMAT_VERSION: u32 = 1; +pub const SNAPSHOT_FORMAT_VERSION: u32 = 2; pub const SNAPSHOT_MANIFEST_FILE: &str = "play-snapshot.ron"; /// Caller-owned bytes staged into an isolated runtime mount. @@ -77,7 +77,7 @@ impl PlaySnapshot { })?; let manifest: PlaySnapshotManifest = ron::from_str(&manifest_text) .map_err(|error| SnapshotError::InvalidManifest(error.to_string()))?; - if manifest.format_version != SNAPSHOT_FORMAT_VERSION { + if !matches!(manifest.format_version, 1 | SNAPSHOT_FORMAT_VERSION) { return Err(SnapshotError::UnsupportedVersion { found: manifest.format_version, supported: SNAPSHOT_FORMAT_VERSION, @@ -131,7 +131,11 @@ impl PlaySnapshot { actual: scene_entry.sha256.clone(), }); } - let aggregate = aggregate_hash(&verified); + let aggregate = if manifest.format_version == 1 { + aggregate_hash_legacy(&verified) + } else { + aggregate_hash(&verified) + }; if aSee the file link for the complete diff.
Engine/crates/renderer-wgpu/Cargo.toml— modified, +2 / -0Diff preview
See the file link for the complete diff.
Engine/crates/renderer-wgpu/src/lib.rs— modified, +79 / -5Diff preview
@@ -205,8 +205,12 @@ async fn initialize_adapter( .request_device(&wgpu::DeviceDescriptor { label: Some(label), required_features: wgpu::Features::empty(), - required_limits: wgpu::Limits::downlevel_defaults() - .using_resolution(adapter.limits()), + required_limits: if cfg!(target_os = "emscripten") { + wgpu::Limits::downlevel_webgl2_defaults() + } else { + wgpu::Limits::downlevel_defaults() + } + .using_resolution(adapter.limits()), experimental_features: wgpu::ExperimentalFeatures::disabled(), memory_hints: wgpu::MemoryHints::Performance, trace: wgpu::Trace::Off, @@ -502,6 +506,7 @@ pub enum SurfaceFrameStatus { /// The concrete surface, adapter, device, queue, and swapchain configuration never /// cross this crate boundary. The owned native window keeps raw handles valid for the /// complete surface lifetime. +#[cfg(not(target_os = "emscripten"))] pub struct SurfaceRenderer { window: Arc<winit::window::Window>, request: BackendRequest, @@See the file link for the complete diff.
More files are included in the full diff linked above.
All reactions