use std::io::{self, BufRead, BufReader};
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::sync::mpsc::{self, Receiver, Sender};
use std::sync::Mutex;
use std::thread;
#[derive(Debug, Clone)]
pub enum BuildOutput {
Line(String),
Progress(u32, u32),
CurrentCrate(String),
BuildComplete,
BuildFailed(String),
GameStarted,
GameExited(Option<i32>),
}
#[derive(Debug, Clone, Default)]
pub enum GameBuildState {
#[default]
Idle,
Building {
progress: Option<(u32, u32)>,
current_crate: Option<String>,
output_lines: Vec<String>,
log_file_path: Option<PathBuf>,
},
Running {
log_file_path: Option<PathBuf>,
},
Finished {
log_file_path: Option<PathBuf>,
},
Failed {
message: String,
log_file_path: Option<PathBuf>,
},
}
pub fn get_build_log_path() -> PathBuf {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let temp_dir = std::env::temp_dir();
temp_dir.join(format!("bevy_map_editor_build_{}.log", timestamp))
}
#[derive(Debug)]
pub enum LaunchError {
IoError(io::Error),
ProjectNotConfigured,
MapNotSaved,
CargoNotFound,
LaunchFailed(String),
ProjectNotFound(PathBuf),
}
impl std::fmt::Display for LaunchError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LaunchError::IoError(e) => write!(f, "IO error: {}", e),
LaunchError::ProjectNotConfigured => {
write!(
f,
"Game project not configured. Go to Project > Game Settings."
)
}
LaunchError::MapNotSaved => write!(f, "Save the map project before running the game."),
LaunchError::CargoNotFound => {
write!(f, "Cargo not found. Please install Rust toolchain.")
}
LaunchError::LaunchFailed(msg) => write!(f, "Failed to launch game: {}", msg),
LaunchError::ProjectNotFound(path) => {
write!(f, "Game project not found at: {}", path.display())
}
}
}
}
impl std::error::Error for LaunchError {}
impl From<io::Error> for LaunchError {
fn from(e: io::Error) -> Self {
LaunchError::IoError(e)
}
}
pub struct LaunchOptions {
pub project_path: PathBuf,
pub release: bool,
pub hot_reload: bool,
}
pub struct LaunchResult {
pub child: Option<Child>,
pub error: Option<LaunchError>,
}
impl LaunchResult {
pub fn success(child: Child) -> Self {
Self {
child: Some(child),
error: None,
}
}
pub fn failure(error: LaunchError) -> Self {
Self {
child: None,
error: Some(error),
}
}
}
pub fn launch_game(options: &LaunchOptions) -> LaunchResult {
if !options.project_path.exists() {
return LaunchResult::failure(LaunchError::ProjectNotFound(options.project_path.clone()));
}
if !options.project_path.join("Cargo.toml").exists() {
return LaunchResult::failure(LaunchError::ProjectNotFound(options.project_path.clone()));
}
let mut cmd = Command::new("cargo");
cmd.arg("run");
cmd.current_dir(&options.project_path);
if options.release {
cmd.arg("--release");
}
if options.hot_reload {
cmd.args(["--features", "hot_reload"]);
}
cmd.stdout(Stdio::inherit());
cmd.stderr(Stdio::inherit());
match cmd.spawn() {
Ok(child) => LaunchResult::success(child),
Err(e) => LaunchResult::failure(LaunchError::LaunchFailed(e.to_string())),
}
}
pub fn is_game_running(child: &mut Option<Child>) -> bool {
if let Some(ref mut c) = child {
match c.try_wait() {
Ok(Some(_)) => {
*child = None;
false
}
Ok(None) => true, Err(_) => {
*child = None;
false
}
}
} else {
false
}
}
pub fn kill_game(child: &mut Option<Child>) {
if let Some(ref mut c) = child {
let _ = c.kill();
let _ = c.wait(); *child = None;
}
}
pub fn sync_map_to_game(
map_path: &Path,
game_project_path: &Path,
) -> Result<PathBuf, std::io::Error> {
let game_assets = game_project_path.join("assets").join("maps");
std::fs::create_dir_all(&game_assets)?;
let map_filename = map_path
.file_name()
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Invalid map path"))?;
let dest_path = game_assets.join(map_filename);
std::fs::copy(map_path, &dest_path)?;
Ok(dest_path)
}
pub fn sync_tileset_to_game(
tileset_path: &Path,
source_assets_dir: &Path,
game_project_path: &Path,
) -> Result<PathBuf, std::io::Error> {
let game_assets = game_project_path.join("assets");
let dest_path = if let Ok(rel) = tileset_path.strip_prefix(source_assets_dir) {
game_assets.join(rel)
} else {
let path_str = tileset_path.to_string_lossy();
if let Some(pos) = path_str
.find("assets/")
.or_else(|| path_str.find("assets\\"))
{
let after_assets = &path_str[pos + 7..]; game_assets.join(after_assets)
} else {
let filename = tileset_path
.file_name()
.unwrap_or_else(|| std::ffi::OsStr::new("unknown"));
game_assets.join(filename)
}
};
if let Some(parent) = dest_path.parent() {
std::fs::create_dir_all(parent)?;
}
if tileset_path.exists() {
let contents = std::fs::read(tileset_path)?;
std::fs::write(&dest_path, contents)?;
}
Ok(dest_path)
}
pub fn sync_all_assets_to_game(
project: &crate::project::Project,
map_path: &Path,
game_project_path: &Path,
assets_base_path: &Path,
) -> Result<PathBuf, std::io::Error> {
let map_dest = sync_map_to_game(map_path, game_project_path)?;
bevy::log::info!("Synced map to: {}", map_dest.display());
for tileset in &project.tilesets {
for image in &tileset.images {
let image_path = assets_base_path.join(&image.path);
if image_path.exists() {
match sync_tileset_to_game(&image_path, assets_base_path, game_project_path) {
Ok(dest) => bevy::log::info!("Synced tileset image: {}", dest.display()),
Err(e) => {
bevy::log::warn!("Failed to sync tileset image {}: {}", image.path, e)
}
}
} else {
bevy::log::warn!("Tileset image not found: {}", image_path.display());
}
}
if let Some(path) = &tileset.path {
let image_path = assets_base_path.join(path);
if image_path.exists() {
match sync_tileset_to_game(&image_path, assets_base_path, game_project_path) {
Ok(dest) => bevy::log::info!("Synced legacy tileset: {}", dest.display()),
Err(e) => bevy::log::warn!("Failed to sync legacy tileset {}: {}", path, e),
}
}
}
}
for sprite_sheet in &project.sprite_sheets {
let sheet_path = assets_base_path.join(&sprite_sheet.sheet_path);
if sheet_path.exists() {
match sync_tileset_to_game(&sheet_path, assets_base_path, game_project_path) {
Ok(dest) => bevy::log::info!("Synced sprite sheet: {}", dest.display()),
Err(e) => {
bevy::log::warn!(
"Failed to sync sprite sheet {}: {}",
sprite_sheet.sheet_path,
e
)
}
}
} else {
bevy::log::warn!("Sprite sheet not found: {}", sheet_path.display());
}
}
Ok(map_dest)
}
pub struct AsyncBuildHandle {
pub receiver: Mutex<Receiver<BuildOutput>>,
pub cancel_sender: Sender<()>,
}
impl AsyncBuildHandle {
pub fn try_recv(&self) -> Option<BuildOutput> {
self.receiver.lock().ok()?.try_recv().ok()
}
pub fn cancel(&self) {
let _ = self.cancel_sender.send(());
}
}
pub fn launch_game_async(options: LaunchOptions) -> Result<AsyncBuildHandle, LaunchError> {
if !options.project_path.exists() {
return Err(LaunchError::ProjectNotFound(options.project_path.clone()));
}
if !options.project_path.join("Cargo.toml").exists() {
return Err(LaunchError::ProjectNotFound(options.project_path.clone()));
}
let (output_tx, output_rx) = mpsc::channel();
let (cancel_tx, cancel_rx) = mpsc::channel();
thread::spawn(move || {
run_build_thread(options, output_tx, cancel_rx);
});
Ok(AsyncBuildHandle {
receiver: Mutex::new(output_rx),
cancel_sender: cancel_tx,
})
}
fn run_build_thread(options: LaunchOptions, tx: Sender<BuildOutput>, cancel_rx: Receiver<()>) {
let mut cmd = Command::new("cargo");
cmd.arg("run");
cmd.current_dir(&options.project_path);
if options.release {
cmd.arg("--release");
}
if options.hot_reload {
cmd.args(["--features", "hot_reload"]);
}
cmd.env("CARGO_TERM_PROGRESS_WHEN", "always");
cmd.env("CARGO_TERM_PROGRESS_WIDTH", "80");
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
let mut child = match cmd.spawn() {
Ok(child) => child,
Err(e) => {
let _ = tx.send(BuildOutput::BuildFailed(format!(
"Failed to spawn cargo: {}",
e
)));
return;
}
};
let stderr = child.stderr.take().unwrap();
let tx_stderr = tx.clone();
let stderr_handle = thread::spawn(move || {
use std::io::Read;
let mut reader = BufReader::new(stderr);
let mut buffer = String::new();
let mut byte_buf = [0u8; 1];
let mut build_complete_sent = false;
loop {
match reader.read(&mut byte_buf) {
Ok(0) => break, Ok(_) => {
let ch = byte_buf[0] as char;
if ch == '\r' || ch == '\n' {
if !buffer.is_empty() {
let line = buffer.clone();
buffer.clear();
if let Some((current, total)) = parse_progress(&line) {
let _ = tx_stderr.send(BuildOutput::Progress(current, total));
}
if let Some(crate_name) = parse_compiling_crate(&line) {
let _ = tx_stderr.send(BuildOutput::CurrentCrate(crate_name));
}
if !build_complete_sent && line.trim_start().starts_with("Finished") {
let _ = tx_stderr.send(BuildOutput::BuildComplete);
build_complete_sent = true;
}
if line.trim_start().starts_with("Running `") {
let _ = tx_stderr.send(BuildOutput::GameStarted);
}
if !line.contains("Building [") {
let _ = tx_stderr.send(BuildOutput::Line(line));
}
}
} else {
buffer.push(ch);
}
}
Err(_) => break,
}
}
});
let stdout = child.stdout.take().unwrap();
let tx_stdout = tx.clone();
let stdout_handle = thread::spawn(move || {
let reader = BufReader::new(stdout);
for line in reader.lines().map_while(Result::ok) {
let _ = tx_stdout.send(BuildOutput::Line(line));
}
});
loop {
if cancel_rx.try_recv().is_ok() {
let _ = child.kill();
let _ = child.wait();
let _ = tx.send(BuildOutput::BuildFailed("Build cancelled".to_string()));
return;
}
match child.try_wait() {
Ok(Some(status)) => {
let _ = stderr_handle.join();
let _ = stdout_handle.join();
if status.success() {
let _ = tx.send(BuildOutput::GameExited(status.code()));
} else {
let _ = tx.send(BuildOutput::BuildFailed(format!(
"Process exited with code: {:?}",
status.code()
)));
}
return;
}
Ok(None) => {
thread::sleep(std::time::Duration::from_millis(50));
}
Err(e) => {
let _ = tx.send(BuildOutput::BuildFailed(format!(
"Error waiting for process: {}",
e
)));
return;
}
}
}
}
fn strip_ansi_codes(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\x1b' {
if chars.peek() == Some(&'[') {
chars.next(); while let Some(&c) = chars.peek() {
chars.next();
if c.is_ascii_alphabetic() {
break;
}
}
}
} else {
result.push(ch);
}
}
result
}
fn parse_progress(line: &str) -> Option<(u32, u32)> {
let clean_line = strip_ansi_codes(line);
if !clean_line.contains("Building [") {
return None;
}
if let Some(bracket_end) = clean_line.find("] ") {
let after_bracket = &clean_line[bracket_end + 2..];
if let Some(colon_pos) = after_bracket.find(':') {
let numbers = &after_bracket[..colon_pos];
let parts: Vec<&str> = numbers.split('/').collect();
if parts.len() == 2 {
if let (Ok(current), Ok(total)) = (parts[0].trim().parse(), parts[1].trim().parse())
{
return Some((current, total));
}
}
}
}
None
}
fn parse_compiling_crate(line: &str) -> Option<String> {
let clean_line = strip_ansi_codes(line);
let trimmed = clean_line.trim();
if let Some(rest) = trimmed.strip_prefix("Compiling ") {
if let Some(v_pos) = rest.find(" v") {
return Some(rest[..v_pos].to_string());
}
}
None
}