tirbofish/dropbear · diff
fix up some issues to do with features and add a new engine.quit() function which quits playing.
jarvis, run github actions
fixes #69
Signature present but could not be verified.
Unverified
@@ -62,8 +62,19 @@ cargo build > [!TIP] > It is recommended to use IntelliJ IDEA with the Rust plugin to help contribute to the engine. > -> If you are a normal joe, -> then just use the standard IntelliJ IDEA. +> If you are a normal joe, then just use the standard IntelliJ IDEA. + +<details> + +<summary>For the engine developers</summary> + +> [!WARNING] +> Ensure that `cargo build` is ran for each iteration of testing instead of `cargo build -p eucalyptus-editor` due to some weird ABI issue with Rust. If you do only want to build +> a specific package, ensure you run `cargo build -p {package} -p eucalyptus-core`. +> +> If you get any FFI errors (likely a getter), you compiled the library wrong. + +</details> ### Prebuilt @@ -44,7 +44,8 @@ rustc_version_runtime.workspace = true [features] default = [] - +# internal redback-runtime configurations because some stuff keep on enabling for JVM +runtime = [] # editor only stuff editor = ["jvm", "rfd", "jvm_debug"] # enabled scripttarget::jvm as an option @@ -1,10 +1,10 @@ use crate::input::InputState; -use crate::window::GraphicsCommand; +use crate::window::CommandBuffer; use crossbeam_channel::Sender; use dropbear_engine::asset::AssetRegistry; use hecs::World; pub type WorldPtr = *mut World; pub type InputStatePtr = *mut InputState; -pub type GraphicsPtr = *const Sender<GraphicsCommand>; +pub type GraphicsPtr = *const Sender<CommandBuffer>; pub type AssetRegistryPtr = *const AssetRegistry; @@ -581,7 +581,7 @@ impl SceneConfig { } log::info!("Loaded {} entities from scene", self.entities.len()); - #[cfg(feature = "editor")] + #[cfg(all(feature = "editor", not(feature = "runtime")))] { log::debug!("editor feature enabled"); use crate::camera::CameraType; @@ -602,7 +602,7 @@ impl SceneConfig { { if let Some(camera_entity) = debug_camera { log::info!("Using existing debug camera for editor"); - Ok(camera_entity) + return Ok(camera_entity); } else { log::info!("No debug camera found, creating viewport camera for editor"); @@ -644,12 +644,12 @@ impl SceneConfig { let component = crate::camera::DebugCamera::new(); let label = Label::new("Viewport Camera"); let camera_entity = { world.spawn((label, camera, component)) }; - Ok(camera_entity) + return Ok(camera_entity); } } } - #[cfg(not(feature = "editor"))] + #[cfg(any(not(feature = "editor"), feature = "runtime"))] { log::debug!("loading player camera without editor feature"); let player_camera = world @@ -667,7 +667,7 @@ impl SceneConfig { log::info!("Using player camera for runtime"); log::debug!("Checkpoint 1: Entity id: {:?}", e); cam.debug_camera_state(); - Ok(e) + return Ok(e); } else { panic!("Runtime mode requires an initial camera, but none was found in the scene"); // todo: get a better way of rendering something without a camera. @@ -8,7 +8,7 @@ use crate::scripting::jni::utils::{ }; use crate::states::{Label, ModelProperties, Value}; use crate::utils::keycode_from_ordinal; -use crate::window::{GraphicsCommand, WindowCommand}; +use crate::window::{CommandBuffer, WindowCommand}; use crate::{convert_jlong_to_entity, convert_jstring, convert_ptr, ffi_error_return}; use dropbear_engine::asset::PointerKind::Const; use dropbear_engine::asset::{ASSET_REGISTRY, AssetHandle, AssetRegistry}; @@ -604,7 +604,7 @@ pub fn Java_com_dropbear_ffi_JNINative_setCursorLocked( _env: JNIEnv, _class: JClass, input_handle: jlong, - graphics_handle: jlong, + command_handle: jlong, locked: jboolean, ) { let input = input_handle as InputStatePtr; @@ -616,7 +616,7 @@ pub fn Java_com_dropbear_ffi_JNINative_setCursorLocked( return; } - let graphics = graphics_handle as GraphicsPtr; + let graphics = command_handle as GraphicsPtr; if graphics.is_null() { eprintln!( @@ -630,7 +630,7 @@ pub fn Java_com_dropbear_ffi_JNINative_setCursorLocked( let is_locked = locked != 0; - if let Err(e) = graphics.send(GraphicsCommand::WindowCommand(WindowCommand::WindowGrab( + if let Err(e) = graphics.send(CommandBuffer::WindowCommand(WindowCommand::WindowGrab( is_locked, ))) { eprintln!( @@ -1859,7 +1859,7 @@ pub fn Java_com_dropbear_ffi_JNINative_setCursorHidden( _env: JNIEnv, _class: JClass, input_handle: jlong, - graphics_handle: jlong, + command_handle: jlong, hide: jboolean, ) { let input = input_handle as InputStatePtr; @@ -1871,7 +1871,7 @@ pub fn Java_com_dropbear_ffi_JNINative_setCursorHidden( } let input = unsafe { &mut *input }; - let graphics = graphics_handle as GraphicsPtr; + let graphics = command_handle as GraphicsPtr; if graphics.is_null() { println!( "[Java_com_dropbear_ffi_JNINative_setCursorHidden] [ERROR] Input state pointer is null" @@ -1882,7 +1882,7 @@ pub fn Java_com_dropbear_ffi_JNINative_setCursorHidden( let hide = hide != JNI_FALSE; - if let Err(e) = graphics.send(GraphicsCommand::WindowCommand(WindowCommand::HideCursor( + if let Err(e) = graphics.send(CommandBuffer::WindowCommand(WindowCommand::HideCursor( hide, ))) { println!( @@ -2609,3 +2609,27 @@ pub fn Java_com_dropbear_ffi_JNINative_getParent( crate::ffi_error_return!("No entity exists") } } + +/// `JNIEXPORT void JNICALL Java_com_dropbear_ffi_JNINative_quit +/// (JNIEnv *, jclass, jlong);` +#[unsafe(no_mangle)] +pub fn Java_com_dropbear_ffi_JNINative_quit( + _env: JNIEnv, + _class: JClass, + command_handle: jlong, +) { + let graphics = command_handle as GraphicsPtr; + + if graphics.is_null() { + eprintln!( + "[Java_com_dropbear_ffi_JNINative_quit] [ERROR] Graphics pointer is null" + ); + panic!("NullHandle while quitting GraphicsCommand, better off to shoot with gun than to nicely ask...") + } + + let graphics = unsafe { &*graphics }; + + if let Err(e) = graphics.send(CommandBuffer::Quit) { + panic!("Unable to send window command while quitting GraphicsCommand, better off to shoot with gun than to nicely ask... \n Error: {}", e); + } +} @@ -12,7 +12,7 @@ use crate::scripting::native::types::{ }; use crate::states::{Label, ModelProperties, Value}; use crate::utils::keycode_from_ordinal; -use crate::window::{GraphicsCommand, WindowCommand}; +use crate::window::{CommandBuffer, WindowCommand}; use dropbear_engine::asset::{PointerKind::Const, ASSET_REGISTRY, AssetHandle}; use dropbear_engine::camera::Camera; use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform}; @@ -955,7 +955,7 @@ pub unsafe extern "C" fn dropbear_set_cursor_locked( input.is_cursor_locked = locked != 0; if graphics - .send(GraphicsCommand::WindowCommand(WindowCommand::WindowGrab( + .send(CommandBuffer::WindowCommand(WindowCommand::WindowGrab( input.is_cursor_locked, ))) .is_err() @@ -1223,7 +1223,7 @@ pub unsafe extern "C" fn dropbear_set_cursor_hidden( input.is_cursor_hidden = hidden != 0; if graphics - .send(GraphicsCommand::WindowCommand(WindowCommand::HideCursor( + .send(CommandBuffer::WindowCommand(WindowCommand::HideCursor( input.is_cursor_hidden, ))) .is_err() @@ -1922,3 +1922,21 @@ pub unsafe extern "C" fn dropbear_get_parent( } } } + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn dropbear_quit( + command_ptr: GraphicsPtr, +) { + if command_ptr.is_null() { + panic!("NullPointer (-1) while quitting GraphicsCommand, better off to shoot with gun than to nicely ask...") + } + + let graphics = unsafe { &*(command_ptr as GraphicsPtr) }; + + if graphics + .send(CommandBuffer::Quit) + .is_err() + { + panic!("SendError (-7) while quitting GraphicsCommand, better off to shoot with gun than to nicely ask...") + } +} @@ -1,23 +1,23 @@ use crossbeam_channel::{Receiver, Sender, unbounded}; +use dropbear_engine::graphics::RenderContext; use once_cell::sync::Lazy; use parking_lot::RwLock; -use std::sync::{Arc, OnceLock}; -use winit::window::{CursorGrabMode, Window}; +use std::sync::{OnceLock}; -pub static GRAPHICS_COMMAND: Lazy<(Box<Sender<GraphicsCommand>>, Receiver<GraphicsCommand>)> = +pub static GRAPHICS_COMMAND: Lazy<(Box<Sender<CommandBuffer>>, Receiver<CommandBuffer>)> = Lazy::new(|| { - let (tx, rx) = unbounded::<GraphicsCommand>(); + let (tx, rx) = unbounded::<CommandBuffer>(); (Box::new(tx), rx) }); static PREVIOUS_CONFIG: OnceLock<RwLock<CommandCache>> = OnceLock::new(); -fn get_config() -> &'static RwLock<CommandCache> { +pub fn get_config() -> &'static RwLock<CommandCache> { PREVIOUS_CONFIG.get_or_init(|| RwLock::new(CommandCache::new())) } -struct CommandCache { - is_locked: bool, - is_hidden: bool, +pub struct CommandCache { + pub is_locked: bool, + pub is_hidden: bool, } impl CommandCache { @@ -30,8 +30,9 @@ impl CommandCache { } #[derive(Debug)] -pub enum GraphicsCommand { +pub enum CommandBuffer { WindowCommand(WindowCommand), + Quit, } #[derive(Debug)] @@ -40,43 +41,7 @@ pub enum WindowCommand { HideCursor(bool), } -pub fn poll(window: Arc<Window>) { - while let Ok(cmd) = GRAPHICS_COMMAND.1.try_recv() { - log::trace!("Received GRAPHICS_COMMAND update: {:?}", cmd); - match cmd { - GraphicsCommand::WindowCommand(w_cmd) => match w_cmd { - WindowCommand::WindowGrab(is_locked) => { - let mut cfg = get_config().write(); - if cfg.is_locked != is_locked { - if is_locked { - if let Err(e) = window - .set_cursor_grab(CursorGrabMode::Confined) - .or_else(|_| window.set_cursor_grab(CursorGrabMode::Locked)) - { - log_once::warn_once!("Failed to grab cursor: {:?}", e); - } else { - log_once::info_once!("Grabbed cursor"); - cfg.is_locked = true; - } - } else if let Err(e) = window.set_cursor_grab(CursorGrabMode::None) { - log_once::warn_once!("Failed to release cursor: {:?}", e); - } else { - log_once::info_once!("Released cursor"); - cfg.is_locked = false; - } - } - } - WindowCommand::HideCursor(should_hide) => { - let cfg = get_config().write(); - if cfg.is_hidden != should_hide { - if should_hide { - window.set_cursor_visible(false); - } else { - window.set_cursor_visible(true); - } - } - } - }, - } - } -} +/// Command buffer that is used for oneway communication between Kotlin to Rust. +pub trait CommandBufferPoller { + fn poll(&mut self, graphics: &RenderContext); +} @@ -326,6 +326,7 @@ pub async fn package( .file_name() .ok_or_else(|| anyhow::anyhow!("Import library missing filename: {}", import_lib.display()))? .to_owned(); + if !format!("{}", file_name.display()).contains("eucalyptus_core") { continue; } let libs_target = libs_dir.join(&file_name); tokio_fs::copy(&import_lib, &libs_target).await?; log::info!( @@ -0,0 +1,52 @@ +use dropbear_engine::graphics::RenderContext; +use eucalyptus_core::window::{CommandBufferPoller, GRAPHICS_COMMAND, CommandBuffer, WindowCommand, get_config}; +use winit::window::CursorGrabMode; + +use crate::editor::Editor; + +impl CommandBufferPoller for Editor { + fn poll(&mut self, graphics: &RenderContext) { + while let Ok(cmd) = GRAPHICS_COMMAND.1.try_recv() { + log::trace!("Received GRAPHICS_COMMAND update: {:?}", cmd); + match cmd { + CommandBuffer::WindowCommand(w_cmd) => match w_cmd { + WindowCommand::WindowGrab(is_locked) => { + let mut cfg = get_config().write(); + if cfg.is_locked != is_locked { + if is_locked { + if let Err(e) = graphics.shared.window + .set_cursor_grab(CursorGrabMode::Confined) + .or_else(|_| graphics.shared.window.set_cursor_grab(CursorGrabMode::Locked)) + { + log_once::warn_once!("Failed to grab cursor: {:?}", e); + } else { + log_once::info_once!("Grabbed cursor"); + cfg.is_locked = true; + } + } else if let Err(e) = graphics.shared.window.set_cursor_grab(CursorGrabMode::None) { + log_once::warn_once!("Failed to release cursor: {:?}", e); + } else { + log_once::info_once!("Released cursor"); + cfg.is_locked = false; + } + } + } + WindowCommand::HideCursor(should_hide) => { + let cfg = get_config().write(); + if cfg.is_hidden != should_hide { + if should_hide { + graphics.shared.window.set_cursor_visible(false); + } else { + graphics.shared.window.set_cursor_visible(true); + } + } + } + }, + CommandBuffer::Quit => { + log::info!("Quit command received in editor, stopping play mode"); + self.signal = crate::editor::Signal::StopPlaying; + }, + } + } + } +} @@ -91,19 +91,13 @@ impl Keyboard for Editor { } } KeyCode::Escape => { - if is_double_press { + if is_playing { + } else if is_double_press { if self.selected_entity.is_some() { self.selected_entity = None; log::debug!("Deselected entity"); } - } else if matches!(self.editor_state, EditorState::Playing) && shift_pressed { - self.viewport_mode = ViewportMode::None; - info!("Switched to Viewport::None"); - if let Some(window) = &self.window { - window.set_cursor_visible(true); - let _ = window.set_cursor_grab(CursorGrabMode::None); - } - } else if self.is_viewport_focused && !is_playing { + } else if self.is_viewport_focused { self.viewport_mode = ViewportMode::None; info!("Switched to Viewport::None"); if let Some(window) = &self.window { @@ -252,6 +246,14 @@ impl Keyboard for Editor { self.input_state.pressed_keys.insert(key); } } + KeyCode::F12 => { + if is_playing { + self.signal = Signal::StopPlaying; + info!("Stopping play mode"); + } else { + self.input_state.pressed_keys.insert(key); + } + } KeyCode::KeyL => { if matches!(self.viewport_mode, ViewportMode::Gizmo) && !is_playing { info!("GizmoOrientation set to Local"); @@ -13,7 +13,7 @@ use dropbear_engine::{ use eucalyptus_core::hierarchy::EntityTransformExt; use eucalyptus_core::logging; use eucalyptus_core::states::{Label, WorldLoadingStatus}; -use eucalyptus_core::window::poll; +use eucalyptus_core::window::{CommandBufferPoller}; use log; use parking_lot::Mutex; use tokio::sync::mpsc::unbounded_channel; @@ -149,7 +149,7 @@ impl Scene for Editor { graphics.shared.window.set_title(&title); } - poll(graphics.shared.window.clone()); + self.poll(graphics); { // basic futurequeue spawn queue management. @@ -9,3 +9,4 @@ pub mod signal; pub mod spawn; pub mod stats; pub mod utils; +pub mod command; @@ -158,6 +158,9 @@ DROPBEAR_NATIVE dropbear_get_last_mouse_pos(const InputState* input_ptr, float* DROPBEAR_NATIVE dropbear_is_cursor_hidden(const InputState* input_ptr, BOOL* out_hidden); DROPBEAR_NATIVE dropbear_set_cursor_hidden(InputState* input_ptr, GraphicsCommandQueue* graphics_ptr, BOOL hidden); +// editor +void dropbear_quit(const GraphicsCommandQueue* command_ptr); + #ifdef __cplusplus } // extern "C" #endif // __cplusplus @@ -8,7 +8,7 @@ readme = "README.md" [dependencies] dropbear-engine = { path = "../dropbear-engine" } -eucalyptus-core = { path = "../eucalyptus-core", features = ["default"] } +eucalyptus-core = { path = "../eucalyptus-core", features = ["runtime"], default-features = false } anyhow.workspace = true log.workspace = true @@ -25,6 +25,7 @@ bytemuck.workspace = true log-once.workspace = true egui-wgpu.workspace = true rustc_version_runtime.workspace = true +winit.workspace = true [features] # enable this to allow for JVM support @@ -0,0 +1,51 @@ +use dropbear_engine::graphics::RenderContext; +use eucalyptus_core::window::{CommandBufferPoller, GRAPHICS_COMMAND, CommandBuffer, WindowCommand, get_config}; +use winit::window::CursorGrabMode; + +use crate::scene::RuntimeScene; + +impl CommandBufferPoller for RuntimeScene { + fn poll(&mut self, graphics: &RenderContext) { + while let Ok(cmd) = GRAPHICS_COMMAND.1.try_recv() { + log::trace!("Received GRAPHICS_COMMAND update: {:?}", cmd); + match cmd { + CommandBuffer::WindowCommand(w_cmd) => match w_cmd { + WindowCommand::WindowGrab(is_locked) => { + let mut cfg = get_config().write(); + if cfg.is_locked != is_locked { + if is_locked { + if let Err(e) = graphics.shared.window + .set_cursor_grab(CursorGrabMode::Confined) + .or_else(|_| graphics.shared.window.set_cursor_grab(CursorGrabMode::Locked)) + { + log_once::warn_once!("Failed to grab cursor: {:?}", e); + } else { + log_once::info_once!("Grabbed cursor"); + cfg.is_locked = true; + } + } else if let Err(e) = graphics.shared.window.set_cursor_grab(CursorGrabMode::None) { + log_once::warn_once!("Failed to release cursor: {:?}", e); + } else { + log_once::info_once!("Released cursor"); + cfg.is_locked = false; + } + } + } + WindowCommand::HideCursor(should_hide) => { + let cfg = get_config().write(); + if cfg.is_hidden != should_hide { + if should_hide { + graphics.shared.window.set_cursor_visible(false); + } else { + graphics.shared.window.set_cursor_visible(true); + } + } + } + }, + CommandBuffer::Quit => { + self.scene_command = dropbear_engine::scene::SceneCommand::Quit; + }, + } + } + } +} @@ -2,6 +2,7 @@ mod input; mod scene; mod debug; mod utils; +mod command; use crate::scene::RuntimeScene; use app_dirs2::AppInfo; @@ -16,7 +16,7 @@ use dropbear_engine::wgpu::{self, Color, RenderPipeline}; use dropbear_engine::winit::event_loop::ActiveEventLoop; use dropbear_engine::winit::window::Window; use eucalyptus_core::camera::CameraComponent; -use eucalyptus_core::egui::{self, CentralPanel, UiBuilder}; +use eucalyptus_core::egui::{self, CentralPanel, Frame, UiBuilder}; use eucalyptus_core::hierarchy::EntityTransformExt; use eucalyptus_core::input::InputState; use eucalyptus_core::ptr::{GraphicsPtr, InputStatePtr, WorldPtr}; @@ -25,7 +25,7 @@ use eucalyptus_core::scene::SceneConfig; use eucalyptus_core::scripting::{ScriptManager, ScriptTarget}; use eucalyptus_core::states::{Camera3D, ConfigFile, Light as LightConfig, ModelProperties, Script, SerializedMeshRenderer}; use eucalyptus_core::traits::registry::ComponentRegistry; -use eucalyptus_core::window::GRAPHICS_COMMAND; +use eucalyptus_core::window::{CommandBufferPoller, GRAPHICS_COMMAND}; use hecs::{Entity, World}; use parking_lot::Mutex; use tokio::sync::oneshot; @@ -48,7 +48,7 @@ pub(crate) struct RuntimeScene { script_manager: ScriptManager, script_target: Option<ScriptTarget>, scripts_ready: bool, - scene_command: SceneCommand, + pub scene_command: SceneCommand, current_scene: Option<String>, world_receiver: Option<oneshot::Receiver<World>>, @@ -434,9 +434,9 @@ impl Scene for RuntimeScene { fn update(&mut self, dt: f32, graphics: &mut RenderContext) { graphics.shared.future_queue.poll(); - self.poll_scene_loading(graphics); - - CentralPanel::default().show(&graphics.shared.get_egui_context(), |ui| { + self.poll_scene_loading(graphics); + self.poll(graphics); + CentralPanel::default().frame(Frame::new()).show(&graphics.shared.get_egui_context(), |ui| { if self.render_pipeline.is_none() { ui.label("Loading scene..."); } @@ -447,6 +447,14 @@ impl Scene for RuntimeScene { self.update_world_state(graphics); + let egui_ctx = graphics.shared.get_egui_context(); + let egui_wants_input = egui_ctx.wants_pointer_input() || egui_ctx.wants_keyboard_input(); + + if egui_wants_input { + self.input_state.pressed_keys.clear(); + self.input_state.mouse_button.clear(); + } + if self.scripts_ready { let world_ptr = self.world.as_mut() as WorldPtr; if let Err(err) = unsafe { self.script_manager.update_script(world_ptr, &self.input_state, dt) } { @@ -457,28 +465,34 @@ impl Scene for RuntimeScene { let texture_id = *graphics.shared.texture_id; let available_size = ui.available_rect_before_wrap().size(); + let is_fullscreen = self.window_config.window_configuration.windowed_mode.is_fullscreen(); + let viewport_aspect = self.viewport_resolution.0 as f32 / self.viewport_resolution.1 as f32; + let available_aspect = available_size.x / available_size.y; let active_camera: Option<Entity> = *self.active_camera.lock(); if let Some(cam_ent) = active_camera { if let Ok(mut q) = self.world.query_one::<&mut Camera>(cam_ent) { if let Some(camera) = q.get() { - camera.aspect = viewport_aspect as f64; + if is_fullscreen { + camera.aspect = viewport_aspect as f64; + } else { + camera.aspect = available_aspect as f64; + } camera.update_view_proj(); camera.update(graphics.shared.clone()); } } } - let available_aspect = available_size.x / available_size.y; - let (display_width, display_height) = if available_aspect > viewport_aspect { - let height = available_size.y; - let width = height * viewport_aspect; - (width, height) + let (display_width, display_height) = if is_fullscreen { + if available_aspect > viewport_aspect { + (available_size.x, available_size.x / viewport_aspect) + } else { + (available_size.y * viewport_aspect, available_size.y) + } } else { - let width = available_size.x; - let height = width / viewport_aspect; - (width, height) + (available_size.x, available_size.y) }; let rect = ui.available_rect_before_wrap(); @@ -497,6 +511,7 @@ impl Scene for RuntimeScene { }); self.input_state.window = self.window.clone(); + self.input_state.mouse_delta = None; }); } @@ -82,4 +82,17 @@ class DropbearEngine(val native: NativeEngine) { * This can be run in your update loop without consequences. */ fun callExceptionOnError(toggle: Boolean) = DropbearEngine.callExceptionOnError(toggle) + + /** + * Quits the currently running app or game. + * + * This function can have different behaviours depending on where it is ran. + * - eucalyptus-editor - When called, this exits your Play Mode session and returns you back to + * `EditorState::Editing` + * - redback-runtime - When called, this will exit your current process and kill the app as is. It will + * also drop any pointers and do any additional cleanup. + */ + fun quit() { + native.quit() + } } @@ -84,4 +84,6 @@ expect class NativeEngine { // fun getConnectedGamepads(): List<Gamepad> // ------------------------------------------------------------------- + + fun quit() } @@ -76,4 +76,7 @@ public class JNINative { public static native boolean isCursorHidden(long inputHandle); public static native void setCursorHidden(long inputHandle, long graphicsHandle, boolean hidden); public static native String[] getAllTextures(long worldHandle, long entityHandle); + + // engine + public static native void quit(long graphicsHandle); } @@ -383,4 +383,8 @@ actual class NativeEngine { EntityRef(EntityId(result)) } } + + actual fun quit() { + JNINative.quit(graphicsHandle) + } } @@ -1172,4 +1172,9 @@ actual class NativeEngine { return if (result == 0) EntityRef(EntityId(outParent.value)) else if (exceptionOnError) throw DropbearNativeException("getParent failed with code: $result") else null } } + + actual fun quit() { + val command = graphicsHandle ?: if (exceptionOnError) throw DropbearNativeException("Unable to quit: graphicsHandle does not exist") else return + dropbear_quit(command.reinterpret()) + } }