Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 30 additions & 4 deletions src/builds.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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('\\', "/");
Comment on lines +171 to +175
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

upload_dir is not canonicalized here, unlike dev/mod.rs:101. Both call sites compute html_relative_path the same way, but only handle_dev resolves symlinks/.//.. first. The unwrap_or(html_path) fallback means a strip-prefix failure silently sends the absolute filesystem path to the server as htmlPath — leaking the local layout and almost certainly producing a 4xx.

Easiest fix: canonicalize upload_dir here too (mirror the dev/mod.rs block), or replace unwrap_or with an explicit error so a mismatch becomes a clear bug rather than bad data.

Comment on lines +171 to +175
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

upload_dir is not canonicalized here (unlike dev/mod.rs:101), and the .unwrap_or(html_path) fallback silently leaks an absolute filesystem path on strip_prefix failure.

handle_dev resolves upload_dir via .canonicalize()? before walking it (src/dev/mod.rs:101). handle_build_push skips that step, so this call site walks a potentially-relative-or-symlinked path. If strip_prefix(&upload_dir) ever fails (mismatch in normalization between the path returned by WalkDir and upload_dir), unwrap_or(html_path) falls back to the full path — that absolute filesystem path gets sent to the server as htmlPath, leaking the local layout and almost certainly producing a 4xx.

Either canonicalize upload_dir first (mirror dev/mod.rs:101), or replace the fallback with an explicit error so a path mismatch surfaces as a clear bug:

let upload_dir = upload_dir.canonicalize().with_context(|| {
    format!("Failed to canonicalize upload_dir: {}", upload_dir.display())
})?;
// ...
let html_relative_path = html_path
    .strip_prefix(&upload_dir)
    .with_context(|| format!("HTML entrypoint {} is not under upload_dir {}", html_path.display(), upload_dir.display()))?
    .to_string_lossy()
    .replace('\\', "/");

Some(
fetch_entrypoint_params("DEFOLD", ver, html_path, Some(&html_relative_path))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Engine label hardcoded as "DEFOLD" instead of routed through EngineKind::Defold.as_label().

The parallel call in src/dev/mod.rs:130 correctly uses engine_kind.unwrap().as_label(). Two sources of truth for the same value can drift silently — e.g. if the wire format ever needs to change ("DEFOLD""defold" for normalization), dev and build push will diverge until both are updated.

Suggested change
fetch_entrypoint_params("DEFOLD", ver, html_path, Some(&html_relative_path))
Some(
fetch_entrypoint_params(EngineKind::Defold.as_label(), ver, html_path, Some(&html_relative_path))
.await?,
)

.await?,
)
Comment on lines +176 to +179
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Engine label hardcoded as "DEFOLD" instead of routed through EngineKind::Defold.as_label(). The parallel call in src/dev/mod.rs:133 correctly uses engine_kind.unwrap().as_label() — these two call sites can now drift silently.

Suggested change
Some(
fetch_entrypoint_params("DEFOLD", ver, html_path, Some(&html_relative_path))
.await?,
)
Some(
fetch_entrypoint_params(EngineKind::Defold.as_label(), ver, html_path, Some(&html_relative_path))
.await?,
)

}
Some(EngineKind::JsDos | EngineKind::Ruffle | EngineKind::RenPy) => {
wavedash_config.executable_entrypoint_params()
}
_ => None,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wildcard match suppresses compiler exhaustiveness check for future engines

Low Severity

The entrypoint_params match in builds.rs uses _ => None as a catch-all, while the equivalent match in dev/mod.rs uses explicit None => None making it exhaustive over all EngineKind variants. If a future engine variant is added to EngineKind, the compiler will force an update in dev/mod.rs but silently accept the wildcard in builds.rs, risking a missing entrypoint-params computation in the build-push flow — exactly the kind of per-engine handling this PR itself demonstrates with Defold.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 10f7bb0. Configure here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wildcard _ => None suppresses the compiler's exhaustiveness check for future engine variants.

