tirbofish/dropbear · diff
feature: got a console working, so the output can be sent through. Unverified
@@ -390,8 +390,23 @@ impl JavaContext { )? .l()?; - let std_out_writer_class = env.find_class("com/dropbear/logging/StdoutWriter")?; - let log_writer_obj = env.new_object(std_out_writer_class, "()V", &[])?; + let log_writer_obj = match RUNTIME_MODE.get() { + Some(RuntimeMode::Editor) | Some(RuntimeMode::PlayMode) => { + let port = 56624; + if std::net::TcpStream::connect(format!("127.0.0.1:{}", port)).is_ok() { + let socket_writer_class = env.find_class("com/dropbear/logging/SocketWriter")?; + env.new_object(socket_writer_class, "()V", &[])? + } else { + log::debug!("Editor console not reachable at 127.0.0.1:{}. Falling back to StdoutWriter.", port); + let std_out_writer_class = env.find_class("com/dropbear/logging/StdoutWriter")?; + env.new_object(std_out_writer_class, "()V", &[])? + } + } + _ => { + let std_out_writer_class = env.find_class("com/dropbear/logging/StdoutWriter")?; + env.new_object(std_out_writer_class, "()V", &[])? + } + }; if self.system_manager_instance.is_none() { let engine_ref = self @@ -257,6 +257,7 @@ pub enum EditorTab { ModelEntityList, // right side, Viewport, // middle, ErrorConsole, + Console, Plugin(usize), } @@ -41,12 +41,11 @@ pub struct UiContext { pub fn poll() { UI_CONTEXT.with(|v| { let ctx = v.borrow(); - let _yakui = ctx.yakui_state.lock(); let instructions = ctx.instruction_set.lock().drain(..).collect::<Vec<UIInstruction>>(); for i in instructions { match i { UIInstruction::StartColumn => { - + }, UIInstruction::EndColumn => { @@ -0,0 +1,88 @@ +use std::io::{BufRead, BufReader}; +use std::net::{TcpListener, TcpStream}; +use std::sync::Arc; +use parking_lot::Mutex; + +pub struct EucalyptusConsole { + pub buffer: Arc<Mutex<Vec<String>>>, + pub history: Vec<String>, + + pub show_info: bool, + pub show_warning: bool, + pub show_error: bool, + pub show_debug: bool, + pub show_trace: bool, + pub auto_scroll: bool, +} + +impl EucalyptusConsole { + /// Creates a new instance of a [EucalyptusConsole], and + pub fn new(port: Option<&str>) -> Self { + let result = Self { + buffer: Arc::new(Default::default()), + history: vec![], + show_info: true, + show_warning: true, + show_error: true, + show_debug: false, + show_trace: false, + auto_scroll: true, + }; + + let buf_clone = result.buffer.clone(); + let addr = format!("127.0.0.1:{}", port.unwrap_or("56624")); + std::thread::spawn(move || { + let listener = TcpListener::bind(&addr).unwrap(); + + log::info!("eucalyptus-editor debug console started at {}", addr); + + loop { + for stream in listener.incoming() { + match stream { + Ok(stream) => { + let buf_clone = buf_clone.clone(); + std::thread::spawn(move || { + EucalyptusConsole::handle_client(stream, buf_clone) + }); + } + Err(e) => { + eprintln!("Connection failed: {}", e); + } + } + } + } + }); + + result + } + + fn handle_client(stream: TcpStream, buf: Arc<Mutex<Vec<String>>>) { + let peer_addr = stream.peer_addr().unwrap(); + println!("New connection from: {}", peer_addr); + + let reader = BufReader::new(stream); + + for line in reader.lines() { + match line { + Ok(text) => { + buf.lock().push(text); + } + Err(e) => { + buf.lock().push(format!("Error reading from {}: {}", peer_addr, e)); + break; + } + } + } + + println!("Connection closed: {}", peer_addr); + } + + /// Drains all from the thread-safe buffer and adds to the history, while returning that value. + /// + /// It is recommended to use this function. + pub fn take(&mut self) -> Vec<String> { + let buf = self.buffer.lock().drain(..).collect::<Vec<String>>(); + buf.iter().for_each(|v| self.history.push(v.clone())); + buf + } +} @@ -1,6 +1,7 @@ use std::path::PathBuf; pub enum ErrorLevel { + Info, Warn, Error, } @@ -36,6 +36,7 @@ use eucalyptus_core::physics::collider::{Collider, ColliderGroup}; use eucalyptus_core::physics::kcc::KCC; use eucalyptus_core::physics::rigidbody::RigidBody; use eucalyptus_core::properties::CustomProperties; +use crate::editor::console::EucalyptusConsole; pub struct EditorTabViewer<'a> { pub view: egui::TextureId, @@ -53,6 +54,7 @@ pub struct EditorTabViewer<'a> { pub plugin_registry: &'a mut PluginRegistry, pub component_registry: &'a ComponentRegistry, pub build_logs: &'a mut Vec<String>, + pub eucalyptus_console: &'a mut EucalyptusConsole, // "wah wah its unsafe, its using raw pointers" shut the fuck up if it breaks i will know pub editor: *mut Editor, @@ -191,7 +193,8 @@ impl<'a> TabViewer for EditorTabViewer<'a> { "Unknown Plugin Name".into() } } - EditorTab::ErrorConsole => "Error Console".into(), + EditorTab::ErrorConsole => "Build Output".into(), + EditorTab::Console => "Console".into(), } } @@ -1012,29 +1015,32 @@ impl<'a> TabViewer for EditorTabViewer<'a> { } let mut list: Vec<ConsoleItem> = Vec::new(); - let index = 0; - for line in log { + for (index, line) in log.iter().enumerate() { if line.contains("The required library") { list.push(ConsoleItem { error_level: ErrorLevel::Error, msg: line.clone(), file_location: None, line_ref: None, - id: index + 1, + id: index as u64, }); - } - - if let Some((error_level, path, loc)) = parse_compiler_location(line) { + } else if let Some((error_level, path, loc)) = parse_compiler_location(line) { list.push(ConsoleItem { error_level, msg: line.clone(), file_location: Some(path), line_ref: Some(loc), - id: index + 1, + id: index as u64, + }); + } else { + list.push(ConsoleItem { + error_level: ErrorLevel::Info, + msg: line.clone(), + file_location: None, + line_ref: None, + id: index as u64, }); } - - // thats it for now } list } @@ -1043,6 +1049,7 @@ impl<'a> TabViewer for EditorTabViewer<'a> { egui::ScrollArea::vertical() .auto_shrink([false, false]) + .stick_to_bottom(true) .show(ui, |ui| { if logs.is_empty() { ui.label("Build output will appear here once available."); @@ -1050,64 +1057,141 @@ impl<'a> TabViewer for EditorTabViewer<'a> { } for item in &logs { - let (bg_color, text_color) = match item.error_level { + let (bg_color, text_color, stroke_color) = match item.error_level { ErrorLevel::Error => ( egui::Color32::from_rgb(60, 20, 20), egui::Color32::from_rgb(255, 200, 200), + egui::Color32::from_rgb(255, 200, 200), ), ErrorLevel::Warn => ( egui::Color32::from_rgb(40, 40, 10), egui::Color32::from_rgb(255, 255, 200), + egui::Color32::from_rgb(255, 255, 200), + ), + ErrorLevel::Info => ( + egui::Color32::TRANSPARENT, + ui.style().visuals.text_color(), + egui::Color32::TRANSPARENT, ), }; - let available_width = ui.available_width(); - let frame = egui::Frame::new() - .inner_margin(Margin::symmetric(8, 6)) - .fill(bg_color) - .stroke(egui::Stroke::new(1.0, text_color)); - - let response = frame - .show(ui, |ui| { - ui.set_width(available_width - 10.0); - ui.horizontal(|ui| { - ui.label(RichText::new(&item.msg).color(text_color)); - }); - }) - .response; - - if response.clicked() { - log::debug!("Log item clicked: {}", &item.id); - if let (Some(path), Some(loc)) = - (&item.file_location, &item.line_ref) - { - let location_arg = format!("{}:{}", path.display(), loc); - - match std::process::Command::new("code") - .args(["-g", &location_arg]) - .spawn() - .map(|_| ()) + if matches!(item.error_level, ErrorLevel::Info) { + ui.label(RichText::new(&item.msg).monospace()); + } else { + let available_width = ui.available_width(); + let frame = egui::Frame::new() + .inner_margin(Margin::symmetric(8, 6)) + .fill(bg_color) + .stroke(egui::Stroke::new(1.0, stroke_color)); + + let response = frame + .show(ui, |ui| { + ui.set_width(available_width - 10.0); + ui.horizontal(|ui| { + ui.label(RichText::new(&item.msg).color(text_color).monospace()); + }); + }) + .response; + + if response.clicked() { + log::debug!("Log item clicked: {}", &item.id); + if let (Some(path), Some(loc)) = + (&item.file_location, &item.line_ref) { - Ok(()) => { - log::info!( - "Launched Visual Studio Code at the error: {}", - &location_arg - ); - } - Err(e) => { - warn!( - "Failed to open '{}' in VS Code: {}", - location_arg, e - ); + let location_arg = format!("{}:{}", path.display(), loc); + + match std::process::Command::new("code") + .args(["-g", &location_arg]) + .spawn() + .map(|_| ()) + { + Ok(()) => { + log::info!( + "Launched Visual Studio Code at the error: {}", + &location_arg + ); + } + Err(e) => { + warn!( + "Failed to open '{}' in VS Code: {}", + location_arg, e + ); + } } } } } - - ui.add_space(4.0); } }); } + EditorTab::Console => { + ui.heading("Console"); + ui.separator(); + + ui.horizontal(|ui| { + if ui.button("Clear").clicked() { + self.eucalyptus_console.history.clear(); + } + + ui.separator(); + + ui.checkbox(&mut self.eucalyptus_console.show_info, "Info"); + ui.checkbox(&mut self.eucalyptus_console.show_warning, "Warning"); + ui.checkbox(&mut self.eucalyptus_console.show_error, "Error"); + ui.checkbox(&mut self.eucalyptus_console.show_debug, "Debug"); + ui.checkbox(&mut self.eucalyptus_console.show_trace, "Trace"); + + ui.separator(); + + ui.checkbox(&mut self.eucalyptus_console.auto_scroll, "Auto-scroll"); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + ui.label(format!("Logs: {}", self.eucalyptus_console.history.len())); + }); + }); + + ui.separator(); + + let _ = self.eucalyptus_console.take(); + + let scroll = egui::ScrollArea::vertical() + .auto_shrink([false, false]) + .stick_to_bottom(self.eucalyptus_console.auto_scroll); + + scroll.show(ui, |ui| { + for log in &self.eucalyptus_console.history { + let is_error = log.contains("[ERROR]") || log.contains("[FATAL]"); + let is_warn = log.contains("[WARN]"); + let is_debug = log.contains("[DEBUG]"); + let is_trace = log.contains("[TRACE]"); + let is_info = !is_error && !is_warn && !is_debug && !is_trace; + + if is_error && !self.eucalyptus_console.show_error { continue; } + if is_warn && !self.eucalyptus_console.show_warning { continue; } + if is_debug && !self.eucalyptus_console.show_debug { continue; } + if is_trace && !self.eucalyptus_console.show_trace { continue; } + if is_info && !self.eucalyptus_console.show_info { continue; } + + let color = if is_error { + egui::Color32::from_rgb(255, 100, 100) + } else if is_warn { + egui::Color32::from_rgb(255, 200, 50) + } else if is_debug { + egui::Color32::from_rgb(100, 200, 255) + } else if is_trace { + egui::Color32::from_rgb(150, 150, 150) + } else { + egui::Color32::LIGHT_GRAY + }; + + ui.add(egui::Label::new( + egui::RichText::new(log) + .color(color) + .monospace() + )); + } + }); + } } } } @@ -4,6 +4,7 @@ pub mod dock; pub mod input; pub mod scene; pub mod settings; +mod console; pub(crate) use crate::editor::dock::*; @@ -66,6 +67,7 @@ use eucalyptus_core::physics::collider::shader::ColliderInstanceRaw; use eucalyptus_core::physics::collider::shader::ColliderWireframePipeline; use eucalyptus_core::properties::CustomProperties; use crate::about::AboutWindow; +use crate::editor::console::EucalyptusConsole; use crate::editor::settings::editor::{EditorSettingsWindow, EDITOR_SETTINGS}; use crate::editor::settings::project::ProjectSettingsWindow; @@ -110,6 +112,7 @@ pub struct Editor { pub(crate) editor_state: EditorState, pub gizmo_mode: EnumSet<GizmoMode>, pub gizmo_orientation: GizmoOrientation, + pub console: EucalyptusConsole, // might as well save some memory if its not required... // #[allow(unused)] // unused to allow for JVM to startup @@ -231,6 +234,7 @@ impl Editor { editor_state: EditorState::Editing, gizmo_mode: EnumSet::empty(), gizmo_orientation: GizmoOrientation::Global, + console: EucalyptusConsole::new(None), play_mode_backup: None, input_state: Box::new(InputState::new()), light_cube_pipeline: None, @@ -976,9 +980,12 @@ impl Editor { if ui_window.button("Open Error Console").clicked() { self.dock_state.push_to_focused_leaf(EditorTab::ErrorConsole); } + if ui_window.button("Open Debug Console").clicked() { + self.dock_state.push_to_focused_leaf(EditorTab::Console); + } if self.plugin_registry.plugins.len() == 0 { ui_window.label( - egui::RichText::new("No plugins ") + egui::RichText::new("No plugins") .color(ui_window.visuals().weak_text_color()) ); } @@ -1100,6 +1107,7 @@ impl Editor { editor: editor_ptr, build_logs: &mut self.build_logs, component_registry: &self.component_registry, + eucalyptus_console: &mut self.console, }, ); }); @@ -113,6 +113,7 @@ pub struct PlayMode { impl PlayMode { pub fn new(initial_scene: Option<String>) -> anyhow::Result<Self> { + eucalyptus_core::utils::start_deadlock_detector(); let mut component_registry = ComponentRegistry::new(); @@ -1,9 +1,9 @@ package com.dropbear.logging -enum class LogLevel { - TRACE, - DEBUG, - INFO, - WARN, - ERROR +enum class LogLevel(val ansi: String) { + TRACE("\u001B[38;5;81m"), + DEBUG("\u001B[38;5;220m"), + INFO("\u001B[38;5;46m"), + WARN("\u001B[38;5;214m"), + ERROR("\u001B[31m"), } @@ -5,7 +5,7 @@ import kotlin.jvm.JvmStatic @Suppress("unused") object Logger { - private var writer: LogWriter = StdoutWriter() + private var writer: LogWriter = SocketWriter() private var minLevel: LogLevel = LogLevel.INFO private var defaultTarget: String = "dropbear" @@ -0,0 +1,11 @@ +package com.dropbear.logging + +expect class SocketWriter(): LogWriter { + override fun log( + level: LogLevel, + target: String, + message: String, + file: String?, + line: Int? + ) +} @@ -5,6 +5,8 @@ import kotlinx.datetime.toLocalDateTime import kotlin.time.Clock class StdoutWriter: LogWriter { + private val reset = "\u001B[0m" + override fun log( level: LogLevel, target: String, @@ -17,8 +19,9 @@ class StdoutWriter: LogWriter { val timestamp = now.toLocalDateTime(timeZone) val location = if (file != null && line != null) "[$file:$line] " else "" when (level) { - LogLevel.ERROR, LogLevel.WARN -> error("[$timestamp] [$level] $location[$target] $message") - else -> println("[$timestamp] [$level] $location[$target] $message") + LogLevel.ERROR -> println("${level.ansi}[$timestamp] [$level] $location[$target] $message$reset") + LogLevel.WARN -> println("${level.ansi}[$timestamp] [$level] $location[$target] $message$reset") + else -> println("[$timestamp] [${level.ansi}$level$reset] $location[$target] $message") } } } @@ -1,10 +1,5 @@ package com.dropbear.ui -import com.dropbear.math.Vector2d -import com.dropbear.ui.primitive.Circle -import com.dropbear.ui.primitive.Rectangle -import com.dropbear.utils.ID - /** * A command buffer to the 2D part of the game. Can be used for HUD and stuff. * @@ -0,0 +1,57 @@ +package com.dropbear.logging + +import java.net.Socket +import java.io.PrintWriter +import java.util.concurrent.Executors +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import kotlin.time.Clock + +actual class SocketWriter actual constructor() : LogWriter { + private val executor = Executors.newSingleThreadExecutor() + private var writer: PrintWriter? = null + + init { + Thread.setDefaultUncaughtExceptionHandler { thread, throwable -> + val stackTrace = java.io.StringWriter().apply { + throwable.printStackTrace(java.io.PrintWriter(this)) + }.toString() + + log(LogLevel.ERROR, "UncaughtException", "Exception in thread \"${thread.name}\": $throwable\n$stackTrace", null, null) + } + + executor.submit { + try { + // Connect to the editor console + val socket = Socket("127.0.0.1", 56624) + writer = PrintWriter(socket.getOutputStream(), true) + } catch (e: Exception) { + // Silently fail or log to stderr if connection fails + System.err.println("Failed to connect to logging socket: ${e.message}") + } + } + } + + actual override fun log( + level: LogLevel, + target: String, + message: String, + file: String?, + line: Int? + ) { + val now = Clock.System.now() + val timeZone = TimeZone.currentSystemDefault() + val timestamp = now.toLocalDateTime(timeZone) + val location = if (file != null && line != null) "[$file:$line] " else "" + + val formatted = "[$timestamp] [$level] $location[$target] $message" + + executor.submit { + try { + writer?.println(formatted) + } catch (e: Exception) { + // Ignore write errors to avoid crashing + } + } + } +} @@ -1 +0,0 @@ -package com.dropbear.ui @@ -33,6 +33,8 @@ actual class NativeEngine { this.physicsEngineHandle = ctx?.physics_engine?.rawValue?.let { interpretCPointer(it) } this.uiBufferHandle = ctx?.graphics?.rawValue?.let { interpretCPointer(it) } + Logger.init(com.dropbear.logging.SocketWriter()) + // if release, always enable exceptionOnError if (!Platform.isDebugBinary) { exceptionOnError = true @@ -0,0 +1,14 @@ +package com.dropbear.logging + +actual class SocketWriter actual constructor() : LogWriter { + actual override fun log( + level: LogLevel, + target: String, + message: String, + file: String?, + line: Int? + ) { + TODO("Not yet implemented") + } + +} @@ -1 +0,0 @@ -package com.dropbear.ui