diff --git a/src/builds.rs b/src/builds.rs index fc48f99..3ebf6fb 100644 --- a/src/builds.rs +++ b/src/builds.rs @@ -1,5 +1,6 @@ use crate::auth::AuthManager; -use crate::config::{self, WavedashConfig}; +use crate::config::{self, EngineKind, WavedashConfig}; +use crate::dev::entrypoint::{fetch_entrypoint_params, locate_html_entrypoint}; use crate::file_staging::FileStaging; use anyhow::Result; use serde::{Deserialize, Serialize}; @@ -157,14 +158,39 @@ pub async fn handle_build_push(config_path: PathBuf, verbose: bool, message: Opt anyhow::bail!("No files found in {}", upload_dir.display()); } - // Get temporary R2 credentials (includes build size) let engine_kind = wavedash_config.engine_type()?; + let engine_version = wavedash_config.engine_version(); + let entrypoint_params = match engine_kind { + Some(EngineKind::Defold) => { + let html_entrypoint = locate_html_entrypoint(&upload_dir); + let html_path = html_entrypoint.as_deref().ok_or_else(|| { + anyhow::anyhow!("No HTML file found in upload_dir; required for DEFOLD builds") + })?; + let ver = engine_version + .ok_or_else(|| anyhow::anyhow!("DEFOLD engine requires a version"))?; + let html_relative_path = html_path + .strip_prefix(&upload_dir) + .unwrap_or(html_path) + .to_string_lossy() + .replace('\\', "/"); + Some( + fetch_entrypoint_params("DEFOLD", ver, html_path, Some(&html_relative_path)) + .await?, + ) + } + Some(EngineKind::JsDos | EngineKind::Ruffle | EngineKind::RenPy) => { + wavedash_config.executable_entrypoint_params() + } + _ => None, + }; + + // Get temporary R2 credentials (includes build size) let creds = get_temp_credentials( &wavedash_config.game_id, engine_kind.map(|e| e.as_label()), - wavedash_config.engine_version(), + engine_version, wavedash_config.entrypoint(), - wavedash_config.executable_entrypoint_params(), + entrypoint_params, message.as_deref(), total_bytes, &api_key, diff --git a/src/config.rs b/src/config.rs index ef5f4cf..76e02ac 100644 --- a/src/config.rs +++ b/src/config.rs @@ -147,6 +147,11 @@ pub struct UnitySection { pub version: String, } +#[derive(Debug, Deserialize)] +pub struct DefoldSection { + pub version: String, +} + /// Shape for engines whose runtime is fetched as a single executable file /// (plus an optional loader script). Used by JSDOS, Ruffle, and Ren'Py. #[derive(Debug, Deserialize)] @@ -168,6 +173,9 @@ pub struct WavedashConfig { #[serde(rename = "unity")] pub unity: Option, + #[serde(rename = "defold")] + pub defold: Option, + #[serde(rename = "jsdos")] pub jsdos: Option, @@ -182,6 +190,7 @@ pub struct WavedashConfig { pub enum EngineKind { Godot, Unity, + Defold, JsDos, Ruffle, RenPy, @@ -192,6 +201,7 @@ impl EngineKind { match self { EngineKind::Godot => "GODOT", EngineKind::Unity => "UNITY", + EngineKind::Defold => "DEFOLD", EngineKind::JsDos => "JSDOS", EngineKind::Ruffle => "RUFFLE", EngineKind::RenPy => "RENPY", @@ -236,6 +246,7 @@ impl WavedashConfig { let engines: Vec = [ self.godot.is_some().then_some(EngineKind::Godot), self.unity.is_some().then_some(EngineKind::Unity), + self.defold.is_some().then_some(EngineKind::Defold), self.jsdos.is_some().then_some(EngineKind::JsDos), self.ruffle.is_some().then_some(EngineKind::Ruffle), self.renpy.is_some().then_some(EngineKind::RenPy), @@ -248,7 +259,7 @@ impl WavedashConfig { 0 => Ok(None), 1 => Ok(Some(engines[0])), _ => anyhow::bail!( - "Config must have at most one engine section: [godot], [unity], [jsdos], [ruffle], or [renpy]" + "Config must have at most one engine section: [godot], [unity], [defold], [jsdos], [ruffle], or [renpy]" ), } } @@ -270,6 +281,9 @@ impl WavedashConfig { if let Some(unity) = &self.unity { return Some(&unity.version); } + if let Some(defold) = &self.defold { + return Some(&defold.version); + } self.executable_section().map(|s| s.version.as_str()) } diff --git a/src/dev/entrypoint.rs b/src/dev/entrypoint.rs index 5266f9c..3387336 100644 --- a/src/dev/entrypoint.rs +++ b/src/dev/entrypoint.rs @@ -14,12 +14,16 @@ struct EntrypointParamsResponse { entrypoint_params: Value, } +// Shared by the Godot/Unity/Defold dev and build flows. A root-level `index.html` +// short-circuits, so single-HTML exports (Godot/Unity) keep their old behavior; the +// mtime/architecture ranking below only matters for nested multi-HTML dists (Defold). pub fn locate_html_entrypoint(upload_dir: &Path) -> Option { let default_index = upload_dir.join("index.html"); if default_index.is_file() { return Some(default_index); } + let mut html_files: Vec = Vec::new(); for entry in WalkDir::new(upload_dir) .min_depth(1) .into_iter() @@ -28,16 +32,50 @@ pub fn locate_html_entrypoint(upload_dir: &Path) -> Option { if entry.file_type().is_file() { if let Some(ext) = entry.path().extension() { if ext.eq_ignore_ascii_case("html") { - return Some(entry.into_path()); + html_files.push(entry.into_path()); } } } } - None + html_files.sort_by(|a, b| { + let modified_a = a.metadata().and_then(|m| m.modified()).ok(); + let modified_b = b.metadata().and_then(|m| m.modified()).ok(); + let relative_a = a + .strip_prefix(upload_dir) + .unwrap_or(a) + .to_string_lossy() + .replace('\\', "/"); + let relative_b = b + .strip_prefix(upload_dir) + .unwrap_or(b) + .to_string_lossy() + .replace('\\', "/"); + let architecture_score = |relative: &str| { + if relative.starts_with("wasm-web/") { + 0 + } else if relative.starts_with("js-web/") { + 1 + } else { + 2 + } + }; + + modified_b + .cmp(&modified_a) + .then_with(|| architecture_score(&relative_a).cmp(&architecture_score(&relative_b))) + .then_with(|| relative_a.cmp(&relative_b)) + }); + + html_files.into_iter().next() } -pub async fn fetch_entrypoint_params(engine: &str, engine_version: &str, html_path: &Path) -> Result { +pub async fn fetch_entrypoint_params( + engine: &str, + engine_version: &str, + html_path: &Path, + html_relative_path: Option<&str>, +) -> Result { let html_content = fs::read_to_string(html_path) .with_context(|| format!("Failed to read {}", html_path.display()))?; let api_host = config::get("api_host")?; @@ -53,6 +91,7 @@ pub async fn fetch_entrypoint_params(engine: &str, engine_version: &str, html_pa "engine": engine, "engineVersion": engine_version, "htmlContent": html_content, + "htmlPath": html_relative_path, })) .send() .await diff --git a/src/dev/mod.rs b/src/dev/mod.rs index 664a5d5..6b8e825 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -9,7 +9,7 @@ use crate::config::{self, EngineKind, WavedashConfig}; use crate::file_staging::FileStaging; mod dev_app; -mod entrypoint; +pub(crate) mod entrypoint; mod launcher; use dev_app::{ensure_dev_app, user_data_dir}; @@ -111,7 +111,7 @@ pub async fn handle_dev(config_path: Option, verbose: bool) -> Result<( let html_entrypoint = locate_html_entrypoint(&upload_dir); let engine_version = wavedash_config.engine_version(); let entrypoint_params = match engine_kind { - Some(EngineKind::Godot | EngineKind::Unity) => { + Some(EngineKind::Godot | EngineKind::Unity | EngineKind::Defold) => { let engine_label = engine_kind.unwrap().as_label(); let html_path = html_entrypoint.as_deref().ok_or_else(|| { anyhow::anyhow!( @@ -121,7 +121,15 @@ pub async fn handle_dev(config_path: Option, verbose: bool) -> Result<( })?; let ver = engine_version .ok_or_else(|| anyhow::anyhow!("{} engine requires a version", engine_label))?; - Some(fetch_entrypoint_params(engine_label, ver, html_path).await?) + let html_relative_path = html_path + .strip_prefix(&upload_dir) + .unwrap_or(html_path) + .to_string_lossy() + .replace('\\', "/"); + Some( + fetch_entrypoint_params(engine_label, ver, html_path, Some(&html_relative_path)) + .await?, + ) } Some(EngineKind::JsDos | EngineKind::Ruffle | EngineKind::RenPy) => { wavedash_config.executable_entrypoint_params() diff --git a/src/init.rs b/src/init.rs index 6a5ba4a..c5e635c 100644 --- a/src/init.rs +++ b/src/init.rs @@ -39,6 +39,7 @@ struct GamesResponse { enum EngineType { Godot, Unity, + Defold, Custom, } @@ -47,6 +48,7 @@ impl EngineType { match self { EngineType::Godot => "build", EngineType::Unity => "build", + EngineType::Defold => "dist", EngineType::Custom => "dist", } } @@ -130,10 +132,25 @@ fn detect_unity(dir: &Path) -> Option { None } +/// Look for a Defold project marker. +fn detect_defold(dir: &Path) -> Option { + if dir.join("game.project").is_file() { + return Some(DetectedEngine { + engine_type: EngineType::Defold, + version_hint: None, + }); + } + + None +} + fn detect_engine(dir: &Path) -> DetectedEngine { if let Some(engine) = detect_godot(dir) { return engine; } + if let Some(engine) = detect_defold(dir) { + return engine; + } if let Some(engine) = detect_unity(dir) { return engine; } @@ -233,6 +250,10 @@ fn generate_toml( let version = engine_version.unwrap_or("2022.3"); toml.push_str(&format!("\n[unity]\nversion = \"{}\"\n", version)); } + EngineType::Defold => { + let version = engine_version.unwrap_or("1.12.4"); + toml.push_str(&format!("\n[defold]\nversion = \"{}\"\n", version)); + } EngineType::Custom => { toml.push_str("\nentrypoint = \"index.html\"\n"); } @@ -279,7 +300,7 @@ pub async fn handle_init() -> Result<()> { let current_dir = std::env::current_dir()?; let detected = detect_engine(¤t_dir); - // Only prompt for version when we detect Godot/Unity. + // Only prompt for version when we detect a first-class engine. // For web builds (threejs, phaser, custom, etc.) no engine config is needed. let engine_version: Option = match &detected.engine_type { EngineType::Godot => { @@ -302,6 +323,13 @@ pub async fn handle_init() -> Result<()> { Some(version) } } + EngineType::Defold => { + let version: String = cliclack::input("Defold version") + .placeholder("1.12.4") + .default_input("1.12.4") + .interact()?; + Some(version) + } EngineType::Custom => None, };