dev/mod.rs:137 uses an explicit None => None, making the match exhaustive over every EngineKind variant. Here, adding a new engine to EngineKind will compile cleanly in builds.rs — silently routing the new engine through the _ => None arm — even when dev (correctly) forces the author to decide. That asymmetry is exactly the per-engine handling this PR demonstrates by wiring up Defold; the wildcard makes the next engine more likely to slip through build push.

Suggested change
_ => None,
Some(EngineKind::Godot | EngineKind::Unity) => None,
None => None,
};

(Or list each variant explicitly. Either way, drop the wildcard.)

};

// 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,
Expand Down
16 changes: 15 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -168,6 +173,9 @@ pub struct WavedashConfig {
#[serde(rename = "unity")]
pub unity: Option<UnitySection>,

#[serde(rename = "defold")]
pub defold: Option<DefoldSection>,

#[serde(rename = "jsdos")]
pub jsdos: Option<ExecutableEngineSection>,

Expand All @@ -182,6 +190,7 @@ pub struct WavedashConfig {
pub enum EngineKind {
Godot,
Unity,
Defold,
JsDos,
Ruffle,
RenPy,
Expand All @@ -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",
Expand Down Expand Up @@ -236,6 +246,7 @@ impl WavedashConfig {
let engines: Vec<EngineKind> = [
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),
Expand All @@ -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]"
),
}
}
Expand All @@ -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())
}

Expand Down
45 changes: 42 additions & 3 deletions src/dev/entrypoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PathBuf> {
let default_index = upload_dir.join("index.html");
if default_index.is_file() {
return Some(default_index);
}

let mut html_files: Vec<PathBuf> = Vec::new();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unchanged early-return on lines 17–21 short-circuits the new architecture-aware selection. If upload_dir/index.html exists for any reason — a stale leftover from a previous engine, a redirect a build tool placed at the root, a user-written launcher — it wins immediately, and wasm-web/index.html is never considered.

For a Defold project migrating onto wavedash, this is a real footgun: any pre-existing root index.html silently pre-empts the proper Defold entrypoint, and fetch_entrypoint_params parses the wrong HTML. Consider gating the early-return on engine kind, or only using it when no nested wasm-web/ / js-web/ directory is present.

for entry in WalkDir::new(upload_dir)
.min_depth(1)
.into_iter()
Expand All @@ -28,16 +32,50 @@ pub fn locate_html_entrypoint(upload_dir: &Path) -> Option<PathBuf> {
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();
Comment on lines +42 to +43
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Option<SystemTime> orders None < Some(_), so any file whose metadata().modified() fails (broken symlink, FS that doesn't expose mtime, permissions error during stat) becomes None and — because of the modified_b.cmp(&modified_a) direction on line 61 — sorts before files with valid mtimes. The file with broken metadata wins selection.

Consider mapping None to SystemTime::UNIX_EPOCH (or Reverse) so that "no metadata" sorts to the bottom rather than the top.

Comment on lines +42 to +43
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Option<SystemTime> orders None < Some(_), so a file whose metadata().modified() fails becomes "newest" under the current comparator.

.ok() produces Option<SystemTime>. If metadata() errors (broken symlink, FS without mtime support, transient EACCES during walk), modified_a is None. Option's Ord impl ranks None before Some(_), so modified_b.cmp(&modified_a) (descending) puts the None file at the top of the sort — it wins selection over every file with a valid mtime.

Map the failure to a far-past time so missing-mtime files sort to the bottom:

let modified_a = a.metadata().and_then(|m| m.modified()).unwrap_or(std::time::SystemTime::UNIX_EPOCH);
let modified_b = b.metadata().and_then(|m| m.modified()).unwrap_or(std::time::SystemTime::UNIX_EPOCH);

This also removes the Option wrapping so the Ord derivation behaves intuitively.

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
}
};
Comment on lines +54 to +62
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

architecture_score only matches when wasm-web/ / js-web/ is the FIRST path segment after upload_dir.

Defold's HTML5 bundle by default writes to build/default/<GameTitle>/{wasm-web,js-web}/.... If a user sets upload_dir = "build/default" (rather than build/default/<GameTitle>), the relative paths become e.g. MyGame/wasm-web/index.html and MyGame/js-web/index.html — both score 2, the architecture preference never kicks in, and the tiebreakers (mtime + alpha) decide arbitrarily.

Consider matching anywhere in the path, e.g. relative.contains("/wasm-web/") || relative.starts_with("wasm-web/").

Comment on lines +54 to +62
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

architecture_score only triggers when wasm-web/ / js-web/ is the FIRST path segment relative to upload_dir — Defold's natural bundle layout puts them under a game-name folder.

bob.jar HTML5 bundling writes to <output>/<GameTitle>/wasm-web/... and <output>/<GameTitle>/js-web/.... A user who sets upload_dir = "build/default" (rather than build/default/<GameTitle>) sees relative paths like MyGame/wasm-web/index.html — both architectures score 2, the preference never fires, and selection falls through to mtime + alpha (i.e. whichever bundler happened to write last, or alphabetic on game name).

Match anywhere in the path:

Suggested change
let architecture_score = |relative: &str| {
if relative.starts_with("wasm-web/") {
0
} else if relative.starts_with("js-web/") {
1
} else {
2
}
};
let architecture_score = |relative: &str| {
if relative.contains("/wasm-web/") || relative.starts_with("wasm-web/") {
0
} else if relative.contains("/js-web/") || 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))
Comment on lines +64 to +67
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sort key precedence likely inverted. Modification time is the primary key, so the wasm-web/ / js-web/ architecture preference (the whole point of architecture_score) only acts as a tiebreaker when two HTML files share an identical mtime.

