feat(editor): zone audio panel scans Data dir for music + ambience files

The Zone Audio panel previously only offered five hardcoded preset
paths. Replaced with a recursive directory walk of <data>/Sound/
Music and <data>/Sound/Ambience for any .mp3/.wav/.ogg files.
Results cached after the first scan; a Refresh button forces a
rebuild for when the user drops new files in.

Two ImGui combos populate from the scan:
- "Music File"     — picks from Sound/Music
- "Ambience File"  — picks from Sound/Ambience

Each combo has a "(none)" option to clear. Selecting an entry
updates both the manifest field and the existing manual text input
buffer below, so users can fine-tune the path after picking from
the combo.

EditorApp gets a public getDataPath() so the UI can reach the
configured asset root without exposing the rest of the private
state.

Audio playback preview is the remaining piece — needs SDL_mixer
or similar wired into the editor; not in this commit.
This commit is contained in:
Kelsi 2026-05-07 12:43:58 -07:00
parent 0cb6a4c536
commit 7a624adada
2 changed files with 83 additions and 0 deletions

View file

@ -2932,6 +2932,86 @@ void EditorUI::renderPropertiesPanel(EditorApp& app) {
std::strncpy(ambDayBuf, manifest.ambienceDay.c_str(), sizeof(ambDayBuf) - 1);
std::strncpy(ambNightBuf, manifest.ambienceNight.c_str(), sizeof(ambNightBuf) - 1);
// Lazy-loaded scan of available music files. Walks
// <data>/Sound/Music recursively and grabs any .mp3/.wav/
// .ogg. Cached after first build; "Refresh" rebuilds the
// list. The dropdown then offers each scanned file as a
// selectable preset alongside the manual text input below.
static std::vector<std::string> musicScan;
static std::vector<std::string> ambienceScan;
static bool scanned = false;
auto rescan = [&]() {
musicScan.clear();
ambienceScan.clear();
namespace fs = std::filesystem;
std::string dp = app.getDataPath();
if (dp.empty()) dp = "Data";
std::string musicRoot = dp + "/Sound/Music";
std::string ambRoot = dp + "/Sound/Ambience";
auto walk = [](const std::string& root,
std::vector<std::string>& out) {
std::error_code ec;
if (!fs::exists(root)) return;
for (const auto& e : fs::recursive_directory_iterator(root, ec)) {
if (!e.is_regular_file()) continue;
std::string ext = e.path().extension().string();
std::transform(ext.begin(), ext.end(), ext.begin(),
[](unsigned char c) { return std::tolower(c); });
if (ext == ".mp3" || ext == ".wav" || ext == ".ogg") {
out.push_back(e.path().string());
}
}
std::sort(out.begin(), out.end());
};
walk(musicRoot, musicScan);
walk(ambRoot, ambienceScan);
scanned = true;
};
if (!scanned) rescan();
if (ImGui::SmallButton("Refresh##audioScan")) rescan();
ImGui::SameLine();
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1),
"%zu music / %zu ambience files",
musicScan.size(), ambienceScan.size());
// Music dropdown.
if (ImGui::BeginCombo("Music File##audio",
manifest.musicTrack.empty()
? "(none)"
: manifest.musicTrack.c_str())) {
if (ImGui::Selectable("(none)", manifest.musicTrack.empty())) {
manifest.musicTrack.clear();
musicBuf[0] = 0;
}
for (const auto& p : musicScan) {
bool sel = (p == manifest.musicTrack);
if (ImGui::Selectable(p.c_str(), sel)) {
manifest.musicTrack = p;
std::strncpy(musicBuf, p.c_str(), sizeof(musicBuf) - 1);
musicBuf[sizeof(musicBuf) - 1] = 0;
}
}
ImGui::EndCombo();
}
// Day-ambience dropdown.
if (ImGui::BeginCombo("Ambience File##audio",
manifest.ambienceDay.empty()
? "(none)"
: manifest.ambienceDay.c_str())) {
if (ImGui::Selectable("(none)", manifest.ambienceDay.empty())) {
manifest.ambienceDay.clear();
ambDayBuf[0] = 0;
}
for (const auto& p : ambienceScan) {
bool sel = (p == manifest.ambienceDay);
if (ImGui::Selectable(p.c_str(), sel)) {
manifest.ambienceDay = p;
std::strncpy(ambDayBuf, p.c_str(), sizeof(ambDayBuf) - 1);
ambDayBuf[sizeof(ambDayBuf) - 1] = 0;
}
}
ImGui::EndCombo();
}
if (ImGui::InputText("Music##audio", musicBuf, sizeof(musicBuf)))
manifest.musicTrack = musicBuf;
if (ImGui::InputText("Ambience (Day)##audio", ambDayBuf, sizeof(ambDayBuf)))