mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-16 00:03:29 +00:00
fix headless
This commit is contained in:
+7
-1
@@ -38,6 +38,7 @@ web-sys = { version = "0.3", optional = true, features = [
|
||||
"Document",
|
||||
"HtmlElement",
|
||||
"Window",
|
||||
"Location",
|
||||
"WorkerGlobalScope"
|
||||
] }
|
||||
clap = { version = "4.4.18", features = ["derive"] }
|
||||
@@ -51,10 +52,13 @@ bytemuck = "1.14.1"
|
||||
space_editor = {git = "https://github.com/rewin123/space_editor", rev = "0f15c0d6b6b5864f4a05a2d57dfc2066e96d1a3b"}
|
||||
anyhow.workspace = true
|
||||
serde_repr = "0.1.18"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
|
||||
async-compat = "0.2"
|
||||
uuid = { version = "1", features = ["v4", "serde"]}
|
||||
indexed_db_futures = { version = "0.4", optional = true}
|
||||
wasm-bindgen-futures = { version = "0.4", optional = true}
|
||||
dashmap = "5"
|
||||
once_cell = "1"
|
||||
bevy_tweening = "0.10.0"
|
||||
|
||||
[dependencies.reqwest]
|
||||
@@ -71,6 +75,8 @@ wasm = [
|
||||
"dep:js-sys",
|
||||
"dep:serde-wasm-bindgen",
|
||||
"dep:web-sys",
|
||||
"dep:indexed_db_futures",
|
||||
"dep:wasm-bindgen-futures",
|
||||
"bevy_rapier3d/wasm-bindgen",
|
||||
]
|
||||
desktop = []
|
||||
|
||||
@@ -209,13 +209,14 @@ fn start_timeline_animation(
|
||||
mut ev: EventWriter<TimelineInteractionEvent>,
|
||||
) {
|
||||
if keyframes.is_empty() {
|
||||
return;
|
||||
// return;
|
||||
}
|
||||
commands.remove_resource::<CameraMotion>();
|
||||
let total_time = keyframes
|
||||
.iter()
|
||||
.fold(0f32, |previous, keyframe| previous.max(keyframe.time))
|
||||
.max(3.0);
|
||||
.max(3.0)
|
||||
.min(1.0);
|
||||
|
||||
let frames = (total_time * headless_settings.fps).ceil() as u32;
|
||||
headless_settings.max_frames = frames;
|
||||
|
||||
+165
-5
@@ -6,6 +6,7 @@ use bevy::{
|
||||
prelude::*,
|
||||
utils::BoxedFuture,
|
||||
};
|
||||
|
||||
use reqwest::StatusCode;
|
||||
|
||||
use super::response::GetMediaFileSuccessResponse;
|
||||
@@ -78,6 +79,29 @@ impl RemoteAssetReader {
|
||||
let reader: Box<Reader> = Box::new(VecReader::new(bytes));
|
||||
Ok(reader)
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "wasm", target_family = "wasm"))]
|
||||
async fn load_local<'a>(&'a self, path: &'a Path) -> Result<Box<Reader<'a>>, AssetReaderError> {
|
||||
let token = path
|
||||
.ext_file_prefix()
|
||||
.ok_or_else(|| {
|
||||
AssetReaderError::Io(std::io::Error::new(
|
||||
ErrorKind::Other,
|
||||
format!("Failed to parse token from '{path:?}'"),
|
||||
))
|
||||
})?
|
||||
.to_str()
|
||||
.unwrap();
|
||||
|
||||
let bytes = wasm::LoadLocalFromIdb::new(token);
|
||||
info!("Sent request to bevy");
|
||||
let bytes = bytes
|
||||
.await
|
||||
.map_err(|e| std::io::Error::new(ErrorKind::Other, format!("{e:?}")))?;
|
||||
|
||||
let reader: Box<Reader> = Box::new(VecReader::new(bytes));
|
||||
Ok(reader)
|
||||
}
|
||||
}
|
||||
|
||||
impl AssetReader for RemoteAssetReader {
|
||||
@@ -85,15 +109,25 @@ impl AssetReader for RemoteAssetReader {
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, Result<Box<Reader<'a>>, AssetReaderError>> {
|
||||
let unboxed = async move { self.load(path).await };
|
||||
if cfg!(not(feature = "wasm")) {
|
||||
#[cfg(not(all(feature = "wasm", target_family = "wasm")))]
|
||||
{
|
||||
info!("wasm mode not detected");
|
||||
// NB: Headless mode (server rendering) is running without a Tokio runtime.
|
||||
// The reqwest HTTP client requires async-compat to function at runtime.
|
||||
Box::pin(Compat::new(unboxed))
|
||||
} else {
|
||||
Box::pin(Compat::new(async move { self.load(path).await }))
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "wasm", target_family = "wasm"))]
|
||||
{
|
||||
info!("wasm mode");
|
||||
Box::pin(unboxed)
|
||||
if let Some(window) = web_sys::window() {
|
||||
if let Ok(search) = window.location().search() {
|
||||
if search.contains("mock-api") {
|
||||
return Box::pin(async move { self.load_local(path).await });
|
||||
}
|
||||
}
|
||||
}
|
||||
Box::pin(async move { self.load(path).await })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,3 +165,129 @@ impl IntoError<AssetReaderError> for reqwest::Error {
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "wasm", target_family = "wasm"))]
|
||||
mod wasm {
|
||||
use std::task::Waker;
|
||||
|
||||
use bevy::log::info;
|
||||
use dashmap::DashMap;
|
||||
use futures::Future;
|
||||
use js_sys::Uint8Array;
|
||||
use once_cell::sync::Lazy;
|
||||
use wasm_bindgen::{prelude::*, JsValue};
|
||||
use web_sys::{CustomEvent, CustomEventInit};
|
||||
|
||||
#[wasm_bindgen(typescript_custom_section)]
|
||||
const TIMELINE_TYPES: &str = r#"
|
||||
export interface RequestFileEvent extends CustomEvent {
|
||||
type: "request-file";
|
||||
detail: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
export interface GlobalEventHandlersEventMap {
|
||||
"request-file": RequestFileEvent;
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
// DashMap + Lazy allow us to create static, global hashmaps that work well concurrently
|
||||
static FILE_WAKERS: Lazy<DashMap<String, Waker>> = Lazy::new(DashMap::new);
|
||||
static FILE_RESPONSES: Lazy<DashMap<String, Result<Vec<u8>, String>>> = Lazy::new(DashMap::new);
|
||||
|
||||
/// Notifies us that the requested file couldn't be received
|
||||
#[wasm_bindgen(js_name = requestedFileFailed)]
|
||||
pub fn requested_file_failed(token: String, error: JsValue) {
|
||||
FILE_RESPONSES.insert(token.clone(), Err(format!("Error: {error:?}")));
|
||||
let waker = FILE_WAKERS.remove(&token);
|
||||
if let Some((_, waker)) = waker {
|
||||
waker.wake();
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides us with the requested file's contents
|
||||
#[wasm_bindgen(js_name = requestedFileReceived)]
|
||||
pub fn requested_file_received(token: String, file: Uint8Array) {
|
||||
info!("Received File From Mock API {token}");
|
||||
FILE_RESPONSES.insert(token.clone(), Ok(file.to_vec()));
|
||||
let waker = FILE_WAKERS.remove(&token);
|
||||
if let Some((_, waker)) = waker {
|
||||
waker.wake();
|
||||
}
|
||||
}
|
||||
|
||||
pub enum LoadLocalFromIdb {
|
||||
RequestingFile { token: String },
|
||||
CheckingForFile { token: String },
|
||||
}
|
||||
|
||||
impl LoadLocalFromIdb {
|
||||
pub fn new(file: impl ToString) -> Self {
|
||||
Self::RequestingFile {
|
||||
token: file.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We are implementing Future ourselves here because all the existing IndexedDb rust libraries
|
||||
// rely on JsValue, which is not Send - preventing us from using it in a BoxedFuture.
|
||||
// Instead, we ask the JS front end to grab the data from our mock API, and return it to us.
|
||||
impl Future for LoadLocalFromIdb {
|
||||
type Output = Result<Vec<u8>, String>;
|
||||
|
||||
fn poll(
|
||||
mut self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Self::Output> {
|
||||
match self.as_ref().get_ref() {
|
||||
LoadLocalFromIdb::RequestingFile { token } => {
|
||||
info!("Requesting file {token}");
|
||||
let Some(window) = web_sys::window() else {
|
||||
return std::task::Poll::Ready(Err(
|
||||
"Couldn't find browser window".to_string()
|
||||
));
|
||||
};
|
||||
let event = CustomEvent::new_with_event_init_dict(
|
||||
"request-file",
|
||||
CustomEventInit::new()
|
||||
.bubbles(true)
|
||||
.cancelable(true)
|
||||
.detail(&JsValue::from_str(token)),
|
||||
)
|
||||
.map_err(|err| format!("{err:?}"));
|
||||
let event = match event {
|
||||
Ok(e) => e,
|
||||
Err(err) => {
|
||||
return std::task::Poll::Ready(Err(err));
|
||||
}
|
||||
};
|
||||
FILE_WAKERS.insert(token.clone(), cx.waker().clone());
|
||||
if let Err(err) = window
|
||||
.document()
|
||||
.ok_or(JsValue::null())
|
||||
.and_then(|document| document.dispatch_event(&event))
|
||||
{
|
||||
return std::task::Poll::Ready(Err(format!(
|
||||
"Error Dispatching Message: {err:?}"
|
||||
)));
|
||||
}
|
||||
self.set(LoadLocalFromIdb::CheckingForFile {
|
||||
token: token.clone(),
|
||||
});
|
||||
std::task::Poll::Pending
|
||||
}
|
||||
LoadLocalFromIdb::CheckingForFile { token } => {
|
||||
info!("Checking on file {token}");
|
||||
match FILE_RESPONSES.remove(token) {
|
||||
Some((_, value)) => {
|
||||
info!("Found {token}");
|
||||
std::task::Poll::Ready(value)
|
||||
}
|
||||
None => std::task::Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ fn check_loading(
|
||||
// `Handle<T>`s.
|
||||
if loading_queue.queue.is_empty()
|
||||
&& **r_scene_state == SceneState::Loading
|
||||
&& *frames_without_loading > 100
|
||||
&& *frames_without_loading > 200
|
||||
{
|
||||
info!("Transition to active");
|
||||
st_next.set(SceneState::Active);
|
||||
|
||||
@@ -148,7 +148,7 @@ fn extract_screen_capture_components(
|
||||
screen_capure_comps: Extract<Query<(Entity, &ScreenCapture), With<Camera>>>,
|
||||
scene_state: Extract<Res<State<SceneState>>>,
|
||||
) {
|
||||
if scene_state.get() != &SceneState::Active {
|
||||
if *scene_state.get() != SceneState::Active {
|
||||
return;
|
||||
};
|
||||
for (entity, screen_capture) in screen_capure_comps.iter() {
|
||||
|
||||
@@ -598,7 +598,7 @@ fn serialize_standart_materal(
|
||||
|
||||
ev_inspect.clear();
|
||||
|
||||
if let Some((_, handle)) = r_selection
|
||||
if let Some((_target, handle)) = r_selection
|
||||
.as_ref()
|
||||
.and_then(|selection| find_handle_in_self_or_children(***selection, &q_children, &q_m))
|
||||
.and_then(|target| q_m.get(target).ok().map(|h| (target, h.clone())))
|
||||
|
||||
Reference in New Issue
Block a user