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#include "util/i18n/tr.h"
3
4#include <filesystem>
5#include <fstream>
6#include <sstream>
7
8#include "absl/strings/match.h"
9#include "absl/strings/str_format.h"
10#include "absl/strings/str_split.h"
12#include "app/gui/core/icons.h"
14#include "core/project.h"
16#include "imgui/imgui.h"
17#include "util/file_util.h"
18#include "util/macro.h"
19#include "yaze_config.h"
20
21#ifdef __EMSCRIPTEN__
23#endif
24namespace yaze {
25namespace editor {
26
27namespace {
28
29#ifndef __EMSCRIPTEN__
30bool PathsReferToSameProjectFile(const std::string& left,
31 const std::string& right) {
32 if (left.empty() || right.empty()) {
33 return false;
34 }
35
36 std::error_code equivalent_error;
37 if (std::filesystem::equivalent(left, right, equivalent_error)) {
38 return true;
39 }
40
41 std::error_code left_error;
42 std::error_code right_error;
43 const auto left_path =
44 std::filesystem::absolute(left, left_error).lexically_normal();
45 const auto right_path =
46 std::filesystem::absolute(right, right_error).lexically_normal();
47 return !left_error && !right_error && left_path == right_path;
48}
49#endif
50
51#ifdef __EMSCRIPTEN__
52std::string ProjectStorageKey(const project::YazeProject* project,
53 const std::string& filepath) {
54 if (project != nullptr && project->filepath == filepath) {
55 return project->MakeStorageKey("project");
56 }
57 std::string key = std::filesystem::path(filepath).stem().string();
58 return key.empty() ? "project" : key;
59}
60#endif
61
62} // namespace
63
69
71 if (!active_)
72 return;
73
74 ImGui::SetNextWindowSize(ImVec2(900, 700), ImGuiCond_FirstUseEver);
75 if (!ImGui::Begin(absl::StrFormat("%s Project Editor###ProjectFileEditor",
77 .c_str(),
78 &active_)) {
79 ImGui::End();
80 return;
81 }
82
83 // Toolbar
84 if (ImGui::BeginTable("ProjectEditorToolbar", 10,
85 ImGuiTableFlags_SizingFixedFit)) {
86 ImGui::TableNextColumn();
87 if (ImGui::Button(absl::StrFormat("%s New", ICON_MD_NOTE_ADD).c_str())) {
88 auto status = NewFile();
89 if (!status.ok() && toast_manager_) {
90 toast_manager_->Show(std::string(status.message()), ToastType::kError);
91 }
92 }
93
94 ImGui::TableNextColumn();
95 if (ImGui::Button(
96 absl::StrFormat("%s Open", ICON_MD_FOLDER_OPEN).c_str())) {
98 if (!file.empty()) {
99 auto status = LoadFile(file);
100 if (!status.ok() && toast_manager_) {
102 std::string(status.message().data(), status.message().size()),
104 }
105 }
106 }
107
108 ImGui::TableNextColumn();
109 bool can_save = !filepath_.empty() && IsModified();
110 if (!can_save)
111 ImGui::BeginDisabled();
112 if (ImGui::Button(absl::StrFormat("%s Save", ICON_MD_SAVE).c_str())) {
113 auto status = SaveFile();
114 if (status.ok() && toast_manager_) {
115 toast_manager_->Show("Project file saved", ToastType::kSuccess);
116 } else if (!status.ok() && toast_manager_) {
118 std::string(status.message().data(), status.message().size()),
120 }
121 }
122 if (!can_save)
123 ImGui::EndDisabled();
124
125 ImGui::TableNextColumn();
126 if (ImGui::Button(absl::StrFormat("%s Save As", ICON_MD_SAVE_AS).c_str())) {
128 filepath_.empty() ? "project" : filepath_, "yaze");
129 if (!file.empty()) {
130 auto status = SaveFileAs(file);
131 if (status.ok() && toast_manager_) {
132 toast_manager_->Show("Project file saved", ToastType::kSuccess);
133 } else if (!status.ok() && toast_manager_) {
135 std::string(status.message().data(), status.message().size()),
137 }
138 }
139 }
140
141 ImGui::TableNextColumn();
142 ImGui::Text("|");
143
144 ImGui::TableNextColumn();
145 // Import ZScream Labels button
146 if (ImGui::Button(
147 absl::StrFormat("%s Import Labels", ICON_MD_LABEL).c_str())) {
148 auto status = ImportLabelsFromZScream();
149 if (status.ok() && toast_manager_) {
150 toast_manager_->Show("Labels imported successfully",
152 } else if (!status.ok() && toast_manager_) {
154 std::string(status.message().data(), status.message().size()),
156 }
157 }
158 if (ImGui::IsItemHovered()) {
159 ImGui::SetTooltip(tr("Import labels from ZScream DefaultNames.txt"));
160 }
161
162 ImGui::TableNextColumn();
163 if (ImGui::Button(
164 absl::StrFormat("%s Validate", ICON_MD_CHECK_CIRCLE).c_str())) {
166 show_validation_ = true;
167 }
168
169 ImGui::TableNextColumn();
170 ImGui::Checkbox(tr("Show Validation"), &show_validation_);
171
172 ImGui::TableNextColumn();
173 if (!filepath_.empty()) {
174 ImGui::TextDisabled("%s", filepath_.c_str());
175 } else {
176 ImGui::TextDisabled(tr("No file loaded"));
177 }
178
179 ImGui::EndTable();
180 }
181
182 ImGui::Separator();
183
184 // Validation errors panel
185 if (show_validation_ && !validation_errors_.empty()) {
186 gui::StyledChild errors_child("ValidationErrors", ImVec2(0, 100),
187 {.bg = ImVec4(0.3f, 0.2f, 0.2f, 0.5f)}, true);
188 if (errors_child) {
189 ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f),
190 tr("%s Validation Errors:"), ICON_MD_ERROR);
191 for (const auto& error : validation_errors_) {
192 ImGui::BulletText("%s", error.c_str());
193 }
194 }
195 }
196
197 // Main editor
198 ImVec2 editor_size = ImGui::GetContentRegionAvail();
199 text_editor_.Render("##ProjectEditor", editor_size);
201 modified_ = true;
202 }
203
204 ImGui::End();
205}
206
218
220 project::YazeProject* project) {
221 project_ = project;
222 filepath_ = state.filepath;
225 active_ = state.active;
226 modified_ = state.modified;
229}
230
232 project_ = project;
233 filepath_.clear();
235 initialized_ = false;
236 active_ = false;
237 modified_ = false;
238 show_validation_ = true;
239 validation_errors_.clear();
240}
241
242absl::Status ProjectFileEditor::LoadFile(const std::string& filepath) {
244#ifdef __EMSCRIPTEN__
245 const std::string key = ProjectStorageKey(project_, filepath);
246 auto storage_or = platform::WasmStorage::LoadProject(key);
247 if (storage_or.ok()) {
248 text_editor_.SetText(storage_or.value());
250 initialized_ = true;
251 modified_ = false;
253 return absl::OkStatus();
254 }
255#endif
256
257 std::ifstream file(filepath);
258 if (!file.is_open()) {
259 return absl::InvalidArgumentError(
260 absl::StrFormat("Cannot open file: %s", filepath));
261 }
262
263 std::stringstream buffer;
264 buffer << file.rdbuf();
265 file.close();
266
267 text_editor_.SetText(buffer.str());
269 initialized_ = true;
270 modified_ = false;
271
273
274 return absl::OkStatus();
275}
276
278 if (filepath_.empty()) {
279 return absl::InvalidArgumentError("No file path specified");
280 }
281
282 return SaveFileAs(filepath_);
283}
284
285absl::Status ProjectFileEditor::SaveFileAs(const std::string& filepath) {
286 // Ensure .yaze extension
287 std::string final_path = filepath;
288 if (!absl::EndsWith(final_path, ".yaze")) {
289 final_path += ".yaze";
290 }
291 const std::string contents = GetDocumentText();
292
294 RETURN_IF_ERROR(save_guard_callback_(final_path, contents));
295 }
296
297#ifdef __EMSCRIPTEN__
298 const std::string key = ProjectStorageKey(project_, final_path);
299 const bool replace_existing =
300 !filepath_.empty() && ProjectStorageKey(project_, filepath_) == key;
301 auto storage_status =
302 platform::WasmStorage::SaveProject(key, contents, replace_existing);
303 if (!storage_status.ok()) {
304 return storage_status;
305 }
307 RETURN_IF_ERROR(save_complete_callback_(final_path, contents));
308 }
309 filepath_ = final_path;
310 initialized_ = true;
311 modified_ = false;
312 auto& recent_mgr = project::RecentFilesManager::GetInstance();
313 recent_mgr.AddFile(filepath_);
314 recent_mgr.Save();
315 return absl::OkStatus();
316#else
317 const bool replace_existing =
318 PathsReferToSameProjectFile(filepath_, final_path);
320 replace_existing));
321
323 RETURN_IF_ERROR(save_complete_callback_(final_path, contents));
324 }
325
326 filepath_ = final_path;
327 initialized_ = true;
328 modified_ = false;
329
330 // Add to recent files
331 auto& recent_mgr = project::RecentFilesManager::GetInstance();
332 recent_mgr.AddFile(filepath_);
333 recent_mgr.Save();
334
335 return absl::OkStatus();
336#endif
337}
338
340 if (initialized_ && modified_) {
341 return absl::FailedPreconditionError(
342 "Project-file draft has unsaved changes; use Save or Save As first");
343 }
344 return absl::OkStatus();
345}
346
349 // Create a template project file
350 const std::string template_content = absl::StrFormat(
351 R"(# yaze Project File
352# Format Version: 2.0
353
354[project]
355name=New Project
356project_id=
357description=
358author=
359license=
360version=1.0
361created_date=
362last_modified=
363yaze_version=%s
364created_by=YAZE
365tags=
366
367[agent_settings]
368ai_provider=auto
369ai_model=
370ollama_host=http://localhost:11434
371use_custom_prompt=false
372
373[files]
374rom_filename=
375rom_backup_folder=backups
376code_folder=asm
377assets_folder=assets
378patches_folder=patches
379labels_filename=labels.txt
380symbols_filename=symbols.txt
381output_folder=build
382custom_objects_folder=
383# Optional: ASM integration (e.g. Oracle-of-Secrets hack_manifest.json)
384hack_manifest_file=
385additional_roms=
386
387[rom]
388role=dev
389expected_hash=
390write_policy=warn
391
392[feature_flags]
393# REMOVED: kLogInstructions - DisassemblyViewer is always active
394kSaveOverworldMaps=true
395kSaveOverworldEntrances=true
396kSaveOverworldExits=true
397kSaveOverworldItems=true
398kSaveOverworldProperties=true
399kSaveDungeonMaps=true
400kSaveDungeonObjects=true
401kSaveDungeonSprites=true
402kSaveDungeonRoomHeaders=true
403kSaveDungeonTorches=true
404kSaveDungeonPits=true
405kSaveDungeonBlocks=true
406kSaveDungeonCollision=true
407kSaveDungeonChests=true
408kSaveDungeonPotItems=true
409kSaveDungeonPalettes=true
410kSaveGraphicsSheet=true
411kSaveAllPalettes=true
412kSaveGfxGroups=true
413kSaveMessages=true
414kLoadCustomOverworld=false
415
416[workspace]
417font_global_scale=1.0
418autosave_enabled=true
419autosave_interval_secs=300
420backup_on_save=true
421backup_retention_count=20
422backup_keep_daily=true
423backup_keep_daily_days=14
424theme=dark
425
426[rom_addresses]
427# expanded_message_start=0x178000
428# expanded_message_end=0x17FFFF
429# expanded_music_hook=0x008919
430# expanded_music_main=0x1A9EF5
431# expanded_music_aux=0x1ACCA7
432# overworld_messages_expanded=0x1417F8
433# overworld_map16_expanded=0x1E8000
434# overworld_map32_tr_expanded=0x020000
435# overworld_map32_bl_expanded=0x1F0000
436# overworld_map32_br_expanded=0x1F8000
437# overworld_entrance_map_expanded=0x0DB55F
438# overworld_entrance_pos_expanded=0x0DB35F
439# overworld_entrance_id_expanded=0x0DB75F
440# overworld_entrance_flag_expanded=0x0DB895
441# overworld_ptr_marker_expanded=0x1423FF
442# overworld_ptr_magic_expanded=0xEA
443# overworld_gfx_ptr1=0x004F80
444# overworld_gfx_ptr2=0x00505F
445# overworld_gfx_ptr3=0x00513E
446
447[custom_objects]
448# object_0x31=track_LR.bin,track_UD.bin,track_corner_TL.bin
449# object_0x32=furnace.bin,firewood.bin,ice_chair.bin
450
451[build]
452build_script=
453output_folder=build
454build_target=
455asm_entry_point=asm/main.asm
456asm_sources=asm
457build_number=0
458last_build_hash=
459
460[music]
461persist_custom_music=true
462storage_key=
463last_saved_at=
464)",
466
467 text_editor_.SetText(template_content);
468 filepath_.clear();
469 initialized_ = true;
470 modified_ = true;
471 validation_errors_.clear();
472 return absl::OkStatus();
473}
474
476 // TODO: Implement custom syntax highlighting for INI format
477 // For now, use C language definition which provides some basic highlighting
478}
479
481 validation_errors_.clear();
482
483 std::string content = GetDocumentText();
484 std::vector<std::string> lines = absl::StrSplit(content, '\n');
485
486 std::string current_section;
487 int line_num = 0;
488
489 for (const auto& line : lines) {
490 line_num++;
491 std::string trimmed = std::string(absl::StripAsciiWhitespace(line));
492
493 // Skip empty lines and comments
494 if (trimmed.empty() || trimmed[0] == '#')
495 continue;
496
497 // Check for section headers
498 if (trimmed[0] == '[' && trimmed[trimmed.size() - 1] == ']') {
499 current_section = trimmed.substr(1, trimmed.size() - 2);
500
501 // Keep the legacy editor alias while sharing the parser's section list.
502 if (!project::IsKnownProjectFileSection(current_section) &&
503 current_section != "workspace_settings") {
504 validation_errors_.push_back(absl::StrFormat(
505 "Line %d: Unknown section [%s]", line_num, current_section));
506 }
507 continue;
508 }
509
510 // Check for key=value pairs
511 size_t equals_pos = trimmed.find('=');
512 if (equals_pos == std::string::npos) {
513 validation_errors_.push_back(absl::StrFormat(
514 "Line %d: Invalid format, expected key=value", line_num));
515 continue;
516 }
517 }
518
520 toast_manager_->Show("Project file validation passed", ToastType::kSuccess);
521 }
522}
523
524std::string ProjectFileEditor::GetDocumentText() const {
525 std::string text = text_editor_.GetText();
526 // TextEditor emits one synthetic trailing newline for its final line.
527 // Exclude that sentinel so capture/restore and load/save are byte-stable.
528 if (!text.empty() && text.back() == '\n') {
529 text.pop_back();
530 }
531 return text;
532}
533
535 if (validation_errors_.empty())
536 return;
537
538 ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), tr("Validation Errors:"));
539 for (const auto& error : validation_errors_) {
540 ImGui::BulletText("%s", error.c_str());
541 }
542}
543
545#ifdef __EMSCRIPTEN__
546 return absl::UnimplementedError(
547 "File-based label import is not supported in the web build");
548#else
549 if (!project_) {
550 return absl::FailedPreconditionError(
551 "No project loaded. Open a project first.");
552 }
553
554 // Show file dialog for DefaultNames.txt
556 if (file.empty()) {
557 return absl::CancelledError("No file selected");
558 }
559
560 // Read the file contents
561 std::ifstream input_file(file);
562 if (!input_file.is_open()) {
563 return absl::InvalidArgumentError(
564 absl::StrFormat("Cannot open file: %s", file));
565 }
566
567 std::stringstream buffer;
568 buffer << input_file.rdbuf();
569 input_file.close();
570
571 // Import using the project's method
572 auto status = project_->ImportLabelsFromZScreamContent(buffer.str());
573 if (!status.ok()) {
574 return status;
575 }
576
577 // Save the project to persist the imported labels
578 return project_->Save();
579#endif
580}
581
582} // namespace editor
583} // namespace yaze
void SetShowWhitespaces(bool aValue)
bool IsTextChanged() const
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.
ProjectFileEditorState CaptureState() const
bool IsModified() const
Get whether the file has unsaved changes.
void RestoreState(const ProjectFileEditorState &state, project::YazeProject *project)
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.
SaveCompleteCallback save_complete_callback_
absl::Status NewFile()
Create a new empty project file.
void ResetForProject(project::YazeProject *project)
const std::string & filepath() const
Get the current filepath.
void Show(const std::string &message, ToastType type=ToastType::kInfo, float ttl_seconds=3.0f, std::string action_label={}, std::function< void()> on_action=nullptr)
RAII guard for ImGui child windows with optional styling.
static RecentFilesManager & GetInstance()
Definition project.h:598
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
bool PathsReferToSameProjectFile(const std::string &left, const std::string &right)
const char * tr(const char *source)
Definition translator.cc:50
absl::Status WriteProjectFileAtomically(absl::string_view target_path, absl::string_view contents, bool replace_existing)
Definition project.cc:387
bool IsKnownProjectFileSection(absl::string_view name)
Definition project.cc:991
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
static const LanguageDefinition & C()
std::vector< std::string > validation_errors
Modern project structure with comprehensive settings consolidation.
Definition project.h:287
std::string MakeStorageKey(absl::string_view suffix) const
Definition project.cc:746
absl::Status ImportLabelsFromZScreamContent(const std::string &content)
Import labels from ZScream format content directly.
Definition project.cc:3048