For Defold, both wasm-web/index.html and js-web/index.html are written by the bundler at roughly the same time, but any subsequent activity (re-running just one target, cp -p, an IDE touching one of them, a file-stager re-copying) gives one a newer mtime than the other and that one wins — even if it's js-web/ and the score says wasm-web/ should be preferred.

If the intent is "prefer wasm-web, then js-web, then most-recently-modified," the comparator order should be flipped:

Suggested change
modified_b
.cmp(&modified_a)
.then_with(|| architecture_score(&relative_a).cmp(&architecture_score(&relative_b)))
.then_with(|| relative_a.cmp(&relative_b))
architecture_score(&relative_a)
.cmp(&architecture_score(&relative_b))
.then_with(|| modified_b.cmp(&modified_a))
.then_with(|| relative_a.cmp(&relative_b))
});

Comment on lines +64 to +67
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sort key precedence is inverted — architecture preference is effectively dead code.

modified_b.cmp(&modified_a) is the primary key; architecture_score only breaks ties when two HTML files share an identical SystemTime. In real bundle output, files written by the same bundler run differ by at least microseconds (and any subsequent cp -p, a partial rebuild of just one target, or an IDE touching one file changes mtime ordering deterministically). So js-web/index.html wins over wasm-web/index.html whenever it happens to be the newer of the two — even though the whole point of architecture_score is to prefer wasm-web.

Flip the comparator so architecture is primary and mtime is the tiebreaker:

Suggested change
modified_b
.cmp(&modified_a)
.then_with(|| architecture_score(&relative_a).cmp(&architecture_score(&relative_b)))
.then_with(|| relative_a.cmp(&relative_b))
architecture_score(&relative_a)
.cmp(&architecture_score(&relative_b))
.then_with(|| modified_b.cmp(&modified_a))
.then_with(|| relative_a.cmp(&relative_b))
});

(Combine with the Option<SystemTime> fix below — see separate comment on line 42–43.)

});
Comment on lines +41 to +68
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sort comparator does file IO on every comparison — O(N log N) stat() syscalls per sort, not O(N).

Each invocation of the closure calls a.metadata() and b.metadata(). Rust's sort_by calls the comparator O(N log N) times, so for N HTML files we issue ~2·N·log₂(N) stat syscalls (plus another lazy traversal building the relative strings each call). For a typical Defold dist this is small, but for any upload_dir that incidentally contains many HTML files (engine demos, generated docs, asset bundles with HTML readmes), this is noticeable I/O — and it duplicates work that has a stable per-file answer.

