yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
project_file_editor.cc
Go to the documentation of this file.
2
3#include <filesystem>
4#include <fstream>
5#include <sstream>
6
7#include "absl/strings/match.h"
8#include "absl/strings/str_format.h"
9#include "absl/strings/str_split.h"
11#include "app/gui/core/icons.h"
13#include "core/project.h"
14#include "imgui/imgui.h"
15#include "util/file_util.h"
16#include "yaze_config.h"
17
18#ifdef __EMSCRIPTEN__
20#endif
21namespace yaze {
22namespace editor {
23
29
31 if (!active_)
32 return;
33
34 ImGui::SetNextWindowSize(ImVec2(900, 700), ImGuiCond_FirstUseEver);
35 if (!ImGui::Begin(absl::StrFormat("%s Project Editor###ProjectFileEditor",
37 .c_str(),
38 &active_)) {
39 ImGui::End();
40 return;
41 }
42
43 // Toolbar
44 if (ImGui::BeginTable("ProjectEditorToolbar", 10,
45 ImGuiTableFlags_SizingFixedFit)) {
46 ImGui::TableNextColumn();
47 if (ImGui::Button(absl::StrFormat("%s New", ICON_MD_NOTE_ADD).c_str())) {
48 NewFile();
49 }
50
51 ImGui::TableNextColumn();
52 if (ImGui::Button(
53 absl::StrFormat("%s Open", ICON_MD_FOLDER_OPEN).c_str())) {
55 if (!file.empty()) {
56 auto status = LoadFile(file);
57 if (!status.ok() && toast_manager_) {
59 std::string(status.message().data(), status.message().size()),
61 }
62 }
63 }
64
65 ImGui::TableNextColumn();
66 bool can_save = !filepath_.empty() && IsModified();
67 if (!can_save)
68 ImGui::BeginDisabled();
69 if (ImGui::Button(absl::StrFormat("%s Save", ICON_MD_SAVE).c_str())) {
70 auto status = SaveFile();
71 if (status.ok() && toast_manager_) {
72 toast_manager_->Show("Project file saved", ToastType::kSuccess);
73 } else if (!status.ok() && toast_manager_) {
75 std::string(status.message().data(), status.message().size()),
77 }
78 }
79 if (!can_save)
80 ImGui::EndDisabled();
81
82 ImGui::TableNextColumn();
83 if (ImGui::Button(absl::StrFormat("%s Save As", ICON_MD_SAVE_AS).c_str())) {
85 filepath_.empty() ? "project" : filepath_, "yaze");
86 if (!file.empty()) {
87 auto status = SaveFileAs(file);
88 if (status.ok() && toast_manager_) {
89 toast_manager_->Show("Project file saved", ToastType::kSuccess);
90 } else if (!status.ok() && toast_manager_) {
92 std::string(status.message().data(), status.message().size()),
94 }
95 }
96 }
97
98 ImGui::TableNextColumn();
99 ImGui::Text("|");
100
101 ImGui::TableNextColumn();
102 // Import ZScream Labels button
103 if (ImGui::Button(
104 absl::StrFormat("%s Import Labels", ICON_MD_LABEL).c_str())) {
105 auto status = ImportLabelsFromZScream();
106 if (status.ok() && toast_manager_) {
107 toast_manager_->Show("Labels imported successfully",
109 } else if (!status.ok() && toast_manager_) {
111 std::string(status.message().data(), status.message().size()),
113 }
114 }
115 if (ImGui::IsItemHovered()) {
116 ImGui::SetTooltip("Import labels from ZScream DefaultNames.txt");
117 }
118
119 ImGui::TableNextColumn();
120 if (ImGui::Button(
121 absl::StrFormat("%s Validate", ICON_MD_CHECK_CIRCLE).c_str())) {
123 show_validation_ = true;
124 }
125
126 ImGui::TableNextColumn();
127 ImGui::Checkbox("Show Validation", &show_validation_);
128
129 ImGui::TableNextColumn();
130 if (!filepath_.empty()) {
131 ImGui::TextDisabled("%s", filepath_.c_str());
132 } else {
133 ImGui::TextDisabled("No file loaded");
134 }
135
136 ImGui::EndTable();
137 }
138
139 ImGui::Separator();
140
141 // Validation errors panel
142 if (show_validation_ && !validation_errors_.empty()) {
143 gui::StyledChild errors_child(
144 "ValidationErrors", ImVec2(0, 100),
145 {.bg = ImVec4(0.3f, 0.2f, 0.2f, 0.5f)}, true);
146 if (errors_child) {
147 ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f),
148 "%s Validation Errors:", ICON_MD_ERROR);
149 for (const auto& error : validation_errors_) {
150 ImGui::BulletText("%s", error.c_str());
151 }
152 }
153 }
154
155 // Main editor
156 ImVec2 editor_size = ImGui::GetContentRegionAvail();
157 text_editor_.Render("##ProjectEditor", editor_size);
158
159 ImGui::End();
160}
161
162absl::Status ProjectFileEditor::LoadFile(const std::string& filepath) {
163#ifdef __EMSCRIPTEN__
164 std::string key = std::filesystem::path(filepath).stem().string();
165 if (key.empty()) {
166 key = "project";
167 }
168 auto storage_or = platform::WasmStorage::LoadProject(key);
169 if (storage_or.ok()) {
170 text_editor_.SetText(storage_or.value());
172 modified_ = false;
174 return absl::OkStatus();
175 }
176#endif
177
178 std::ifstream file(filepath);
179 if (!file.is_open()) {
180 return absl::InvalidArgumentError(
181 absl::StrFormat("Cannot open file: %s", filepath));
182 }
183
184 std::stringstream buffer;
185 buffer << file.rdbuf();
186 file.close();
187
188 text_editor_.SetText(buffer.str());
190 modified_ = false;
191
193
194 return absl::OkStatus();
195}
196
198 if (filepath_.empty()) {
199 return absl::InvalidArgumentError("No file path specified");
200 }
201
202 return SaveFileAs(filepath_);
203}
204
205absl::Status ProjectFileEditor::SaveFileAs(const std::string& filepath) {
206 // Ensure .yaze extension
207 std::string final_path = filepath;
208 if (!absl::EndsWith(final_path, ".yaze")) {
209 final_path += ".yaze";
210 }
211
212#ifdef __EMSCRIPTEN__
213 std::string key = std::filesystem::path(final_path).stem().string();
214 if (key.empty()) {
215 key = "project";
216 }
217 auto storage_status =
218 platform::WasmStorage::SaveProject(key, text_editor_.GetText());
219 if (!storage_status.ok()) {
220 return storage_status;
221 }
222 filepath_ = final_path;
223 modified_ = false;
224 auto& recent_mgr = project::RecentFilesManager::GetInstance();
225 recent_mgr.AddFile(filepath_);
226 recent_mgr.Save();
227 return absl::OkStatus();
228#else
229 std::ofstream file(final_path);
230 if (!file.is_open()) {
231 return absl::InvalidArgumentError(
232 absl::StrFormat("Cannot create file: %s", final_path));
233 }
234
235 file << text_editor_.GetText();
236 file.close();
237
238 filepath_ = final_path;
239 modified_ = false;
240
241 // Add to recent files
242 auto& recent_mgr = project::RecentFilesManager::GetInstance();
243 recent_mgr.AddFile(filepath_);
244 recent_mgr.Save();
245
246 return absl::OkStatus();
247#endif
248}
249
251 // Create a template project file
252 const std::string template_content = absl::StrFormat(
253 R"(# yaze Project File
254# Format Version: 2.0
255
256[project]
257name=New Project
258project_id=
259description=
260author=
261license=
262version=1.0
263created_date=
264last_modified=
265yaze_version=%s
266created_by=YAZE
267tags=
268
269[agent_settings]
270ai_provider=auto
271ai_model=
272ollama_host=http://localhost:11434
273use_custom_prompt=false
274
275[files]
276rom_filename=
277rom_backup_folder=backups
278code_folder=asm
279assets_folder=assets
280patches_folder=patches
281labels_filename=labels.txt
282symbols_filename=symbols.txt
283output_folder=build
284custom_objects_folder=
285# Optional: ASM integration (e.g. Oracle-of-Secrets hack_manifest.json)
286hack_manifest_file=
287additional_roms=
288
289[rom]
290role=dev
291expected_hash=
292write_policy=warn
293
294[feature_flags]
295# REMOVED: kLogInstructions - DisassemblyViewer is always active
296kSaveOverworldMaps=true
297kSaveOverworldEntrances=true
298kSaveOverworldExits=true
299kSaveOverworldItems=true
300kSaveOverworldProperties=true
301kSaveDungeonMaps=true
302kSaveDungeonObjects=true
303kSaveDungeonSprites=true
304kSaveDungeonRoomHeaders=true
305kSaveDungeonTorches=true
306kSaveDungeonPits=true
307kSaveDungeonBlocks=true
308kSaveDungeonCollision=true
309kSaveDungeonChests=true
310kSaveDungeonPotItems=true
311kSaveDungeonPalettes=true
312kSaveGraphicsSheet=true
313kSaveAllPalettes=true
314kSaveGfxGroups=true
315kSaveMessages=true
316kLoadCustomOverworld=false
317
318[workspace]
319font_global_scale=1.0
320autosave_enabled=true
321autosave_interval_secs=300
322backup_on_save=true
323backup_retention_count=20
324backup_keep_daily=true
325backup_keep_daily_days=14
326theme=dark
327
328[rom_addresses]
329# expanded_message_start=0x178000
330# expanded_message_end=0x17FFFF
331# expanded_music_hook=0x008919
332# expanded_music_main=0x1A9EF5
333# expanded_music_aux=0x1ACCA7
334# overworld_messages_expanded=0x1417F8
335# overworld_map16_expanded=0x1E8000
336# overworld_map32_tr_expanded=0x020000
337# overworld_map32_bl_expanded=0x1F0000
338# overworld_map32_br_expanded=0x1F8000
339# overworld_entrance_map_expanded=0x0DB55F
340# overworld_entrance_pos_expanded=0x0DB35F
341# overworld_entrance_id_expanded=0x0DB75F
342# overworld_entrance_flag_expanded=0x0DB895
343# overworld_ptr_marker_expanded=0x1423FF
344# overworld_ptr_magic_expanded=0xEA
345# overworld_gfx_ptr1=0x004F80
346# overworld_gfx_ptr2=0x00505F
347# overworld_gfx_ptr3=0x00513E
348
349[custom_objects]
350# object_0x31=track_LR.bin,track_UD.bin,track_corner_TL.bin
351# object_0x32=furnace.bin,firewood.bin,ice_chair.bin
352
353[build]
354build_script=
355output_folder=build
356build_target=
357asm_entry_point=asm/main.asm
358asm_sources=asm
359build_number=0
360last_build_hash=
361
362[music]
363persist_custom_music=true
364storage_key=
365last_saved_at=
366)",
368
369 text_editor_.SetText(template_content);
370 filepath_.clear();
371 modified_ = true;
372 validation_errors_.clear();
373}
374
376 // TODO: Implement custom syntax highlighting for INI format
377 // For now, use C language definition which provides some basic highlighting
378}
379
381 validation_errors_.clear();
382
383 std::string content = text_editor_.GetText();
384 std::vector<std::string> lines = absl::StrSplit(content, '\n');
385
386 std::string current_section;
387 int line_num = 0;
388
389 for (const auto& line : lines) {
390 line_num++;
391 std::string trimmed = std::string(absl::StripAsciiWhitespace(line));
392
393 // Skip empty lines and comments
394 if (trimmed.empty() || trimmed[0] == '#')
395 continue;
396
397 // Check for section headers
398 if (trimmed[0] == '[' && trimmed[trimmed.size() - 1] == ']') {
399 current_section = trimmed.substr(1, trimmed.size() - 2);
400
401 // Validate known sections
402 if (current_section != "project" && current_section != "files" &&
403 current_section != "feature_flags" &&
404 current_section != "workspace" &&
405 current_section != "workspace_settings" && current_section != "rom" &&
406 current_section != "rom_addresses" &&
407 current_section != "custom_objects" &&
408 current_section != "dungeon_overlay" && current_section != "build" &&
409 current_section != "agent_settings" && current_section != "music" &&
410 current_section != "keybindings" &&
411 current_section != "editor_visibility") {
412 validation_errors_.push_back(absl::StrFormat(
413 "Line %d: Unknown section [%s]", line_num, current_section));
414 }
415 continue;
416 }
417
418 // Check for key=value pairs
419 size_t equals_pos = trimmed.find('=');
420 if (equals_pos == std::string::npos) {
421 validation_errors_.push_back(absl::StrFormat(
422 "Line %d: Invalid format, expected key=value", line_num));
423 continue;
424 }
425 }
426
428 toast_manager_->Show("Project file validation passed", ToastType::kSuccess);
429 }
430}
431
433 if (validation_errors_.empty())
434 return;
435
436 ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Validation Errors:");
437 for (const auto& error : validation_errors_) {
438 ImGui::BulletText("%s", error.c_str());
439 }
440}
441
443#ifdef __EMSCRIPTEN__
444 return absl::UnimplementedError(
445 "File-based label import is not supported in the web build");
446#else
447 if (!project_) {
448 return absl::FailedPreconditionError(
449 "No project loaded. Open a project first.");
450 }
451
452 // Show file dialog for DefaultNames.txt
454 if (file.empty()) {
455 return absl::CancelledError("No file selected");
456 }
457
458 // Read the file contents
459 std::ifstream input_file(file);
460 if (!input_file.is_open()) {
461 return absl::InvalidArgumentError(
462 absl::StrFormat("Cannot open file: %s", file));
463 }
464
465 std::stringstream buffer;
466 buffer << input_file.rdbuf();
467 input_file.close();
468
469 // Import using the project's method
470 auto status = project_->ImportLabelsFromZScreamContent(buffer.str());
471 if (!status.ok()) {
472 return status;
473 }
474
475 // Save the project to persist the imported labels
476 return project_->Save();
477#endif
478}
479
480} // namespace editor
481} // namespace yaze
void SetShowWhitespaces(bool aValue)
std::string GetText() const
void Render(const char *aTitle, const ImVec2 &aSize=ImVec2(), bool aBorder=false)
void SetText(const std::string &aText)
void SetTabSize(int aValue)
void SetLanguageDefinition(const LanguageDefinition &aLanguageDef)
absl::Status SaveFileAs(const std::string &filepath)
Save to a new file path.
absl::Status LoadFile(const std::string &filepath)
Load a project file into the editor.
void NewFile()
Create a new empty project file.
bool IsModified() const
Get whether the file has unsaved changes.
std::vector< std::string > validation_errors_
absl::Status SaveFile()
Save the current editor contents to disk.
absl::Status ImportLabelsFromZScream()
Import labels from a ZScream DefaultNames.txt file.
const std::string & filepath() const
Get the current filepath.
void Show(const std::string &message, ToastType type=ToastType::kInfo, float ttl_seconds=3.0f)
RAII guard for ImGui child windows with optional styling.
static RecentFilesManager & GetInstance()
Definition project.h:374
static std::string ShowSaveFileDialog(const std::string &default_name="", const std::string &default_extension="")
ShowSaveFileDialog opens a save file dialog and returns the selected filepath. Uses global feature fl...
static std::string ShowOpenFileDialog()
ShowOpenFileDialog opens a file dialog and returns the selected filepath. Uses global feature flag to...
#define YAZE_VERSION_STRING
#define ICON_MD_FOLDER_OPEN
Definition icons.h:813
#define ICON_MD_NOTE_ADD
Definition icons.h:1330
#define ICON_MD_SAVE_AS
Definition icons.h:1646
#define ICON_MD_LABEL
Definition icons.h:1053
#define ICON_MD_ERROR
Definition icons.h:686
#define ICON_MD_CHECK_CIRCLE
Definition icons.h:400
#define ICON_MD_DESCRIPTION
Definition icons.h:539
#define ICON_MD_SAVE
Definition icons.h:1644
static const LanguageDefinition & C()
absl::Status ImportLabelsFromZScreamContent(const std::string &content)
Import labels from ZScream format content directly.
Definition project.cc:2074