From 5cc557cc5f2c6e26e4756504f607d7de251fb11c Mon Sep 17 00:00:00 2001 From: N Date: Tue, 20 Aug 2024 13:35:56 +0300 Subject: [PATCH 1/4] feat: reorganise luminol's main crate (native + wasm) --- Cargo.lock | 17 +- Cargo.toml | 11 +- crates/result/Cargo.toml | 31 ++ crates/result/src/lib.rs | 52 ++++ src/entrypoint/app/log_window.rs | 144 +++++++++ src/entrypoint/app/mod.rs | 452 +++++++++++++++++++++++++++ src/entrypoint/app/top_bar.rs | 474 ++++++++++++++++++++++++++++ src/entrypoint/mod.rs | 9 + src/{ => entrypoint/native}/log.rs | 0 src/entrypoint/native/mod.rs | 267 ++++++++++++++++ src/entrypoint/web.rs | 365 ++++++++++++++++++++++ src/main.rs | 478 +++-------------------------- 12 files changed, 1860 insertions(+), 440 deletions(-) create mode 100644 crates/result/Cargo.toml create mode 100644 crates/result/src/lib.rs create mode 100644 src/entrypoint/app/log_window.rs create mode 100644 src/entrypoint/app/mod.rs create mode 100644 src/entrypoint/app/top_bar.rs create mode 100644 src/entrypoint/mod.rs rename src/{ => entrypoint/native}/log.rs (100%) create mode 100644 src/entrypoint/native/mod.rs create mode 100644 src/entrypoint/web.rs diff --git a/Cargo.lock b/Cargo.lock index 0c0d027c..161d818a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3068,6 +3068,7 @@ dependencies = [ "luminol-filesystem", "luminol-graphics", "luminol-macros", + "luminol-result", "luminol-term", "luminol-ui", "luminol-web", @@ -3359,6 +3360,18 @@ dependencies = [ "syn 2.0.51", ] +[[package]] +name = "luminol-result" +version = "0.4.0" +dependencies = [ + "color-eyre", + "image 0.25.2", + "steamworks", + "tempfile", + "thiserror", + "tracing-log 0.1.4", +] + [[package]] name = "luminol-term" version = "0.4.0" @@ -4749,9 +4762,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.3" +version = "1.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick", "memchr", diff --git a/Cargo.toml b/Cargo.toml index e74c026b..41397fab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -167,6 +167,7 @@ luminol-proc-macros = { version = "0.4.0", path = "crates/proc-macros/" } luminol-modals = { version = "0.4.0", path = "crates/modals/" } luminol-term = { version = "0.4.0", path = "crates/term/" } luminol-ui = { version = "0.4.0", path = "crates/ui/" } +luminol-result = { version = "0.4.0", path = "crates/result" } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -195,6 +196,7 @@ luminol-config.workspace = true luminol-filesystem.workspace = true luminol-graphics.workspace = true luminol-ui.workspace = true +luminol-result.workspace = true # luminol-windows = { version = "0.1.0", path = "../windows/" } # luminol-tabs = { version = "0.1.0", path = "../tabs/" } @@ -256,6 +258,9 @@ web-sys = { workspace = true, features = [ "Worker", "WorkerOptions", "WorkerType", + "Document", + "Element", + "CssStyleDeclaration", ] } [target.'cfg(target_arch = "wasm32")'.dependencies.wgpu] @@ -263,7 +268,7 @@ workspace = true features = ["webgpu", "webgl"] [features] -steamworks = ["dep:steamworks"] +steamworks = ["dep:steamworks", "luminol-result/steamworks"] [build-dependencies] shadow-rs = { version = "0.32.0", default-features = false } @@ -312,10 +317,6 @@ opt-level = 3 [profile.dev.package.glam] opt-level = 3 -# Backtraces for color-eyre errors and panics -[profile.dev.package.backtrace] -opt-level = 3 - # See why config is set up this way. # https://bevy-cheatbook.github.io/pitfalls/performance.html#why-not-use---release diff --git a/crates/result/Cargo.toml b/crates/result/Cargo.toml new file mode 100644 index 00000000..8517930a --- /dev/null +++ b/crates/result/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "luminol-result" + +version.workspace = true +authors.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +readme.workspace = true +repository.workspace = true +keywords.workspace = true +categories.workspace = true + + +[lints] +workspace = true + +[dependencies] +thiserror.workspace = true +color-eyre.workspace = true +tempfile.workspace = true +image.workspace = true + +[target.'cfg(target_arch = "wasm32")'.dependencies] +tracing-log = "0.1.3" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +steamworks = { optional = true, version = "0.10.0" } + +[features] +steamworks = ["dep:steamworks"] diff --git a/crates/result/src/lib.rs b/crates/result/src/lib.rs new file mode 100644 index 00000000..5fcc8cfe --- /dev/null +++ b/crates/result/src/lib.rs @@ -0,0 +1,52 @@ +// Copyright (C) 2024 Melody Madeline Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this Program, or any covered work, by linking or combining +// it with Steamworks API by Valve Corporation, containing parts covered by +// terms of the Steamworks API by Valve Corporation, the licensors of this +// Program grant you additional permission to convey the resulting work. + +use std::io; + +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[cfg(feature = "steamworks")] + #[error("Steam error: {0}\nPerhaps you want to compile yourself a free copy?")] + Steamworks(#[from] steamworks::SteamError), + #[error("Failed to install color-eyre hooks")] + ColorEyreInstall(#[from] color_eyre::eyre::InstallError), + #[error("I/O Error: {0}")] + Io(#[from] io::Error), + #[error("Temporary file error: {0}")] + TempFilePersist(#[from] tempfile::PersistError), + #[error("Image loader error: {0}")] + Image(#[from] image::ImageError), + #[cfg(target_arch = "wasm32")] + #[error("Failed to initialise tracing-log")] + Tracing(#[from] tracing_log::log::SetLoggerError), + + #[error("Could not get path to the current executable")] + ExePathQueryFailed, + #[error("Egui context cell has been already set (this shouldn't happen!)")] + EguiContextCellAlreadySet, +} + +pub type Result = core::result::Result; diff --git a/src/entrypoint/app/log_window.rs b/src/entrypoint/app/log_window.rs new file mode 100644 index 00000000..d939d829 --- /dev/null +++ b/src/entrypoint/app/log_window.rs @@ -0,0 +1,144 @@ +// Copyright (C) 2024 Melody Madeline Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this Program, or any covered work, by linking or combining +// it with Steamworks API by Valve Corporation, containing parts covered by +// terms of the Steamworks API by Valve Corporation, the licensors of this +// Program grant you additional permission to convey the resulting work. + +use std::{ + collections::VecDeque, + sync::mpsc::{Receiver, Sender}, +}; + +pub struct LogWindow { + pub(super) term_shown: bool, + byte_rx: Receiver, + byte_tx: Sender, + buffer: VecDeque, + term: luminol_term::widget::ChannelTerminal, + save_promise: Option>>, +} +const MAX_BUFFER_CAPACITY: usize = 1 << 24; + +impl LogWindow { + pub fn new( + config: &luminol_config::terminal::Config, + byte_rx: std::sync::mpsc::Receiver, + ) -> Self { + let (byte_tx, term_byte_rx) = std::sync::mpsc::channel(); + let term = luminol_term::widget::Terminal::channel(term_byte_rx, config); + + Self { + byte_rx, + byte_tx, + buffer: VecDeque::new(), + term, + term_shown: false, + save_promise: None, + } + } + + pub fn ui(&mut self, ui: &mut egui::Ui, update_state: &mut luminol_core::UpdateState<'_>) { + let mut did_recv = false; + for byte in self.byte_rx.try_iter() { + let _ = self.byte_tx.send(byte); + + if self.buffer.len() >= MAX_BUFFER_CAPACITY { + self.buffer.pop_front(); + self.buffer.push_back(byte); + } else { + self.buffer.push_back(byte); + } + + did_recv = true; + } + + if did_recv { + update_state.ctx.request_repaint(); + } + + // We update the log terminal even if it's not open so that we don't encounter + // performance problems when the terminal has to parse all the new input at once + self.term.update(); + + egui::Window::new("Log") + .id(self.term.id) + .open(&mut self.term_shown) + .show(ui.ctx(), |ui| { + ui.horizontal(|ui| { + ui.add_space(ui.style().spacing.indent); + + if ui.button("Clear").clicked() { + self.buffer.clear(); + self.term.erase_scrollback_and_viewport(); + } + + if let Some(p) = self.save_promise.take() { + ui.spinner(); + + match p.try_take() { + Ok(Ok(())) => { + luminol_core::info!(update_state.toasts, "Successfully saved log!") + } + Ok(Err(e)) + if !matches!( + e.root_cause().downcast_ref(), + Some(luminol_filesystem::Error::CancelledLoading) + ) => + { + luminol_core::error!( + update_state.toasts, + color_eyre::eyre::eyre!(e) + .wrap_err("Error saving the log to a file") + ) + } + Ok(Err(_)) => {} + Err(p) => self.save_promise = Some(p), + } + } else if ui.button("Save to file").clicked() { + let buffer = self.buffer.make_contiguous().to_vec(); + + self.save_promise = + Some(luminol_core::spawn_future(Self::save_promise(buffer))); + } + }); + + ui.add_space(ui.spacing().item_spacing.y); + + if let Err(e) = self.term.ui(update_state, ui) { + luminol_core::error!( + update_state.toasts, + e.wrap_err("Error displaying log window"), + ); + } + }); + } + + async fn save_promise(buffer: Vec) -> luminol_filesystem::Result<()> { + use futures_lite::AsyncWriteExt; + + let mut tmp = luminol_filesystem::host::File::new()?; + let mut cursor = async_std::io::Cursor::new(buffer); + async_std::io::copy(&mut cursor, &mut tmp).await?; + tmp.flush().await?; + tmp.save("luminol.log", "Log files").await?; + Ok(()) + } +} diff --git a/src/entrypoint/app/mod.rs b/src/entrypoint/app/mod.rs new file mode 100644 index 00000000..8916e938 --- /dev/null +++ b/src/entrypoint/app/mod.rs @@ -0,0 +1,452 @@ +// Copyright (C) 2024 Melody Madeline Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this Program, or any covered work, by linking or combining +// it with Steamworks API by Valve Corporation, containing parts covered by +// terms of the Steamworks API by Valve Corporation, the licensors of this +// Program grant you additional permission to convey the resulting work. + +use crate::{lumi::Lumi, BUILD_DIAGNOSTIC}; +#[cfg(feature = "steamworks")] +use crate::steam::Steamworks; +use std::sync::Arc; + +#[cfg(not(target_arch = "wasm32"))] +mod log_window; +mod top_bar; + +/// The main Luminol struct. Handles rendering, GUI state, that sort of thing. +pub struct App { + top_bar: top_bar::TopBar, + #[cfg(not(target_arch = "wasm32"))] + log: log_window::LogWindow, + lumi: Lumi, + + audio: luminol_audio::Audio, + + graphics: Arc, + filesystem: luminol_filesystem::project::FileSystem, + data: luminol_core::Data, + bytes_loader: Arc, + + toasts: luminol_core::Toasts, + + windows: luminol_core::Windows, + tabs: luminol_core::Tabs, + + global_config: luminol_config::global::Config, + project_config: Option, + + toolbar: luminol_core::ToolbarState, + + modified: luminol_core::ModifiedState, + modified_during_prev_frame: bool, + project_manager: luminol_core::ProjectManager, + + #[cfg(not(target_arch = "wasm32"))] + _runtime: tokio::runtime::Runtime, + + egui_ctx: egui::Context, + + #[cfg(feature = "steamworks")] + steamworks: Steamworks, +} + +macro_rules! let_with_mut_on_native { + ($name:ident, $value:expr) => { + #[cfg(not(target_arch = "wasm32"))] + let mut $name = $value; + #[cfg(target_arch = "wasm32")] + let $name = $value; + }; +} + +impl App { + /// Called once before the first frame. + #[must_use] + pub fn new( + cc: &luminol_eframe::CreationContext<'_>, + report: Option, + modified: luminol_core::ModifiedState, + #[cfg(not(target_arch = "wasm32"))] log_byte_rx: std::sync::mpsc::Receiver, + #[cfg(not(target_arch = "wasm32"))] try_load_path: Option, + #[cfg(target_arch = "wasm32")] audio: luminol_audio::Audio, + #[cfg(feature = "steamworks")] steamworks: Steamworks, + ) -> Self { + luminol_core::set_git_revision(crate::git_revision()); + + let render_state = cc + .wgpu_render_state + .clone() + .expect("wgpu backend not enabled"); + + // Add custom fallback fonts for glyphs that egui's default font doesn't support + let mut fonts = egui::FontDefinitions::default(); + fonts.font_data.insert( + String::from("Source Han Sans Regular"), + egui::FontData::from_owned( + zstd::bulk::decompress( + luminol_macros::include_asset!("assets/fonts/SourceHanSans-Regular.ttc.zst"), + 19485724, + ) + .unwrap(), + ), + ); + + #[cfg(not(target_arch = "wasm32"))] + let fd = zstd::bulk::decompress( + luminol_macros::include_asset!("assets/fonts/IosevkaTermNerdFont-Extended.ttf.zst"), + 11849324, + ) + .unwrap(); + + #[cfg(not(target_arch = "wasm32"))] + fonts + .font_data + .insert("Iosevka Term".to_owned(), egui::FontData::from_owned(fd)); + + fonts + .families + .get_mut(&egui::FontFamily::Proportional) + .unwrap() + .push("Source Han Sans Regular".to_owned()); + fonts + .families + .get_mut(&egui::FontFamily::Monospace) + .unwrap() + .push("Source Han Sans Regular".to_owned()); + + #[cfg(not(target_arch = "wasm32"))] + fonts.families.insert( + egui::FontFamily::Name("Iosevka Term".into()), + vec![ + "Iosevka Term".to_owned(), + "Source Han Sans Regular".to_owned(), + ], + ); + cc.egui_ctx.set_fonts(fonts); + + #[cfg(not(debug_assertions))] + render_state.device.on_uncaptured_error(Box::new(|e| { + use std::fmt::Write; + + let mut message_description = String::new(); + match e { + wgpu::Error::OutOfMemory { source } => { + message_description.push_str("wgpu error: Out of memory\n"); + writeln!(message_description, "{source:#?}").unwrap(); + } + wgpu::Error::Validation { + source, + description, + } => { + message_description.push_str("wgpu error: Validation error\n"); + writeln!(message_description, "{source}").unwrap(); + writeln!(message_description, "---------").unwrap(); + writeln!(message_description, "{}", source.source().unwrap()).unwrap(); + writeln!(message_description, "---------").unwrap(); + writeln!(message_description, "{source:#?}").unwrap(); + writeln!(message_description, "---------").unwrap(); + message_description.push_str(&description); + } + wgpu::Error::Internal { + source, + description, + } => { + message_description.push_str("wgpu error: Internal error\n"); + writeln!(message_description, "{source}").unwrap(); + writeln!(message_description, "---------").unwrap(); + writeln!(message_description, "{}", source.source().unwrap()).unwrap(); + writeln!(message_description, "---------").unwrap(); + writeln!(message_description, "{source:#?}").unwrap(); + writeln!(message_description, "---------").unwrap(); + message_description.push_str(&description); + } + } + rfd::MessageDialog::new() + .set_title("Luminol has crashed!") + .set_level(rfd::MessageLevel::Error) + .set_description(&message_description) + .show(); + + let backtrace = std::backtrace::Backtrace::force_capture(); + rfd::MessageDialog::new() + .set_title("Backtrace") + .set_level(rfd::MessageLevel::Error) + .set_description(&backtrace.to_string()) + .show(); + + std::process::abort(); + })); + + let graphics = std::sync::Arc::new(luminol_graphics::GraphicsState::new(render_state)); + + egui_extras::install_image_loaders(&cc.egui_ctx); + + let storage = cc.storage.unwrap(); + + let_with_mut_on_native!( + global_config, + luminol_eframe::get_value(storage, "SavedState").unwrap_or_default() + ); + let_with_mut_on_native!(project_config, None); + + let_with_mut_on_native!(filesystem, luminol_filesystem::project::FileSystem::new()); + let_with_mut_on_native!(data, luminol_core::Data::default()); + + let_with_mut_on_native!(toasts, luminol_core::Toasts::default()); + + #[cfg(not(target_arch = "wasm32"))] + if let Some(path) = try_load_path { + let path = camino::Utf8PathBuf::from_path_buf(std::path::PathBuf::from(path)) + .expect("project path not utf-8"); + + match filesystem.load_project_from_path(&mut project_config, &mut global_config, path) { + Ok(_) => { + if let Err(e) = + data.load(&filesystem, &mut toasts, project_config.as_mut().unwrap()) + { + luminol_core::error!(toasts, e) + } + } + Err(e) => luminol_core::error!(toasts, e), + } + } + + if let Some(style) = luminol_eframe::get_value::(storage, "EguiStyle") { + cc.egui_ctx.set_style(style); + } + + let bytes_loader = Arc::new(luminol_filesystem::egui_bytes_loader::Loader::new()); + cc.egui_ctx.add_bytes_loader(bytes_loader.clone()); + + let lumi = Lumi::new().expect("failed to load lumi images"); + + #[cfg(not(target_arch = "wasm32"))] + let runtime = tokio::runtime::Builder::new_multi_thread() + .worker_threads(2) // TODO use single threaded runtime + .enable_all() + .build() + .expect("failed to build tokio runtime"); + //enter the runtime permanently + #[cfg(not(target_arch = "wasm32"))] + { + std::mem::forget(runtime.enter()); + } + + #[cfg(not(target_arch = "wasm32"))] + let audio = luminol_audio::Audio::default(); + + Self { + top_bar: top_bar::TopBar::default(), + #[cfg(not(target_arch = "wasm32"))] + log: log_window::LogWindow::new(&global_config.terminal, log_byte_rx), + lumi, + + audio, + graphics, + filesystem, + data, + bytes_loader, + + toasts, + windows: report.map_or_else(luminol_core::Windows::new, |report| { + luminol_core::Windows::new_with_windows(vec![ + luminol_ui::windows::reporter::Window::new(report, crate::git_revision()), + ]) + }), + tabs: luminol_core::Tabs::new_with_tabs( + "luminol_main_tabs", + vec![luminol_ui::tabs::started::Tab::default()], + true, + ), + global_config, + project_config, + toolbar: luminol_core::ToolbarState::default(), + + modified, + modified_during_prev_frame: false, + project_manager: luminol_core::ProjectManager::new(&cc.egui_ctx), + + #[cfg(not(target_arch = "wasm32"))] + _runtime: runtime, + + egui_ctx: cc.egui_ctx.clone(), + + #[cfg(feature = "steamworks")] + steamworks, + } + } +} + +impl luminol_eframe::App for App { + /// Called each time the UI needs repainting, which may be many times per second. + fn update(&mut self, ctx: &egui::Context, _frame: &mut luminol_eframe::Frame) { + #[cfg(not(target_arch = "wasm32"))] + ctx.input(|i| { + if let Some(f) = i.raw.dropped_files.first() { + super::RESTART_AFTER_PANIC.store(true, std::sync::atomic::Ordering::Relaxed); + + let path = f.path.clone().expect("dropped file has no path"); + let path = camino::Utf8PathBuf::from_path_buf(path).expect("path was not utf8"); + + if let Err(e) = self.filesystem.load_project_from_path( + &mut self.project_config, + &mut self.global_config, + path, + ) { + luminol_core::error!( + self.toasts, + color_eyre::eyre::eyre!(e).wrap_err("Error opening dropped project") + ) + } else { + luminol_core::info!( + self.toasts, + format!( + "Successfully opened {:?}", + self.filesystem.project_path().expect("project not open") + ) + ); + } + } + }); + + let mut update_state = luminol_core::UpdateState { + ctx, + audio: &mut self.audio, + graphics: self.graphics.clone(), + filesystem: &mut self.filesystem, + data: &mut self.data, + bytes_loader: self.bytes_loader.clone(), + edit_windows: &mut luminol_core::EditWindows::default(), + edit_tabs: &mut luminol_core::EditTabs::default(), + toasts: &mut self.toasts, + project_config: &mut self.project_config, + global_config: &mut self.global_config, + toolbar: &mut self.toolbar, + modified: self.modified.clone(), + modified_during_prev_frame: &mut self.modified_during_prev_frame, + project_manager: &mut self.project_manager, + build_diagnostics: &BUILD_DIAGNOSTIC + }; + + // If a file/folder picker is open, prevent the user from interacting with the application + // with the mouse. + if update_state.project_manager.is_picker_open() { + egui::Area::new("luminol_picker_overlay".into()).show(ctx, |ui| { + ui.allocate_response( + ui.ctx().input(|i| i.screen_rect.size()), + egui::Sense::click_and_drag(), + ); + }); + } + + egui::TopBottomPanel::top("top_toolbar").show(ctx, |ui| { + // We want the top menubar to be horizontal. Without this it would fill up vertically. + ui.horizontal_wrapped(|ui| { + // Turn off button frame. + ui.visuals_mut().button_frame = false; + // Show the bar + self.top_bar.ui(ui, &mut update_state); + + // Handle loading and closing projects but don't show the unsaved changes modal + // because we're going to do that after the windows and tabs are also displayed so + // that it doesn't take an extra frame for the modal to be shown if the windows or + // tabs load or close a project. + update_state.manage_projects(false); + + // Process edit tabs for any changes made by top bar. + // If we don't do this before displaying windows and tabs, any changes made by the top bar will be delayed a frame. + // This means closing the project, for example, won't close tabs until the frame after. + self.tabs + .process_edit_tabs(std::mem::take(update_state.edit_tabs)); + self.windows + .process_edit_windows(std::mem::take(update_state.edit_windows)); + }); + }); + + // Central panel with tabs. + egui::CentralPanel::default() + .frame(egui::Frame::central_panel(&ctx.style()).inner_margin(0.)) + .show(ctx, |ui| { + ui.group(|ui| self.tabs.ui_without_edit(ui, &mut update_state)); + + // Show the log window if it's open. + #[cfg(not(target_arch = "wasm32"))] + { + if self.top_bar.show_log { + self.top_bar.show_log = false; + self.log.term_shown = true; + } + self.log.ui(ui, &mut update_state); + } + }); + + // Update all windows. + self.windows.display_without_edit(ctx, &mut update_state); + + // Handle loading and closing projects, and if applicable, show the modal asking the user + // if they want to save their changes. + update_state.manage_projects(true); + + // If we don't do this tabs added by windows won't be added. + // It also cleans up code nicely. + self.tabs + .process_edit_tabs(std::mem::take(update_state.edit_tabs)); + self.windows + .process_edit_windows(std::mem::take(update_state.edit_windows)); + + // Create toasts for any texture loading errors encountered this frame. + for error in self.graphics.texture_errors() { + luminol_core::error!(self.toasts, error); + } + + // Show toasts. + self.toasts.show(ctx); + + self.lumi.ui(ctx); + + super::RESTART_AFTER_PANIC.store(true, std::sync::atomic::Ordering::Relaxed); + + self.bytes_loader.load_unloaded_files(ctx, &self.filesystem); + + #[cfg(feature = "steamworks")] + self.steamworks.update(); + + self.modified_during_prev_frame = self.modified.get_this_frame(); + self.modified.set_this_frame(false); + + // Call the exit handler if the user or the app requested to close the window. + #[cfg(not(target_arch = "wasm32"))] + if ctx.input(|i| i.viewport().close_requested()) && self.modified.get() { + self.project_manager.quit(); + ctx.send_viewport_cmd(egui::ViewportCommand::CancelClose); + } + } + + /// Called by the frame work to save state before shutdown. + fn save(&mut self, storage: &mut dyn luminol_eframe::Storage) { + luminol_eframe::set_value(storage, "EguiStyle", &self.egui_ctx.style()); + luminol_eframe::set_value(storage, "SavedState", &self.global_config); + } + + fn persist_egui_memory(&self) -> bool { + true + } +} diff --git a/src/entrypoint/app/top_bar.rs b/src/entrypoint/app/top_bar.rs new file mode 100644 index 00000000..bd6d1126 --- /dev/null +++ b/src/entrypoint/app/top_bar.rs @@ -0,0 +1,474 @@ +// Copyright (C) 2024 Melody Madeline Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this Program, or any covered work, by linking or combining +// it with Steamworks API by Valve Corporation, containing parts covered by +// terms of the Steamworks API by Valve Corporation, the licensors of this +// Program grant you additional permission to convey the resulting work. + +use strum::IntoEnumIterator; + +/// The top bar for managing the project. +#[derive(Default)] +pub struct TopBar { + #[cfg(not(target_arch = "wasm32"))] + fullscreen: bool, + #[cfg(not(target_arch = "wasm32"))] + pub(super) show_log: bool, +} + +impl TopBar { + /// Display the top bar. + #[allow(unused_variables)] + pub fn ui(&mut self, ui: &mut egui::Ui, update_state: &mut luminol_core::UpdateState<'_>) { + #[cfg(not(target_arch = "wasm32"))] + { + let old_fullscreen = self.fullscreen; + ui.checkbox(&mut self.fullscreen, "Fullscreen"); + if self.fullscreen != old_fullscreen { + update_state + .ctx + .send_viewport_cmd(egui::ViewportCommand::Fullscreen(self.fullscreen)); + } + + ui.separator(); + } + + let mut open_project = ui.input(|i| i.modifiers.command && i.key_pressed(egui::Key::O)) + && update_state.filesystem.project_loaded(); + let mut save_project = ui.input(|i| i.modifiers.command && i.key_pressed(egui::Key::S)) + && update_state.filesystem.project_loaded(); + if ui.input(|i| i.modifiers.command && i.key_pressed(egui::Key::N)) { + update_state + .edit_windows + .add_window(luminol_ui::windows::new_project::Window::default()); + } + + ui.menu_button("File", |ui| { + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); + + // Hide this menu if the unsaved changes modal or a file/folder picker is open + if update_state.project_manager.is_modal_open() + || update_state.project_manager.is_picker_open() + { + ui.close_menu(); + } + + ui.label(if let Some(path) = update_state.filesystem.project_path() { + format!("Current project:\n{}", path) + } else { + "No project open".to_string() + }); + + ui.add_enabled_ui( + update_state + .project_manager + .load_filesystem_promise + .is_none(), + |ui| { + if ui.button("New Project").clicked() { + update_state + .edit_windows + .add_window(luminol_ui::windows::new_project::Window::default()); + } + + open_project |= ui.button("Open Project").clicked(); + }, + ); + + ui.separator(); + + ui.add_enabled_ui(update_state.filesystem.project_loaded(), |ui| { + if ui.button("Close Project").clicked() { + update_state.project_manager.close_project(); + } + + save_project |= ui.button("Save Project").clicked(); + }); + + #[cfg(not(target_arch = "wasm32"))] + { + ui.separator(); + + if ui.button("Quit").clicked() { + update_state + .ctx + .send_viewport_cmd(egui::ViewportCommand::Close); + } + } + }); + + ui.separator(); + + ui.menu_button("Edit", |ui| { + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); + + // Hide this menu if the unsaved changes modal or a file/folder picker is open + if update_state.project_manager.is_modal_open() + || update_state.project_manager.is_picker_open() + { + ui.close_menu(); + } + + if ui.button("Preferences").clicked() { + update_state + .edit_windows + .add_window(luminol_ui::windows::preferences::Window::default()) + } + + ui.add_enabled_ui(update_state.filesystem.project_loaded(), |ui| { + if ui.button("Project Config").clicked() { + let config = update_state.project_config.as_ref().unwrap(); + update_state + .edit_windows + .add_window(luminol_ui::windows::config_window::Window::new(config)); + } + + if ui.button("Event Commands").clicked() { + // update_state.windows.add_window( + // luminol_ui::windows::command_gen::CommandGeneratorWindow::default(), + // ); + } + }); + }); + + ui.separator(); + + ui.menu_button("Data", |ui| { + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); + + // Hide this menu if the unsaved changes modal or a file/folder picker is open + if update_state.project_manager.is_modal_open() + || update_state.project_manager.is_picker_open() + { + ui.close_menu(); + } + + ui.add_enabled_ui(update_state.filesystem.project_loaded(), |ui| { + if ui.button("Maps").clicked() { + update_state + .edit_windows + .add_window(luminol_ui::windows::map_picker::Window::default()); + } + + ui.add_enabled_ui(false, |ui| { + if ui.button("Tilesets [TODO]").clicked() { + todo!(); + } + }); + + if ui.button("Animations").clicked() { + update_state + .edit_windows + .add_window(luminol_ui::windows::animations::Window::default()); + } + + if ui.button("Common Events").clicked() { + update_state + .edit_windows + .add_window(luminol_ui::windows::common_event_edit::Window::default()); + } + + if ui.button("Scripts").clicked() { + update_state + .edit_windows + .add_window(luminol_ui::windows::script_edit::Window::default()); + } + + if ui.button("Sound Test").clicked() { + update_state.edit_windows.add_window( + luminol_ui::windows::sound_test::Window::new(update_state.filesystem), + ); + } + + ui.add_enabled_ui(false, |ui| { + if ui.button("System [TODO]").clicked() { + todo!(); + } + }); + + ui.separator(); + + if ui.button("Items").clicked() { + update_state + .edit_windows + .add_window(luminol_ui::windows::items::Window::new(update_state)); + } + + if ui.button("Skills").clicked() { + update_state + .edit_windows + .add_window(luminol_ui::windows::skills::Window::new()); + } + + if ui.button("Weapons").clicked() { + update_state + .edit_windows + .add_window(luminol_ui::windows::weapons::Window::new()); + } + + if ui.button("Armor").clicked() { + update_state + .edit_windows + .add_window(luminol_ui::windows::armor::Window::new()); + } + + if ui.button("States").clicked() { + update_state + .edit_windows + .add_window(luminol_ui::windows::states::Window::new()); + } + + ui.separator(); + + if ui.button("Actors").clicked() { + update_state + .edit_windows + .add_window(luminol_ui::windows::actors::Window::new(update_state)); + } + + if ui.button("Classes").clicked() { + update_state + .edit_windows + .add_window(luminol_ui::windows::classes::Window::new()); + } + + if ui.button("Enemies").clicked() { + update_state + .edit_windows + .add_window(luminol_ui::windows::enemies::Window::new(update_state)); + } + + ui.add_enabled_ui(false, |ui| { + if ui.button("Troops [TODO]").clicked() { + todo!(); + } + }); + }); + }); + + ui.separator(); + + ui.menu_button("Tools", |ui| { + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); + + // Hide this menu if the unsaved changes modal or a file/folder picker is open + if update_state.project_manager.is_modal_open() + || update_state.project_manager.is_picker_open() + { + ui.close_menu(); + } + + if ui.button("RGSSAD Archive Manager").clicked() { + update_state + .edit_windows + .add_window(luminol_ui::windows::archive_manager::Window::default()); + } + + if ui.button("Script Manager").clicked() { + update_state + .edit_windows + .add_window(luminol_ui::windows::script_manager::Window::default()); + } + }); + + ui.separator(); + + ui.menu_button("Help", |ui| { + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); + + // Hide this menu if the unsaved changes modal or a file/folder picker is open + if update_state.project_manager.is_modal_open() + || update_state.project_manager.is_picker_open() + { + ui.close_menu(); + } + + ui.button("Contents").clicked(); + + if ui.button("About...").clicked() { + update_state + .edit_windows + .add_window(luminol_ui::windows::about::Window::default()); + }; + }); + + ui.menu_button("Debug", |ui| { + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); + + // Hide this menu if the unsaved changes modal or a file/folder picker is open + if update_state.project_manager.is_modal_open() + || update_state.project_manager.is_picker_open() + { + ui.close_menu(); + } + + if ui.button("Egui Inspection").clicked() { + update_state + .edit_windows + .add_window(luminol_ui::windows::misc::EguiInspection::default()); + } + + if ui.button("Egui Memory").clicked() { + update_state + .edit_windows + .add_window(luminol_ui::windows::misc::EguiMemory::default()); + } + + #[cfg(debug_assertions)] + { + let mut debug_on_hover = ui.ctx().debug_on_hover(); + ui.toggle_value(&mut debug_on_hover, "Debug on hover"); + ui.ctx().set_debug_on_hover(debug_on_hover); + } + + ui.separator(); + + if ui.button("Filesystem Debug").clicked() { + update_state + .edit_windows + .add_window(luminol_ui::windows::misc::FilesystemDebug::default()); + } + + if ui.button("WGPU Debug Info").clicked() { + update_state + .edit_windows + .add_window(luminol_ui::windows::misc::WgpuDebugInfo::new(update_state)); + } + + #[cfg(not(target_arch = "wasm32"))] + { + ui.separator(); + + if ui.button("Log").clicked() { + self.show_log = true; + } + } + }); + + #[cfg(not(target_arch = "wasm32"))] + { + ui.separator(); + + ui.add_enabled_ui(update_state.filesystem.project_loaded(), |ui| { + if ui.button("Playtest").clicked() { + let program = update_state + .project_config + .as_ref() + .expect("project not loaded") + .project + .playtest_exe + .clone(); + let working_directory = update_state + .filesystem + .project_path() + .expect("project not loaded") + .into_std_path_buf(); + + let exec = luminol_term::widget::ExecOptions { + program: Some(program.clone()), + working_directory: Some(working_directory), + ..Default::default() + }; + + match luminol_ui::windows::console::Window::new(exec.clone(), update_state) { + Ok(w) => update_state.edit_windows.add_window(w), + Err(e) => luminol_core::error!( + update_state.toasts, + color_eyre::eyre::eyre!(e) + .wrap_err(format!("Error starting {program:?}")) + ), + } + } + + if ui.button("Terminal").clicked() { + let working_directory = update_state + .filesystem + .project_path() + .expect("project not loaded") + .into_std_path_buf(); + + let exec = luminol_term::widget::ExecOptions { + working_directory: Some(working_directory), + ..Default::default() + }; + + match luminol_ui::windows::console::Window::new(exec, update_state) { + Ok(w) => update_state.edit_windows.add_window(w), + Err(e) => luminol_core::error!( + update_state.toasts, + color_eyre::eyre::eyre!(e).wrap_err("Error starting shell") + ), + } + } + }); + } + + ui.separator(); + + ui.vertical(|ui| { + ui.add_space(ui.spacing().button_padding.y.max( + (ui.spacing().interact_size.y - ui.text_style_height(&egui::TextStyle::Body)) / 2., + )); + ui.label("Brush:"); + }); + + for brush in luminol_core::Pencil::iter() { + ui.selectable_value(&mut update_state.toolbar.pencil, brush, brush.to_string()); + } + + ui.add(egui::Slider::new( + &mut update_state.toolbar.brush_density, + 0.0..=1.0, + )) + .on_hover_text("The proportion of tiles the brush is able to draw on"); + + let alt_down = ui.input(|i| i.modifiers.alt); + let mut brush_random = update_state.toolbar.brush_random != alt_down; + ui.add(egui::Checkbox::new( + &mut brush_random, "Randomize ID", + )) + .on_hover_text("If enabled, the brush will randomly place tiles out of the selected tiles in the tilepicker instead of placing them in a pattern"); + update_state.toolbar.brush_random = brush_random != alt_down; + + if open_project { + update_state.project_manager.open_project_picker(); + } + + if save_project { + if let Some(config) = update_state.project_config { + match update_state.data.save(update_state.filesystem, config) { + Ok(_) => { + update_state.modified.set(false); + luminol_core::info!(update_state.toasts, "Saved project successfully!"); + } + Err(e) => luminol_core::error!(update_state.toasts, e), + } + } + } + + if update_state + .project_manager + .load_filesystem_promise + .is_some() + { + ui.spinner(); + } + } +} diff --git a/src/entrypoint/mod.rs b/src/entrypoint/mod.rs new file mode 100644 index 00000000..01f1007a --- /dev/null +++ b/src/entrypoint/mod.rs @@ -0,0 +1,9 @@ +#[cfg(not(target_arch = "wasm32"))] +mod native; +#[cfg(target_arch = "wasm32")] +mod web; + +#[cfg(not(target_arch = "wasm32"))] +pub use native::{handle_fatal_error, run}; +#[cfg(target_arch = "wasm32")] +pub use web::{handle_fatal_error, run, worker_start}; diff --git a/src/log.rs b/src/entrypoint/native/log.rs similarity index 100% rename from src/log.rs rename to src/entrypoint/native/log.rs diff --git a/src/entrypoint/native/mod.rs b/src/entrypoint/native/mod.rs new file mode 100644 index 00000000..48d07f93 --- /dev/null +++ b/src/entrypoint/native/mod.rs @@ -0,0 +1,267 @@ +#![allow(clippy::arc_with_non_send_sync)] +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +use once_cell::sync::OnceCell; + +use crate::{ + app, + result::{Error, Result}, +}; +use std::{ + env, + fmt::Write, + fs, + io::Read, + panic, process, + sync::{self, atomic}, + thread, time, +}; + +mod log; + +const FILTERS: &[&str] = &[ + "_", + "core::", + "alloc::", + "cocoa::", + "tokio::", + "winit::", + "accesskit", + "std::rt::", + "std::sys_", + "windows::", + "egui::ui::", + "E as eyre::", + "T as core::", + "egui_dock::", + "std::panic::", + "egui::context::", + "luminol_eframe::", + "std::panicking::", + "egui::containers::", + "glPushClientAttrib", + "std::thread::local::", +]; +const ICON: &[u8] = luminol_macros::include_asset!("assets/icons/icon.png"); + +pub fn handle_fatal_error(why: Error) { + rfd::MessageDialog::new() + .set_title("Error") + .set_level(rfd::MessageLevel::Error) + .set_description(why.to_string()) + .show(); + process::exit(1); +} +fn unwrap_option(opt: Option, err: Error) -> T { + match opt { + Some(val) => val, + None => { + handle_fatal_error(err); + unreachable!() + } + } +} +fn unwrap_result(res: Result) -> T { + match res { + Ok(val) => val, + Err(why) => { + handle_fatal_error(why); + unreachable!(); + } + } +} + +fn load_panic_report() -> Option { + let mut report = None; + if let Some(path) = env::var_os("LUMINOL_PANIC_REPORT_FILE") { + if let Ok(mut file) = fs::File::open(path) { + let mut buffer = String::new(); + if file.read_to_string(&mut buffer).is_ok() { + report = Some(buffer); + } + } + } + report +} + +#[cfg(debug_assertions)] +fn detect_deadlocks() { + thread::spawn(|| loop { + thread::sleep(time::Duration::from_secs(5)); + + let deadlocks = parking_lot::deadlock::check_deadlock(); + if deadlocks.is_empty() { + continue; + } + + rfd::MessageDialog::new() + .set_title("Fatal Error") + .set_level(rfd::MessageLevel::Error) + .set_description(format!( + "Luminol has deadlocked! Please file an issue.\n{} deadlocks detected", + deadlocks.len() + )) + .show(); + for (i, threads) in deadlocks.iter().enumerate() { + let mut description = String::new(); + for t in threads { + writeln!(description, "Thread Id {:#?}", t.thread_id()).unwrap(); + writeln!(description, "{:#?}", t.backtrace()).unwrap(); + } + rfd::MessageDialog::new() + .set_title(&format!("Deadlock #{i}")) + .set_level(rfd::MessageLevel::Error) + .set_description(&description) + .show(); + } + + process::abort(); + }); +} + +fn setup_hooks() -> Result<()> { + let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default() + .panic_section(format!("Luminol version: {}", crate::git_revision())) + .add_frame_filter(Box::new(|frames| { + frames.retain(|frame| { + !FILTERS.iter().any(|f| { + frame.name.as_ref().is_some_and(|name| { + name.starts_with(|c: char| c.is_ascii_uppercase()) + || name.strip_prefix('<').unwrap_or(name).starts_with(f) + }) + }) + }) + })) + .into_hooks(); + eyre_hook.install()?; + panic::set_hook(Box::new(move |info| { + let report = panic_hook.panic_report(info).to_string(); + eprintln!("{report}"); + + if !RESTART_AFTER_PANIC.load(atomic::Ordering::Relaxed) { + return; + } + + let mut args = env::args_os(); + let arg0 = args.next(); + let exe_path = env::current_exe().map_or_else( + |_| unwrap_option(arg0, Error::ExePathQueryFailed), + |exe_path| exe_path.into_os_string(), + ); + + let mut file = unwrap_result(tempfile::NamedTempFile::new().map_err(Error::Io)); + let path = unwrap_result(|report: String| -> Result { + use std::io::Write; + + file.write_all(report.as_bytes())?; + file.flush()?; + let (_, path) = file.keep()?; + Ok(path) + }(report)); + env::set_var("LUMINOL_PANIC_REPORT_FILE", &path); + + #[cfg(unix)] + { + use std::os::unix::process::CommandExt; + + let error = process::Command::new(exe_path).args(args).exec(); + eprintln!("Failed to restart Luminol: {error:?}"); + let _ = fs::remove_file(&path); + } + #[cfg(not(unix))] + { + if let Err(error) = std::process::Command::new(exe_path).args(args).spawn() { + eprintln!("Failed to restart Luminol: {error:?}"); + let _ = std::fs::remove_file(&path); + } + } + })); + + Ok(()) +} + +fn init_log() -> (sync::Arc>, sync::mpsc::Receiver) { + let (log_byte_tx, log_byte_rx) = sync::mpsc::channel(); + let ctx_cell = sync::Arc::new(OnceCell::new()); + log::initialize_log(log_byte_tx, ctx_cell.clone()); + (ctx_cell, log_byte_rx) +} + +fn run_app( + report: Option, + ctx_cell: sync::Arc>, + log_byte_rx: sync::mpsc::Receiver, +) -> Result<()> { + let icon_image = image::load_from_memory(ICON)?; + + luminol_eframe::run_native( + "Luminol", + luminol_eframe::NativeOptions { + viewport: egui::ViewportBuilder::default() + .with_drag_and_drop(true) + .with_icon(egui::IconData { + width: icon_image.width(), + height: icon_image.height(), + rgba: icon_image.to_rgba8().to_vec(), + }) + .with_app_id("astrabit.luminol"), + wgpu_options: luminol_egui_wgpu::WgpuConfiguration { + supported_backends: wgpu::util::backend_bits_from_env() + .unwrap_or(wgpu::Backends::PRIMARY | wgpu::Backends::SECONDARY), + // TODO: Load this value from a settings file + power_preference: wgpu::util::power_preference_from_env() + .unwrap_or(wgpu::PowerPreference::LowPower), + ..Default::default() + }, + persist_window: true, + + ..Default::default() + }, + Box::new(move |cc| { + unwrap_result( + ctx_cell + .set(cc.egui_ctx.clone()) + .map_err(|_| Error::EguiContextCellAlreadySet), + ); + + Ok(Box::new(app::App::new( + cc, + report, + Default::default(), + log_byte_rx, + std::env::args_os().nth(1), + #[cfg(feature = "steamworks")] + steamworks, + ))) + }), + ); + + Ok(()) +} + +pub fn run() -> Result<()> { + /* Load the latest panic report */ + let report = load_panic_report(); + + /* Initialise the Steamworks Application Programming Interface */ + #[cfg(feature = "steamworks")] + let steamworks = crate::steam::Steamworks::new()?; + + /* Detect deadlocks */ + #[cfg(debug_assertions)] + detect_deadlocks(); + + /* Enable full backtraces */ + env::set_var("RUST_BACKTRACE", "full"); + + /* Set up hooks for formatting errors and panics */ + setup_hooks()?; + + /* Initialise the log system */ + let (ctx_cell, log_byte_rx) = init_log(); + + /* Show the graphical user interface */ + run_app(report, ctx_cell, log_byte_rx)?; + + Ok(()) +} diff --git a/src/entrypoint/web.rs b/src/entrypoint/web.rs new file mode 100644 index 00000000..a795ff32 --- /dev/null +++ b/src/entrypoint/web.rs @@ -0,0 +1,365 @@ +use crate::{ + git_revision, + result::{Error, Result}, + RESTART_AFTER_PANIC, +}; +use std::{rc::Rc, sync::Arc}; +use wasm_bindgen::{closure::Closure, prelude::wasm_bindgen, JsCast}; + +type PanicSender = + Arc>>>; + +const CANVAS_ID: &str = "luminol-canvas"; + +static WORKER_DATA: parking_lot::Mutex> = parking_lot::Mutex::new(None); + +struct WorkerData { + report: Option, + audio: luminol_audio::Audio, + modified: luminol_core::ModifiedState, + prefers_color_scheme_dark: Option, + fs_worker_channels: luminol_filesystem::web::WorkerChannels, + runner_worker_channels: luminol_eframe::web::WorkerChannels, + runner_panic_tx: std::sync::Arc>>>, +} + +#[wasm_bindgen( + inline_js = "let report = null; export function get_panic_report() { return report; }; export function set_panic_report(r) { report = r; window.restartLuminol(); };" +)] +extern "C" { + fn get_panic_report() -> Option; + fn set_panic_report(r: String); +} + +pub fn handle_fatal_error(why: Error) { + handle_fatal_error_str(why.to_string()); +} +fn handle_fatal_error_str>(text: Str) { + let mut panicked = false; + let mut text: String = text.into(); + let mut location: Option = None; + + if text.starts_with("The application panicked") { + panicked = true; + let ntext = text.clone(); + let lines: Vec<&str> = ntext.lines().collect(); + + text = lines[1].split(':').collect::>()[1] + .trim() + .to_string(); + location = Some( + lines[2].split(':').collect::>()[1] + .trim() + .to_string(), + ) + } + + let window = web_sys::window().expect("could not get `window` object"); + let document = window + .document() + .expect("could not get `window.document` object"); + + let div = document + .create_element("div") + .expect("could not create a `div` element") + .unchecked_into::(); + let div_style = div.style(); + let _ = div_style.set_property("position", "absolute"); + let _ = div_style.set_property("top", "50%"); + let _ = div_style.set_property("left", "50%"); + let _ = div_style.set_property("transform", "translate(-50%, -50%)"); + let _ = div_style.set_property("color", "white"); + let _ = div_style.set_property("display", "flex"); + + let img = document + .create_element("img") + .expect("could not create an `` element") + .unchecked_into::(); + img.set_src("./icon-256.png"); + img.set_width(128); + + let msg_div = document + .create_element("div") + .expect("could not create a `div` element") + .unchecked_into::(); + let msg_div_style = msg_div.style(); + let _ = msg_div_style.set_property("padding-left", "1rem"); + let _ = msg_div_style.set_property("font-family", "monospace"); + + let h1 = document + .create_element("h1") + .expect("could not create a `h2` element"); + h1.set_inner_html("Oops! Luminol crashed."); + + let p = document + .create_element("p") + .expect("could not create a `p`"); + p.set_inner_html( + if panicked { + format!("{text}

Location: {}", location.unwrap()) + } else { + text + } + .as_str(), + ); + + msg_div + .append_child(&h1) + .expect("could not append a `

` to `
`'s body"); + msg_div + .append_child(&p) + .expect("could not append a `

` to `

`'s body"); + div.append_child(&img) + .expect("could not append an `` to `
`'s body"); + div.append_child(&msg_div) + .expect("could not append a `
` to the root `
`"); + document + .body() + .expect("could not get `document.body` object") + .append_child(&div) + .expect("could not append a `
` to the document's body"); +} + +fn setup_hooks(panic_tx: PanicSender) { + let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default() + .panic_section(format!("Luminol version: {}", git_revision())) + .theme(color_eyre::config::Theme::new()) // owo-colors doesn't work on web + .into_hooks(); + eyre_hook + .install() + .expect("failed to install color-eyre hooks"); + std::panic::set_hook(Box::new(move |info| { + let report = panic_hook.panic_report(info).to_string(); + web_sys::console::log_1(&report.as_str().into()); + + // Send the panic report to the main thread to be persisted + // We need to send the panic report to the main thread because JavaScript global variables + // are thread-local and this panic handler runs on the thread that panicked + if let Some(mut panic_tx) = panic_tx.try_lock() { + if let Some(panic_tx) = panic_tx.take() { + let _ = panic_tx.send(report); + } + } + })); +} + +fn check_cross_origin_isolation() { + if !luminol_web::bindings::cross_origin_isolated() { + tracing::error!( + "Cross-Origin Isolation is not enabled. Reloading the page in an attempt to enable it." + ); + web_sys::window() + .expect("could not get `window` object") + .location() + .reload() + .expect("could not reload the page"); + } +} + +fn get_canvases() -> (web_sys::HtmlCanvasElement, web_sys::OffscreenCanvas) { + let window = web_sys::window().expect("could not get `window` object"); + let document = window + .document() + .expect("could not get `window.document` object"); + let canvas = document + .create_element("canvas") + .expect("could not create a `` element") + .unchecked_into::(); + document + .get_element_by_id(CANVAS_ID) + .expect(format!("could not find an element with the id of `{CANVAS_ID}`").as_str()) + .replace_children_with_node_1(&canvas); + let offscreen_canvas = canvas + .transfer_control_to_offscreen() + .expect("could not transfer canvas control to offscreen"); + + (canvas, offscreen_canvas) +} +fn prefers_color_scheme_dark() -> Option { + let window = web_sys::window().expect("could not get `window` object"); + window + .match_media("(prefers-color-scheme: dark)") + .unwrap() + .map(|x| x.matches()) +} + +pub fn init_fs() -> luminol_filesystem::web::WorkerChannels { + let (fs_worker_channels, fs_main_channels) = luminol_filesystem::web::channels(); + luminol_filesystem::web::setup_main_thread_hooks(fs_main_channels); + fs_worker_channels +} + +pub fn launch_worker( + canvas: web_sys::HtmlCanvasElement, + offscreen_canvas: web_sys::OffscreenCanvas, + report: Option, + prefers_color_scheme_dark: Option, + fs_worker_channels: luminol_filesystem::web::WorkerChannels, + worker_cell: Rc>, + before_unload_cell: Rc>>>, +) { + let window = web_sys::window().expect("could not get `window` object"); + + let (runner_worker_channels, runner_main_channels) = luminol_eframe::web::channels(); + let runner_panic_tx = + luminol_eframe::WebRunner::setup_main_thread_hooks(luminol_eframe::web::MainState { + inner: Default::default(), + text_agent: Default::default(), + canvas: canvas.clone(), + channels: runner_main_channels, + }) + .expect("unable to setup web runner main thread hooks"); + let modified_state = luminol_core::ModifiedState::default(); + + *WORKER_DATA.lock() = Some(WorkerData { + report, + audio: luminol_audio::Audio::default(), + modified: modified_state.clone(), + prefers_color_scheme_dark, + fs_worker_channels, + runner_worker_channels, + runner_panic_tx, + }); + + // Show confirmation dialogue if the user tries to close the browser tab while there are + // unsaved changes in the current project + { + let closure: Closure = Closure::new(move |e: web_sys::BeforeUnloadEvent| { + if modified_state.get() { + // Recommended method of activating the confirmation dialogue + e.prevent_default(); + // Fallback for Chromium < 119 + e.set_return_value("arbitrary non-empty string"); + } + }); + window + .add_event_listener_with_callback("beforeunload", closure.as_ref().unchecked_ref()) + .expect("failed to add beforeunload listener"); + *before_unload_cell.borrow_mut() = Some(closure); + } + + canvas.focus().expect("could not focus the canvas"); + + let mut worker_options = web_sys::WorkerOptions::new(); + worker_options.name("luminol-primary"); + worker_options.type_(web_sys::WorkerType::Module); + let worker = web_sys::Worker::new_with_options("./worker.js", &worker_options) + .expect("failed to spawn web worker"); + worker_cell.set(worker.clone()).unwrap(); + + let message = js_sys::Array::new(); + message.push(&wasm_bindgen::module()); + message.push(&wasm_bindgen::memory()); + message.push(&offscreen_canvas); + let transfer = js_sys::Array::new(); + transfer.push(&offscreen_canvas); + worker + .post_message_with_transfer(&message, &transfer) + .expect("failed to post message to web worker"); +} + +pub fn run() -> Result<()> { + /* Load the latest panic report */ + let report = get_panic_report(); + + let worker_cell = Rc::new(once_cell::unsync::OnceCell::::new()); + let before_unload_cell = Rc::new(std::cell::RefCell::new( + None::>, + )); + let (panic_tx, panic_rx) = oneshot::channel(); + let panic_tx = Arc::new(parking_lot::Mutex::new(Some(panic_tx))); + + { + let worker_cell = worker_cell.clone(); + let before_unload_cell = before_unload_cell.clone(); + + wasm_bindgen_futures::spawn_local(async move { + if let Ok(report) = panic_rx.await { + if let Some(worker) = worker_cell.get() { + worker.terminate(); + } + + if let (Some(window), Some(closure)) = + (web_sys::window(), before_unload_cell.take()) + { + let _ = window.remove_event_listener_with_callback( + "beforeunload", + closure.as_ref().unchecked_ref(), + ); + } + + if RESTART_AFTER_PANIC.load(std::sync::atomic::Ordering::Relaxed) { + set_panic_report(report); + } else { + handle_fatal_error_str(report); + } + } + }); + } + + /* Set up hooks for formatting errors and panics */ + setup_hooks(panic_tx); + + /* Redirect tracing to console.log and friends */ + tracing_wasm::set_as_global_default_with_config( + tracing_wasm::WASMLayerConfigBuilder::new() + .set_max_level(tracing::Level::INFO) + .build(), + ); + + /* Redirect log (mainly egui) to tracing */ + tracing_log::LogTracer::init()?; + + /* Check if the Cross-Origin Isolation header is enabld */ + check_cross_origin_isolation(); + + /* Create canvases */ + let (canvas, offscreen_canvas) = get_canvases(); + /* Check if the user prefers the dark colour scheme */ + let prefers_color_scheme_dark = prefers_color_scheme_dark(); + + /* Initialise the file system driver */ + let fs_worker_channels = init_fs(); + + launch_worker( + canvas, + offscreen_canvas, + report, + prefers_color_scheme_dark, + fs_worker_channels, + worker_cell, + before_unload_cell, + ); + + Ok(()) +} + +#[wasm_bindgen] +pub async fn worker_start(canvas: web_sys::OffscreenCanvas) { + let WorkerData { + report, + audio, + modified, + prefers_color_scheme_dark, + fs_worker_channels, + runner_worker_channels, + runner_panic_tx, + } = WORKER_DATA.lock().take().unwrap(); + + luminol_filesystem::host::FileSystem::setup_worker_channels(fs_worker_channels); + + let web_options = luminol_eframe::WebOptions::default(); + + luminol_eframe::WebRunner::new(runner_panic_tx) + .start( + canvas, + web_options, + Box::new(|cc| Ok(Box::new(crate::app::App::new(cc, report, modified, audio)))), + luminol_eframe::web::WorkerOptions { + prefers_color_scheme_dark, + channels: runner_worker_channels, + }, + ) + .await + .expect("failed to start eframe"); +} diff --git a/src/main.rs b/src/main.rs index ff68eca4..c8b92d59 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,48 +14,36 @@ // // You should have received a copy of the GNU General Public License // along with Luminol. If not, see . +// // Additional permission under GNU GPL version 3 section 7 // // If you modify this Program, or any covered work, by linking or combining // it with Steamworks API by Valve Corporation, containing parts covered by // terms of the Steamworks API by Valve Corporation, the licensors of this // Program grant you additional permission to convey the resulting work. -#![cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))] -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release #![cfg_attr(target_arch = "wasm32", no_main)] // there is no main function in web builds shadow_rs::shadow!(build); -#[cfg(not(target_arch = "wasm32"))] -use std::io::{Read, Write}; - -#[cfg(target_arch = "wasm32")] -use wasm_bindgen::prelude::*; +pub(crate) use luminol_result as result; -#[cfg(not(target_arch = "wasm32"))] -/// Embedded icon 256x256 in size. -const ICON: &[u8] = luminol_macros::include_asset!("assets/icons/icon.png"); +pub const BUILD_DIAGNOSTIC: luminol_core::BuildDiagnostics = luminol_core::BuildDiagnostics { + build_time: build::BUILD_TIME, + rustc_version: build::RUST_VERSION, + cargo_version: build::CARGO_VERSION, + build_os: build::BUILD_OS, + git_revision: git_revision(), + is_debug: cfg!(debug_assertions), +}; -static RESTART_AFTER_PANIC: std::sync::atomic::AtomicBool = +pub static RESTART_AFTER_PANIC: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); -mod app; -#[cfg(not(target_arch = "wasm32"))] -mod log; -mod lumi; - -#[cfg(all(feature = "steamworks", target_arch = "wasm32"))] -compile_error!("Steamworks is not supported on webassembly"); - -#[cfg(feature = "steamworks")] -mod steam; - const fn git_revision() -> &'static str { #[cfg(not(target_arch = "wasm32"))] { git_version::git_version!() } - // I would reach for unwrap_or but it's not const fn yet #[cfg(target_arch = "wasm32")] match option_env!("LUMINOL_VERSION") { Some(v) => v, @@ -63,427 +51,51 @@ const fn git_revision() -> &'static str { } } -pub const BUILD_DIAGNOSTIC: luminol_core::BuildDiagnostics = luminol_core::BuildDiagnostics { - build_time: build::BUILD_TIME, - rustc_version: build::RUST_VERSION, - cargo_version: build::CARGO_VERSION, - build_os: build::BUILD_OS, - git_revision: git_revision(), - is_debug: cfg!(debug_assertions), -}; +pub(crate) mod app; +mod entrypoint; +pub(crate) mod lumi; +#[cfg(feature = "steamworks")] +mod steam; +/// The native application entry point. +/// +/// The application initialisation process on native platforms differs from web.\ +/// Before showing any UI, Luminol has to: +/// +/// 1. Load the latest panic report. (if there is one) +/// 2. Initialise the Steamworks Application Programming Interface. (if 'steamworks' feature is enabled) +/// 3. Set up all appropriate loggers, hooks and debugging utilities to handle a possible error/crash. +/// +/// Only then can Luminol show the main window. #[cfg(not(target_arch = "wasm32"))] fn main() { - // Load the panic report from the previous run if it exists - let mut report = None; - if let Some(path) = std::env::var_os("LUMINOL_PANIC_REPORT_FILE") { - if let Ok(mut file) = std::fs::File::open(&path) { - let mut buffer = String::new(); - if file.read_to_string(&mut buffer).is_ok() { - report = Some(buffer); - } - } - let _ = std::fs::remove_file(path); + if let Err(why) = entrypoint::run() { + entrypoint::handle_fatal_error(why); } - - #[cfg(feature = "steamworks")] - let steamworks = match steam::Steamworks::new() { - Ok(s) => s, - Err(e) => { - rfd::MessageDialog::new() - .set_title("Error") - .set_level(rfd::MessageLevel::Error) - .set_description(format!( - "Steam error: {e}\nPerhaps you want to compile yourself a free copy?" - )) - .show(); - return; - } - }; - - #[cfg(debug_assertions)] - std::thread::spawn(|| loop { - use std::fmt::Write; - - std::thread::sleep(std::time::Duration::from_secs(5)); - - let deadlocks = parking_lot::deadlock::check_deadlock(); - if deadlocks.is_empty() { - continue; - } - - rfd::MessageDialog::new() - .set_title("Fatal Error") - .set_level(rfd::MessageLevel::Error) - .set_description(format!( - "Luminol has deadlocked! Please file an issue.\n{} deadlocks detected", - deadlocks.len() - )) - .show(); - for (i, threads) in deadlocks.iter().enumerate() { - let mut description = String::new(); - for t in threads { - writeln!(description, "Thread Id {:#?}", t.thread_id()).unwrap(); - writeln!(description, "{:#?}", t.backtrace()).unwrap(); - } - rfd::MessageDialog::new() - .set_title(&format!("Deadlock #{i}")) - .set_level(rfd::MessageLevel::Error) - .set_description(&description) - .show(); - } - - std::process::abort(); - }); - - // Enable full backtraces - std::env::set_var("RUST_BACKTRACE", "full"); - - // Set up hooks for formatting errors and panics - let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default() - .panic_section(format!("Luminol version: {}", git_revision())) - .add_frame_filter(Box::new(|frames| { - let filters = &[ - "_", - "core::", - "alloc::", - "cocoa::", - "tokio::", - "winit::", - "accesskit", - "std::rt::", - "std::sys_", - "windows::", - "egui::ui::", - "E as eyre::", - "T as core::", - "egui_dock::", - "std::panic::", - "egui::context::", - "luminol_eframe::", - "std::panicking::", - "egui::containers::", - "glPushClientAttrib", - "std::thread::local::", - ]; - frames.retain(|frame| { - !filters.iter().any(|f| { - frame.name.as_ref().is_some_and(|name| { - name.starts_with(|c: char| c.is_ascii_uppercase()) - || name.strip_prefix('<').unwrap_or(name).starts_with(f) - }) - }) - }) - })) - .into_hooks(); - eyre_hook - .install() - .expect("failed to install color-eyre hooks"); - std::panic::set_hook(Box::new(move |info| { - let report = panic_hook.panic_report(info).to_string(); - eprintln!("{report}"); - - if !RESTART_AFTER_PANIC.load(std::sync::atomic::Ordering::Relaxed) { - return; - } - - let mut args = std::env::args_os(); - let arg0 = args.next(); - let exe_path = std::env::current_exe().map_or_else( - |_| arg0.expect("could not get path to current executable"), - |exe_path| exe_path.into_os_string(), - ); - - let mut file = tempfile::NamedTempFile::new().expect("failed to create temporary file"); - file.write_all(report.as_bytes()) - .expect("failed to write to temporary file"); - file.flush().expect("failed to flush temporary file"); - let (_, path) = file.keep().expect("failed to persist temporary file"); - std::env::set_var("LUMINOL_PANIC_REPORT_FILE", &path); - - #[cfg(unix)] - { - use std::os::unix::process::CommandExt; - - let error = std::process::Command::new(exe_path).args(args).exec(); - eprintln!("Failed to restart Luminol: {error:?}"); - let _ = std::fs::remove_file(&path); - } - - #[cfg(not(unix))] - { - if let Err(error) = std::process::Command::new(exe_path).args(args).spawn() { - eprintln!("Failed to restart Luminol: {error:?}"); - let _ = std::fs::remove_file(&path); - } - } - })); - - let (log_byte_tx, log_byte_rx) = std::sync::mpsc::channel(); - let ctx_cell = std::sync::Arc::new(once_cell::sync::OnceCell::new()); - log::initialize_log(log_byte_tx, ctx_cell.clone()); - - let image = image::load_from_memory(ICON).expect("Failed to load Icon data."); - - let native_options = luminol_eframe::NativeOptions { - viewport: egui::ViewportBuilder::default() - .with_drag_and_drop(true) - .with_icon(egui::IconData { - width: image.width(), - height: image.height(), - rgba: image.to_rgba8().into_vec(), - }) - .with_app_id("astrabit.luminol"), - wgpu_options: luminol_egui_wgpu::WgpuConfiguration { - supported_backends: wgpu::util::backend_bits_from_env() - .unwrap_or(wgpu::Backends::PRIMARY | wgpu::Backends::SECONDARY), - power_preference: wgpu::util::power_preference_from_env() - .unwrap_or(wgpu::PowerPreference::LowPower), - ..Default::default() - }, - persist_window: true, - - ..Default::default() - }; - - luminol_eframe::run_native( - "Luminol", - native_options, - Box::new(move |cc| { - ctx_cell - .set(cc.egui_ctx.clone()) - .expect("egui context cell already set (this shouldn't happen!)"); - - Ok(Box::new(app::App::new( - cc, - report, - Default::default(), - log_byte_rx, - std::env::args_os().nth(1), - #[cfg(feature = "steamworks")] - steamworks, - ))) - }), - ) - .expect("failed to start luminol"); -} - -#[cfg(target_arch = "wasm32")] -#[wasm_bindgen( - inline_js = "let report = null; export function get_panic_report() { return report; }; export function set_panic_report(r) { report = r; window.restartLuminol(); };" -)] -extern "C" { - fn get_panic_report() -> Option; - fn set_panic_report(r: String); -} - -#[cfg(target_arch = "wasm32")] -const CANVAS_ID: &str = "luminol-canvas"; - -#[cfg(target_arch = "wasm32")] -struct WorkerData { - report: Option, - audio: luminol_audio::Audio, - modified: luminol_core::ModifiedState, - prefers_color_scheme_dark: Option, - fs_worker_channels: luminol_filesystem::web::WorkerChannels, - runner_worker_channels: luminol_eframe::web::WorkerChannels, - runner_panic_tx: std::sync::Arc>>>, } +/// The web entry point. +/// +/// The application initialisation process on web engines differs from native platforms.\ +/// Before showing any UI, Luminol has to: +/// +/// 1. Load the latest panic report. +/// 2. Set up all appropriate loggers, hooks and debugging utilities to handle a possible error/crash. +/// 3. Check for appropriate headers and safety mechanisms. +/// 4. Create a canvas to render the application. +/// 5. Initialise the file system interface. +/// +/// Only then can Luminol show the main window. #[cfg(target_arch = "wasm32")] -static WORKER_DATA: parking_lot::Mutex> = parking_lot::Mutex::new(None); - -#[cfg(target_arch = "wasm32")] -#[wasm_bindgen] +#[wasm_bindgen::prelude::wasm_bindgen] pub fn luminol_main_start() { - // Load the panic report from the previous run if it exists - let report = get_panic_report(); - - let worker_cell = std::rc::Rc::new(once_cell::unsync::OnceCell::::new()); - let before_unload_cell = std::rc::Rc::new(std::cell::RefCell::new( - None::>, - )); - let (panic_tx, panic_rx) = oneshot::channel(); - let panic_tx = std::sync::Arc::new(parking_lot::Mutex::new(Some(panic_tx))); - - { - let worker_cell = worker_cell.clone(); - let before_unload_cell = before_unload_cell.clone(); - - wasm_bindgen_futures::spawn_local(async move { - if let Ok(report) = panic_rx.await { - if let Some(worker) = worker_cell.get() { - worker.terminate(); - } - - if let (Some(window), Some(closure)) = - (web_sys::window(), before_unload_cell.take()) - { - let _ = window.remove_event_listener_with_callback( - "beforeunload", - closure.as_ref().unchecked_ref(), - ); - } - - if RESTART_AFTER_PANIC.load(std::sync::atomic::Ordering::Relaxed) { - set_panic_report(report); - } else { - let _ = web_sys::window().map(|window| window.alert_with_message("Luminol has crashed! Please check your browser's developer console for more details.")); - } - } - }); + if let Err(why) = entrypoint::run() { + entrypoint::handle_fatal_error(why); } - - // Set up hooks for formatting errors and panics - let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default() - .panic_section(format!("Luminol version: {}", git_revision())) - .into_hooks(); - eyre_hook - .install() - .expect("failed to install color-eyre hooks"); - std::panic::set_hook(Box::new(move |info| { - let report = panic_hook.panic_report(info).to_string(); - web_sys::console::log_1(&report.as_str().into()); - - // Send the panic report to the main thread to be persisted - // We need to send the panic report to the main thread because JavaScript global variables - // are thread-local and this panic handler runs on the thread that panicked - if let Some(mut panic_tx) = panic_tx.try_lock() { - if let Some(panic_tx) = panic_tx.take() { - let _ = panic_tx.send(report); - } - } - })); - - let window = web_sys::window().expect("could not get `window` object"); - let prefers_color_scheme_dark = window - .match_media("(prefers-color-scheme: dark)") - .unwrap() - .map(|x| x.matches()); - - let document = window - .document() - .expect("could not get `window.document` object"); - let canvas = document - .create_element("canvas") - .expect("could not create canvas element") - .unchecked_into::(); - document - .get_element_by_id(CANVAS_ID) - .expect(format!("could not find HTML element with ID '{CANVAS_ID}'").as_str()) - .replace_children_with_node_1(&canvas); - let offscreen_canvas = canvas - .transfer_control_to_offscreen() - .expect("could not transfer canvas control to offscreen"); - - // Redirect tracing to console.log and friends: - tracing_wasm::set_as_global_default_with_config( - tracing_wasm::WASMLayerConfigBuilder::new() - .set_max_level(tracing::Level::INFO) - .build(), - ); - - // Redirect log (currently used by egui) to tracing - tracing_log::LogTracer::init().expect("failed to initialize tracing-log"); - - if !luminol_web::bindings::cross_origin_isolated() { - tracing::error!( - "Cross-Origin Isolation is not enabled. Reloading page to attempt to enable it." - ); - window.location().reload().expect("failed to reload page"); - return; - } - - let (fs_worker_channels, fs_main_channels) = luminol_filesystem::web::channels(); - let (runner_worker_channels, runner_main_channels) = luminol_eframe::web::channels(); - - luminol_filesystem::host::setup_main_thread_hooks(fs_main_channels); - let runner_panic_tx = - luminol_eframe::WebRunner::setup_main_thread_hooks(luminol_eframe::web::MainState { - inner: Default::default(), - text_agent: Default::default(), - canvas: canvas.clone(), - channels: runner_main_channels, - }) - .expect("unable to setup web runner main thread hooks"); - - let modified = luminol_core::ModifiedState::default(); - - *WORKER_DATA.lock() = Some(WorkerData { - report, - audio: luminol_audio::Audio::default(), - modified: modified.clone(), - prefers_color_scheme_dark, - fs_worker_channels, - runner_worker_channels, - runner_panic_tx, - }); - - // Show confirmation dialogue if the user tries to close the browser tab while there are - // unsaved changes in the current project - { - let closure: Closure = Closure::new(move |e: web_sys::BeforeUnloadEvent| { - if modified.get() { - // Recommended method of activating the confirmation dialogue - e.prevent_default(); - // Fallback for Chromium < 119 - e.set_return_value("arbitrary non-empty string"); - } - }); - window - .add_event_listener_with_callback("beforeunload", closure.as_ref().unchecked_ref()) - .expect("failed to add beforeunload listener"); - *before_unload_cell.borrow_mut() = Some(closure); - } - - canvas.focus().expect("could not focus the canvas"); - - let mut worker_options = web_sys::WorkerOptions::new(); - worker_options.name("luminol-primary"); - worker_options.type_(web_sys::WorkerType::Module); - let worker = web_sys::Worker::new_with_options("./worker.js", &worker_options) - .expect("failed to spawn web worker"); - worker_cell.set(worker.clone()).unwrap(); - - let message = js_sys::Array::new(); - message.push(&wasm_bindgen::module()); - message.push(&wasm_bindgen::memory()); - message.push(&offscreen_canvas); - let transfer = js_sys::Array::new(); - transfer.push(&offscreen_canvas); - worker - .post_message_with_transfer(&message, &transfer) - .expect("failed to post message to web worker"); } #[cfg(target_arch = "wasm32")] -#[wasm_bindgen] +#[wasm_bindgen::prelude::wasm_bindgen] pub async fn luminol_worker_start(canvas: web_sys::OffscreenCanvas) { - let WorkerData { - report, - audio, - modified, - prefers_color_scheme_dark, - fs_worker_channels, - runner_worker_channels, - runner_panic_tx, - } = WORKER_DATA.lock().take().unwrap(); - - luminol_filesystem::host::FileSystem::setup_worker_channels(fs_worker_channels); - - let web_options = luminol_eframe::WebOptions::default(); - - luminol_eframe::WebRunner::new(runner_panic_tx) - .start( - canvas, - web_options, - Box::new(|cc| Ok(Box::new(app::App::new(cc, report, modified, audio)))), - luminol_eframe::web::WorkerOptions { - prefers_color_scheme_dark, - channels: runner_worker_channels, - }, - ) - .await - .expect("failed to start eframe"); + entrypoint::worker_start(canvas).await; } From 638b279e947b86d5a360ad3c487aa3ce5d0a2f09 Mon Sep 17 00:00:00 2001 From: N Date: Tue, 20 Aug 2024 14:00:40 +0300 Subject: [PATCH 2/4] feat: move the main crate to crates/ --- .gitignore | 2 +- Cargo.toml | 126 +----------------- Trunk.toml | 1 + crates/launcher/Cargo.toml | 123 +++++++++++++++++ .../launcher/assets/icons}/favicon.ico | Bin .../launcher/assets/icons}/icon-1024.png | Bin .../launcher/assets/icons}/icon-256.png | Bin .../assets/icons}/icon_ios_touch_192.png | Bin .../assets/icons}/maskable_icon_x512.png | Bin .../launcher/assets}/js/compat-test.js | 0 {assets => crates/launcher/assets}/js/main.js | 0 {assets => crates/launcher/assets}/js/sw.js | 0 .../launcher/assets}/js/worker.js | 0 .../launcher/assets}/manifest.json | 0 build.rs => crates/launcher/build.rs | 0 index.html => crates/launcher/index.html | 10 +- .../launcher/src}/app/log_window.rs | 0 {src => crates/launcher/src}/app/mod.rs | 0 {src => crates/launcher/src}/app/top_bar.rs | 0 .../src}/entrypoint/app/log_window.rs | 0 .../launcher/src}/entrypoint/app/mod.rs | 0 .../launcher/src}/entrypoint/app/top_bar.rs | 0 .../launcher/src}/entrypoint/mod.rs | 0 .../launcher/src}/entrypoint/native/log.rs | 0 .../launcher/src}/entrypoint/native/mod.rs | 2 +- .../launcher/src}/entrypoint/web.rs | 0 .../launcher/src}/lumi/assets/lumi-idle.svg | 0 .../launcher/src}/lumi/assets/lumi-speak.svg | 0 {src => crates/launcher/src}/lumi/mod.rs | 0 {src => crates/launcher/src}/lumi/state.rs | 0 {src => crates/launcher/src}/main.rs | 0 {src => crates/launcher/src}/steam.rs | 0 hooks/trunk_enable_build_std.sh | 2 +- hooks/trunk_enable_build_std.sh.cmd | 2 +- hooks/trunk_enable_build_std_background.sh | 2 +- .../trunk_enable_build_std_background.sh.cmd | 2 +- hooks/trunk_enable_build_std_pre.sh | 10 +- hooks/trunk_enable_build_std_pre.sh.cmd | 10 +- 38 files changed, 146 insertions(+), 146 deletions(-) create mode 100644 crates/launcher/Cargo.toml rename {assets/icons/web => crates/launcher/assets/icons}/favicon.ico (100%) rename {assets/icons/web => crates/launcher/assets/icons}/icon-1024.png (100%) rename {assets/icons/web => crates/launcher/assets/icons}/icon-256.png (100%) rename {assets/icons/web => crates/launcher/assets/icons}/icon_ios_touch_192.png (100%) rename {assets/icons/web => crates/launcher/assets/icons}/maskable_icon_x512.png (100%) rename {assets => crates/launcher/assets}/js/compat-test.js (100%) rename {assets => crates/launcher/assets}/js/main.js (100%) rename {assets => crates/launcher/assets}/js/sw.js (100%) rename {assets => crates/launcher/assets}/js/worker.js (100%) rename {assets => crates/launcher/assets}/manifest.json (100%) rename build.rs => crates/launcher/build.rs (100%) rename index.html => crates/launcher/index.html (91%) rename {src => crates/launcher/src}/app/log_window.rs (100%) rename {src => crates/launcher/src}/app/mod.rs (100%) rename {src => crates/launcher/src}/app/top_bar.rs (100%) rename {src => crates/launcher/src}/entrypoint/app/log_window.rs (100%) rename {src => crates/launcher/src}/entrypoint/app/mod.rs (100%) rename {src => crates/launcher/src}/entrypoint/app/top_bar.rs (100%) rename {src => crates/launcher/src}/entrypoint/mod.rs (100%) rename {src => crates/launcher/src}/entrypoint/native/log.rs (100%) rename {src => crates/launcher/src}/entrypoint/native/mod.rs (99%) rename {src => crates/launcher/src}/entrypoint/web.rs (100%) rename {src => crates/launcher/src}/lumi/assets/lumi-idle.svg (100%) rename {src => crates/launcher/src}/lumi/assets/lumi-speak.svg (100%) rename {src => crates/launcher/src}/lumi/mod.rs (100%) rename {src => crates/launcher/src}/lumi/state.rs (100%) rename {src => crates/launcher/src}/main.rs (100%) rename {src => crates/launcher/src}/steam.rs (100%) diff --git a/.gitignore b/.gitignore index 5f0ce608..f35c4080 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ #! Rust Build Output /target /marshal/target -/dist +/crates/launcher/dist #! GDB-related files .gdb_* diff --git a/Cargo.toml b/Cargo.toml index 41397fab..e3b6800e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,23 +1,7 @@ -# As much as I would like to move the luminol package into the crates folder, trunk won't let me :( -# It wants a root package, and it has to be this -[package] -name = "luminol" -description = "Luminol is a FOSS recreation of RPG Maker XP in Rust with love ❤️" -build = "build.rs" - -version.workspace = true -authors.workspace = true -edition.workspace = true -license.workspace = true -rust-version.workspace = true -readme.workspace = true -repository.workspace = true -keywords.workspace = true -categories.workspace = true - # Setup various shared workspace values [workspace] members = ["crates/*"] +default-members = ["crates/launcher"] resolver = "2" [workspace.lints.rust] @@ -171,114 +155,6 @@ luminol-result = { version = "0.4.0", path = "crates/result" } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[dependencies] -luminol-eframe.workspace = true -luminol-egui-wgpu.workspace = true -luminol-macros.workspace = true -egui.workspace = true -egui_extras.workspace = true - -wgpu.workspace = true - -rfd.workspace = true - -parking_lot.workspace = true -once_cell.workspace = true - -image.workspace = true - -tracing-subscriber = "0.3.17" -color-eyre.workspace = true - -luminol-audio.workspace = true -luminol-core.workspace = true -luminol-config.workspace = true -luminol-filesystem.workspace = true -luminol-graphics.workspace = true -luminol-ui.workspace = true -luminol-result.workspace = true -# luminol-windows = { version = "0.1.0", path = "../windows/" } -# luminol-tabs = { version = "0.1.0", path = "../tabs/" } - -camino.workspace = true - -strum.workspace = true - -zstd = "0.13.0" - -async-std.workspace = true -futures-lite.workspace = true - -git-version = "0.3.9" -shadow-rs = { version = "0.32.0", default-features = false } - -# Native -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -steamworks = { version = "0.10.0", optional = true } -tokio = { version = "1.33", features = [ - "sync", - "macros", - "io-util", - "rt-multi-thread", - "parking_lot", -] } # *sigh* -tempfile.workspace = true -luminol-term.workspace = true - -# Set poll promise features here based on the target -# I'd much rather do it in the workspace, but cargo doesn't support that yet -# -# Doing this also relies on a quirk of features, that any crate specifying features applies to ALL crates -[target.'cfg(not(target_arch = "wasm32"))'.dependencies.poll-promise] -workspace = true -features = ["tokio"] - -[target.'cfg(target_arch = "wasm32")'.dependencies.poll-promise] -workspace = true -features = ["web"] - -# Web -# Look into somehow pinning these as workspace dependencies -[target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen.workspace = true -wasm-bindgen-futures.workspace = true -js-sys.workspace = true - -oneshot.workspace = true - -luminol-web = { version = "0.4.0", path = "crates/web/" } - -tracing-wasm = "0.2" -tracing-log = "0.1.3" -tracing.workspace = true - -web-sys = { workspace = true, features = [ - "BeforeUnloadEvent", - "Window", - "Worker", - "WorkerOptions", - "WorkerType", - "Document", - "Element", - "CssStyleDeclaration", -] } - -[target.'cfg(target_arch = "wasm32")'.dependencies.wgpu] -workspace = true -features = ["webgpu", "webgl"] - -[features] -steamworks = ["dep:steamworks", "luminol-result/steamworks"] - -[build-dependencies] -shadow-rs = { version = "0.32.0", default-features = false } - -[target.'cfg(windows)'.build-dependencies] -winres = "0.1" - -[package.metadata.winres] -OriginalFilename = "Luminol.exe" -ProductName = "Luminol" # Fast and performant. [profile.release] diff --git a/Trunk.toml b/Trunk.toml index 0621c299..e0e1ee73 100644 --- a/Trunk.toml +++ b/Trunk.toml @@ -1,4 +1,5 @@ [build] +target = "crates/launcher/index.html" filehash = false inject_scripts = false diff --git a/crates/launcher/Cargo.toml b/crates/launcher/Cargo.toml new file mode 100644 index 00000000..c67b2fff --- /dev/null +++ b/crates/launcher/Cargo.toml @@ -0,0 +1,123 @@ +[package] +name = "luminol" +description = "Luminol is a FOSS recreation of RPG Maker XP in Rust with love ❤️" +build = "build.rs" + +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true +repository.workspace = true +keywords.workspace = true +categories.workspace = true + +[dependencies] +luminol-eframe.workspace = true +luminol-egui-wgpu.workspace = true +luminol-macros.workspace = true +egui.workspace = true +egui_extras.workspace = true + +wgpu.workspace = true + +rfd.workspace = true + +parking_lot.workspace = true +once_cell.workspace = true + +image.workspace = true + +tracing-subscriber = "0.3.17" +color-eyre.workspace = true + +luminol-audio.workspace = true +luminol-core.workspace = true +luminol-config.workspace = true +luminol-filesystem.workspace = true +luminol-graphics.workspace = true +luminol-ui.workspace = true +luminol-result.workspace = true +# luminol-windows = { version = "0.1.0", path = "../windows/" } +# luminol-tabs = { version = "0.1.0", path = "../tabs/" } + +camino.workspace = true + +strum.workspace = true + +zstd = "0.13.0" + +async-std.workspace = true +futures-lite.workspace = true + +git-version = "0.3.9" +shadow-rs = { version = "0.32.0", default-features = false } + +# Native +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +steamworks = { version = "0.10.0", optional = true } +tokio = { version = "1.33", features = [ + "sync", + "macros", + "io-util", + "rt-multi-thread", + "parking_lot", +] } # *sigh* +tempfile.workspace = true +luminol-term.workspace = true + +# Set poll promise features here based on the target +# I'd much rather do it in the workspace, but cargo doesn't support that yet +# +# Doing this also relies on a quirk of features, that any crate specifying features applies to ALL crates +[target.'cfg(not(target_arch = "wasm32"))'.dependencies.poll-promise] +workspace = true +features = ["tokio"] + +[target.'cfg(target_arch = "wasm32")'.dependencies.poll-promise] +workspace = true +features = ["web"] + +# Web +# Look into somehow pinning these as workspace dependencies +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen.workspace = true +wasm-bindgen-futures.workspace = true +js-sys.workspace = true + +oneshot.workspace = true + +luminol-web = { version = "0.4.0", path = "../web/" } + +tracing-wasm = "0.2" +tracing-log = "0.1.3" +tracing.workspace = true + +web-sys = { workspace = true, features = [ + "BeforeUnloadEvent", + "Window", + "Worker", + "WorkerOptions", + "WorkerType", + "Document", + "Element", + "CssStyleDeclaration", +] } + +[target.'cfg(target_arch = "wasm32")'.dependencies.wgpu] +workspace = true +features = ["webgpu", "webgl"] + +[features] +steamworks = ["dep:steamworks", "luminol-result/steamworks"] + +[build-dependencies] +shadow-rs = { version = "0.32.0", default-features = false } + +[target.'cfg(windows)'.build-dependencies] +winres = "0.1" + +[package.metadata.winres] +OriginalFilename = "Luminol.exe" +ProductName = "Luminol" diff --git a/assets/icons/web/favicon.ico b/crates/launcher/assets/icons/favicon.ico similarity index 100% rename from assets/icons/web/favicon.ico rename to crates/launcher/assets/icons/favicon.ico diff --git a/assets/icons/web/icon-1024.png b/crates/launcher/assets/icons/icon-1024.png similarity index 100% rename from assets/icons/web/icon-1024.png rename to crates/launcher/assets/icons/icon-1024.png diff --git a/assets/icons/web/icon-256.png b/crates/launcher/assets/icons/icon-256.png similarity index 100% rename from assets/icons/web/icon-256.png rename to crates/launcher/assets/icons/icon-256.png diff --git a/assets/icons/web/icon_ios_touch_192.png b/crates/launcher/assets/icons/icon_ios_touch_192.png similarity index 100% rename from assets/icons/web/icon_ios_touch_192.png rename to crates/launcher/assets/icons/icon_ios_touch_192.png diff --git a/assets/icons/web/maskable_icon_x512.png b/crates/launcher/assets/icons/maskable_icon_x512.png similarity index 100% rename from assets/icons/web/maskable_icon_x512.png rename to crates/launcher/assets/icons/maskable_icon_x512.png diff --git a/assets/js/compat-test.js b/crates/launcher/assets/js/compat-test.js similarity index 100% rename from assets/js/compat-test.js rename to crates/launcher/assets/js/compat-test.js diff --git a/assets/js/main.js b/crates/launcher/assets/js/main.js similarity index 100% rename from assets/js/main.js rename to crates/launcher/assets/js/main.js diff --git a/assets/js/sw.js b/crates/launcher/assets/js/sw.js similarity index 100% rename from assets/js/sw.js rename to crates/launcher/assets/js/sw.js diff --git a/assets/js/worker.js b/crates/launcher/assets/js/worker.js similarity index 100% rename from assets/js/worker.js rename to crates/launcher/assets/js/worker.js diff --git a/assets/manifest.json b/crates/launcher/assets/manifest.json similarity index 100% rename from assets/manifest.json rename to crates/launcher/assets/manifest.json diff --git a/build.rs b/crates/launcher/build.rs similarity index 100% rename from build.rs rename to crates/launcher/build.rs diff --git a/index.html b/crates/launcher/index.html similarity index 91% rename from index.html rename to crates/launcher/index.html index 981fd96b..858cd26a 100644 --- a/index.html +++ b/crates/launcher/index.html @@ -21,11 +21,11 @@ - - - - - + + + + + diff --git a/src/app/log_window.rs b/crates/launcher/src/app/log_window.rs similarity index 100% rename from src/app/log_window.rs rename to crates/launcher/src/app/log_window.rs diff --git a/src/app/mod.rs b/crates/launcher/src/app/mod.rs similarity index 100% rename from src/app/mod.rs rename to crates/launcher/src/app/mod.rs diff --git a/src/app/top_bar.rs b/crates/launcher/src/app/top_bar.rs similarity index 100% rename from src/app/top_bar.rs rename to crates/launcher/src/app/top_bar.rs diff --git a/src/entrypoint/app/log_window.rs b/crates/launcher/src/entrypoint/app/log_window.rs similarity index 100% rename from src/entrypoint/app/log_window.rs rename to crates/launcher/src/entrypoint/app/log_window.rs diff --git a/src/entrypoint/app/mod.rs b/crates/launcher/src/entrypoint/app/mod.rs similarity index 100% rename from src/entrypoint/app/mod.rs rename to crates/launcher/src/entrypoint/app/mod.rs diff --git a/src/entrypoint/app/top_bar.rs b/crates/launcher/src/entrypoint/app/top_bar.rs similarity index 100% rename from src/entrypoint/app/top_bar.rs rename to crates/launcher/src/entrypoint/app/top_bar.rs diff --git a/src/entrypoint/mod.rs b/crates/launcher/src/entrypoint/mod.rs similarity index 100% rename from src/entrypoint/mod.rs rename to crates/launcher/src/entrypoint/mod.rs diff --git a/src/entrypoint/native/log.rs b/crates/launcher/src/entrypoint/native/log.rs similarity index 100% rename from src/entrypoint/native/log.rs rename to crates/launcher/src/entrypoint/native/log.rs diff --git a/src/entrypoint/native/mod.rs b/crates/launcher/src/entrypoint/native/mod.rs similarity index 99% rename from src/entrypoint/native/mod.rs rename to crates/launcher/src/entrypoint/native/mod.rs index 48d07f93..0ef547df 100644 --- a/src/entrypoint/native/mod.rs +++ b/crates/launcher/src/entrypoint/native/mod.rs @@ -138,7 +138,7 @@ fn setup_hooks() -> Result<()> { let report = panic_hook.panic_report(info).to_string(); eprintln!("{report}"); - if !RESTART_AFTER_PANIC.load(atomic::Ordering::Relaxed) { + if !crate::RESTART_AFTER_PANIC.load(atomic::Ordering::Relaxed) { return; } diff --git a/src/entrypoint/web.rs b/crates/launcher/src/entrypoint/web.rs similarity index 100% rename from src/entrypoint/web.rs rename to crates/launcher/src/entrypoint/web.rs diff --git a/src/lumi/assets/lumi-idle.svg b/crates/launcher/src/lumi/assets/lumi-idle.svg similarity index 100% rename from src/lumi/assets/lumi-idle.svg rename to crates/launcher/src/lumi/assets/lumi-idle.svg diff --git a/src/lumi/assets/lumi-speak.svg b/crates/launcher/src/lumi/assets/lumi-speak.svg similarity index 100% rename from src/lumi/assets/lumi-speak.svg rename to crates/launcher/src/lumi/assets/lumi-speak.svg diff --git a/src/lumi/mod.rs b/crates/launcher/src/lumi/mod.rs similarity index 100% rename from src/lumi/mod.rs rename to crates/launcher/src/lumi/mod.rs diff --git a/src/lumi/state.rs b/crates/launcher/src/lumi/state.rs similarity index 100% rename from src/lumi/state.rs rename to crates/launcher/src/lumi/state.rs diff --git a/src/main.rs b/crates/launcher/src/main.rs similarity index 100% rename from src/main.rs rename to crates/launcher/src/main.rs diff --git a/src/steam.rs b/crates/launcher/src/steam.rs similarity index 100% rename from src/steam.rs rename to crates/launcher/src/steam.rs diff --git a/hooks/trunk_enable_build_std.sh b/hooks/trunk_enable_build_std.sh index 36a0f868..be2f16a9 100755 --- a/hooks/trunk_enable_build_std.sh +++ b/hooks/trunk_enable_build_std.sh @@ -1,4 +1,4 @@ #!/bin/sh set -e -$TRUNK_SOURCE_DIR/hooks/trunk_enable_build_std_background.sh & +./hooks/trunk_enable_build_std_background.sh & diff --git a/hooks/trunk_enable_build_std.sh.cmd b/hooks/trunk_enable_build_std.sh.cmd index 05d263db..274531a4 100644 --- a/hooks/trunk_enable_build_std.sh.cmd +++ b/hooks/trunk_enable_build_std.sh.cmd @@ -1,4 +1,4 @@ @echo off setlocal -start /b %TRUNK_SOURCE_DIR%\hooks\trunk_enable_build_std_background.sh +start /b hooks\trunk_enable_build_std_background.sh diff --git a/hooks/trunk_enable_build_std_background.sh b/hooks/trunk_enable_build_std_background.sh index 2b4f83a0..2e186795 100755 --- a/hooks/trunk_enable_build_std_background.sh +++ b/hooks/trunk_enable_build_std_background.sh @@ -6,4 +6,4 @@ sleep 1 while [ -d $TRUNK_STAGING_DIR ] && [ ! -f $TRUNK_STAGING_DIR/luminol.js ] && pgrep -x 'trunk' > /dev/null; do sleep 1 done -mv $TRUNK_SOURCE_DIR/.cargo/config.toml.bak $TRUNK_SOURCE_DIR/.cargo/config.toml +mv .cargo/config.toml.bak .cargo/config.toml diff --git a/hooks/trunk_enable_build_std_background.sh.cmd b/hooks/trunk_enable_build_std_background.sh.cmd index d5d27c80..5244dd6a 100644 --- a/hooks/trunk_enable_build_std_background.sh.cmd +++ b/hooks/trunk_enable_build_std_background.sh.cmd @@ -10,4 +10,4 @@ tasklist | find trunk.exe >nul if errorlevel 1 goto end goto loop :end -move %TRUNK_SOURCE_DIR%\.cargo\config.toml.bak %TRUNK_SOURCE_DIR%\.cargo\config.toml +move .cargo\config.toml.bak .cargo\config.toml diff --git a/hooks/trunk_enable_build_std_pre.sh b/hooks/trunk_enable_build_std_pre.sh index c258ccbd..9f26123c 100755 --- a/hooks/trunk_enable_build_std_pre.sh +++ b/hooks/trunk_enable_build_std_pre.sh @@ -8,10 +8,10 @@ git_version=$(git describe --always --dirty=-modified) echo "{\"epoch\":0,\"rev\":\"$git_version\",\"profile\":\"$TRUNK_PROFILE\"}" > $TRUNK_STAGING_DIR/buildinfo.json # Enable std support for multithreading and set the LUMINOL_VERSION environment variable -[ ! -f $TRUNK_SOURCE_DIR/.cargo/config.toml.bak ] || mv $TRUNK_SOURCE_DIR/.cargo/config.toml.bak $TRUNK_SOURCE_DIR/.cargo/config.toml -cp $TRUNK_SOURCE_DIR/.cargo/config.toml $TRUNK_SOURCE_DIR/.cargo/config.toml.bak +[ ! -f .cargo/config.toml.bak ] || mv .cargo/config.toml.bak .cargo/config.toml +cp .cargo/config.toml .cargo/config.toml.bak -echo "LUMINOL_VERSION = { value = \"$git_version\", force = true }" >> $TRUNK_SOURCE_DIR/.cargo/config.toml +echo "LUMINOL_VERSION = { value = \"$git_version\", force = true }" >> .cargo/config.toml -echo '[unstable]' >> $TRUNK_SOURCE_DIR/.cargo/config.toml -echo 'build-std = ["std", "panic_abort"]' >> $TRUNK_SOURCE_DIR/.cargo/config.toml +echo '[unstable]' >> .cargo/config.toml +echo 'build-std = ["std", "panic_abort"]' >> .cargo/config.toml diff --git a/hooks/trunk_enable_build_std_pre.sh.cmd b/hooks/trunk_enable_build_std_pre.sh.cmd index ce2d2246..1da24ae1 100644 --- a/hooks/trunk_enable_build_std_pre.sh.cmd +++ b/hooks/trunk_enable_build_std_pre.sh.cmd @@ -8,10 +8,10 @@ for /f "tokens=*" %%i in ('git describe --always --dirty=-modified') do set git_ echo {"epoch":0,"rev":"%git_version%","profile":"%TRUNK_PROFILE%"} > %TRUNK_STAGING_DIR%\buildinfo.json :: Enable std support for multithreading and set the LUMINOL_VERSION environment variable -if exist %TRUNK_SOURCE_DIR%\.cargo\config.toml.bak move %TRUNK_SOURCE_DIR%\.cargo\config.toml.bak %TRUNK_SOURCE_DIR%\.cargo\config.toml -copy %TRUNK_SOURCE_DIR%\.cargo\config.toml %TRUNK_SOURCE_DIR%\.cargo\config.toml.bak +if exist .cargo\config.toml.bak move .cargo\config.toml.bak .cargo\config.toml +copy .cargo\config.toml .cargo\config.toml.bak -echo LUMINOL_VERSION = { value = "%git_version%", force = true } >> %TRUNK_SOURCE_DIR%\.cargo\config.toml +echo LUMINOL_VERSION = { value = "%git_version%", force = true } >> .cargo\config.toml -echo [unstable] >> %TRUNK_SOURCE_DIR%\.cargo\config.toml -echo build-std = ["std", "panic_abort"] >> %TRUNK_SOURCE_DIR%\.cargo\config.toml +echo [unstable] >> .cargo\config.toml +echo build-std = ["std", "panic_abort"] >> .cargo\config.toml From 45ded07e1b9d464f3538ddce6b22bd2c827d4f3f Mon Sep 17 00:00:00 2001 From: N Date: Tue, 20 Aug 2024 14:45:18 +0300 Subject: [PATCH 3/4] fix: expected value, found crate `steamworks` --- crates/launcher/src/entrypoint/native/mod.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/launcher/src/entrypoint/native/mod.rs b/crates/launcher/src/entrypoint/native/mod.rs index 0ef547df..1b6bd56f 100644 --- a/crates/launcher/src/entrypoint/native/mod.rs +++ b/crates/launcher/src/entrypoint/native/mod.rs @@ -191,6 +191,7 @@ fn run_app( report: Option, ctx_cell: sync::Arc>, log_byte_rx: sync::mpsc::Receiver, + #[cfg(feature = "steamworks")] steamworks: crate::steam::Steamworks, ) -> Result<()> { let icon_image = image::load_from_memory(ICON)?; @@ -261,7 +262,13 @@ pub fn run() -> Result<()> { let (ctx_cell, log_byte_rx) = init_log(); /* Show the graphical user interface */ - run_app(report, ctx_cell, log_byte_rx)?; + run_app( + report, + ctx_cell, + log_byte_rx, + #[cfg(feature = "steamworks")] + steamworks, + )?; Ok(()) } From eb8d23f2c67bd0d099d11db04428c5188e2b3dc3 Mon Sep 17 00:00:00 2001 From: N Date: Tue, 20 Aug 2024 19:15:04 +0300 Subject: [PATCH 4/4] fix: crash if eframe's run_native method fails --- crates/launcher/src/entrypoint/native/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/launcher/src/entrypoint/native/mod.rs b/crates/launcher/src/entrypoint/native/mod.rs index 1b6bd56f..c1190b5c 100644 --- a/crates/launcher/src/entrypoint/native/mod.rs +++ b/crates/launcher/src/entrypoint/native/mod.rs @@ -235,7 +235,8 @@ fn run_app( steamworks, ))) }), - ); + ) + .expect("failed to start luminol"); Ok(()) }