Pre-compute keys once with sort_by_cached_key:

html_files.sort_by_cached_key(|p| {
    let relative = p.strip_prefix(upload_dir).unwrap_or(p).to_string_lossy().replace('\\', "/");
    let architecture = architecture_score(&relative);
    let modified = p.metadata().and_then(|m| m.modified()).unwrap_or(std::time::SystemTime::UNIX_EPOCH);
    // Reverse mtime via Reverse() so newest sorts first
    (architecture, std::cmp::Reverse(modified), relative)
});

This also makes the comparator total/consistent if a file is unlinked mid-sort (the key is captured once at the start).


html_files.into_iter().next()
}

pub async fn fetch_entrypoint_params(engine: &str, engine_version: &str, html_path: &Path) -> Result<Value> {
pub async fn fetch_entrypoint_params(
engine: &str,
engine_version: &str,
html_path: &Path,
html_relative_path: Option<&str>,
) -> Result<Value> {
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")?;
Expand All @@ -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
Expand Down
14 changes: 11 additions & 3 deletions src/dev/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -111,7 +111,7 @@ pub async fn handle_dev(config_path: Option<PathBuf>, 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!(
Expand All @@ -121,7 +121,15 @@ pub async fn handle_dev(config_path: Option<PathBuf>, 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()
Expand Down
30 changes: 29 additions & 1 deletion src/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ struct GamesResponse {
enum EngineType {
Godot,
Unity,
Defold,
Custom,
}

Expand All @@ -47,6 +48,7 @@ impl EngineType {
match self {
EngineType::Godot => "build",
EngineType::Unity => "build",
EngineType::Defold => "dist",
EngineType::Custom => "dist",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"dist" is not a Defold convention. Defold's bob.jar HTML5 bundler writes to build/default/<GameTitle>/ by default — most Defold users won't have a dist/ directory at all. Accepting the default in wavedash init produces a wavedash.toml whose upload_dir doesn't exist, and the next wavedash dev / build push errors immediately.

Consider defaulting to build/default (or prompting to enter the bundle path).

}
}
Expand Down Expand Up @@ -130,10 +132,25 @@ fn detect_unity(dir: &Path) -> Option<DetectedEngine> {
None
}

/// Look for a Defold project marker.
fn detect_defold(dir: &Path) -> Option<DetectedEngine> {
if dir.join("game.project").is_file() {
return Some(DetectedEngine {
engine_type: EngineType::Defold,
version_hint: None,
});
}

None
}
Comment on lines +136 to +145
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

detect_defold has no version-extraction logic and no fallback heuristic. Compare with:

  • detect_godot (lines 64–108): parses config/features and config_version to populate version_hint.
  • detect_unity (lines 112–133): parses ProjectSettings/ProjectVersion.txt for the exact editor version AND falls back to detecting an Assets/ directory.

For Defold, version_hint is always None, so the prompt placeholder (line 325) is the hard-coded "1.0" — a value users may accept without thinking. Defold's game.project does carry a [bootstrap] section and tools like the .defignore / bob.jar runner could imply a version; even just defaulting to the latest known release would be safer than "1.0".


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;
}
Expand Down Expand Up @@ -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");
}
Expand Down Expand Up @@ -279,7 +300,7 @@ pub async fn handle_init() -> Result<()> {
let current_dir = std::env::current_dir()?;
let detected = detect_engine(&current_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<String> = match &detected.engine_type {
EngineType::Godot => {
Expand All @@ -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)
}
Comment on lines +326 to +332
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default Defold version "1.0" is not a real Defold release. Defold ships versions like 1.10.x / 1.9.x — Godot's default is 4.4, Unity's is 2022.3, both plausible releases on their actual streams. A user who hits Enter at this prompt ends up with [defold] version = "1.0" in wavedash.toml, which the server can't resolve to any runtime.

Also note generate_toml on line 254 has the same "1.0" fallback; both should be updated to a recent Defold release (e.g. 1.10.4 or whatever the current minimum-supported is).

EngineType::Custom => None,
};

Expand Down
Loading