yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
project.cc
Go to the documentation of this file.
1#include "core/project.h"
2
3#include <chrono>
4#include <cctype>
5#include <filesystem>
6#include <fstream>
7#include <iomanip>
8#include <sstream>
9
10#include "absl/strings/str_format.h"
11#include "absl/strings/str_join.h"
12#include "absl/strings/str_split.h"
13#include "app/gui/core/icons.h"
14#include "imgui/imgui.h"
15#include "util/file_util.h"
16#include "util/json.h"
17#include "util/log.h"
18#include "util/platform_paths.h"
19#include "util/macro.h"
20#include "yaze_config.h"
22
23
24#ifdef __EMSCRIPTEN__
26#endif
27
28// #ifdef YAZE_ENABLE_JSON_PROJECT_FORMAT
29// #include "nlohmann/json.hpp"
30// using json = nlohmann::json;
31// #endif
32
33namespace yaze {
34namespace project {
35
36namespace {
37// Helper functions for parsing key-value pairs
38std::pair<std::string, std::string> ParseKeyValue(const std::string& line) {
39 size_t eq_pos = line.find('=');
40 if (eq_pos == std::string::npos)
41 return {"", ""};
42
43 std::string key = line.substr(0, eq_pos);
44 std::string value = line.substr(eq_pos + 1);
45
46 // Trim whitespace
47 key.erase(0, key.find_first_not_of(" \t"));
48 key.erase(key.find_last_not_of(" \t") + 1);
49 value.erase(0, value.find_first_not_of(" \t"));
50 value.erase(value.find_last_not_of(" \t") + 1);
51
52 return {key, value};
53}
54
55bool ParseBool(const std::string& value) {
56 return value == "true" || value == "1" || value == "yes";
57}
58
59float ParseFloat(const std::string& value) {
60 try {
61 return std::stof(value);
62 } catch (...) {
63 return 0.0f;
64 }
65}
66
67std::vector<std::string> ParseStringList(const std::string& value) {
68 std::vector<std::string> result;
69 if (value.empty())
70 return result;
71
72 std::vector<std::string> parts = absl::StrSplit(value, ',');
73 for (const auto& part : parts) {
74 std::string trimmed = std::string(part);
75 trimmed.erase(0, trimmed.find_first_not_of(" \t"));
76 trimmed.erase(trimmed.find_last_not_of(" \t") + 1);
77 if (!trimmed.empty()) {
78 result.push_back(trimmed);
79 }
80 }
81 return result;
82}
83
84std::string SanitizeStorageKey(absl::string_view input) {
85 std::string key(input);
86 for (char& c : key) {
87 if (!std::isalnum(static_cast<unsigned char>(c))) {
88 c = '_';
89 }
90 }
91 if (key.empty()) {
92 key = "project";
93 }
94 return key;
95}
96} // namespace
97
98// YazeProject Implementation
99absl::Status YazeProject::Create(const std::string& project_name,
100 const std::string& base_path) {
101 name = project_name;
102 filepath = base_path + "/" + project_name + ".yaze";
103
104 // Initialize metadata
105 auto now = std::chrono::system_clock::now();
106 auto time_t = std::chrono::system_clock::to_time_t(now);
107 std::stringstream ss;
108 ss << std::put_time(std::localtime(&time_t), "%Y-%m-%d %H:%M:%S");
109
110 metadata.created_date = ss.str();
111 metadata.last_modified = ss.str();
112 metadata.yaze_version = "0.3.2"; // TODO: Get from version header
113 metadata.version = "2.0";
114 metadata.created_by = "YAZE";
116
118
119#ifndef __EMSCRIPTEN__
120 // Create project directory structure
121 std::filesystem::path project_dir(base_path + "/" + project_name);
122 std::filesystem::create_directories(project_dir);
123 std::filesystem::create_directories(project_dir / "code");
124 std::filesystem::create_directories(project_dir / "assets");
125 std::filesystem::create_directories(project_dir / "patches");
126 std::filesystem::create_directories(project_dir / "backups");
127 std::filesystem::create_directories(project_dir / "output");
128
129 // Set folder paths
130 code_folder = (project_dir / "code").string();
131 assets_folder = (project_dir / "assets").string();
132 patches_folder = (project_dir / "patches").string();
133 rom_backup_folder = (project_dir / "backups").string();
134 output_folder = (project_dir / "output").string();
135 labels_filename = (project_dir / "labels.txt").string();
136 symbols_filename = (project_dir / "symbols.txt").string();
137#else
138 // WASM: keep paths relative; persistence handled by WasmStorage/IDBFS
139 code_folder = "code";
140 assets_folder = "assets";
141 patches_folder = "patches";
142 rom_backup_folder = "backups";
143 output_folder = "output";
144 labels_filename = "labels.txt";
145 symbols_filename = "symbols.txt";
146#endif
147
148 return Save();
149}
150
151absl::Status YazeProject::Open(const std::string& project_path) {
152 filepath = project_path;
153
154#ifdef __EMSCRIPTEN__
155 // Prefer persistent storage in WASM builds
156 auto storage_key = MakeStorageKey("project");
157 auto storage_or = platform::WasmStorage::LoadProject(storage_key);
158 if (storage_or.ok()) {
159 return ParseFromString(storage_or.value());
160 }
161#endif
162
163 // Determine format and load accordingly
164 if (project_path.ends_with(".yaze")) {
166
167 // Try to detect if it's JSON format by peeking at first character
168 std::ifstream file(project_path);
169 if (file.is_open()) {
170 std::stringstream buffer;
171 buffer << file.rdbuf();
172 std::string content = buffer.str();
173
174#ifdef YAZE_ENABLE_JSON_PROJECT_FORMAT
175 if (!content.empty() && content.front() == '{') {
176 LOG_DEBUG("Project", "Detected JSON format project file");
177 return LoadFromJsonFormat(project_path);
178 }
179#endif
180
181 return ParseFromString(content);
182 }
183
184 return absl::InvalidArgumentError(
185 absl::StrFormat("Cannot open project file: %s", project_path));
186 } else if (project_path.ends_with(".zsproj")) {
188 return ImportFromZScreamFormat(project_path);
189 }
190
191 return absl::InvalidArgumentError("Unsupported project file format");
192}
193
194absl::Status YazeProject::Save() {
195 return SaveToYazeFormat();
196}
197
198absl::Status YazeProject::SaveAs(const std::string& new_path) {
199 std::string old_filepath = filepath;
200 filepath = new_path;
201
202 auto status = Save();
203 if (!status.ok()) {
204 filepath = old_filepath; // Restore on failure
205 }
206
207 return status;
208}
209
210std::string YazeProject::MakeStorageKey(absl::string_view suffix) const {
211 std::string base;
212 if (!metadata.project_id.empty()) {
213 base = metadata.project_id;
214 } else if (!name.empty()) {
215 base = name;
216 } else if (!filepath.empty()) {
217 base = std::filesystem::path(filepath).stem().string();
218 }
219 base = SanitizeStorageKey(base);
220 if (suffix.empty()) {
221 return base;
222 }
223 return absl::StrFormat("%s_%s", base, suffix);
224}
225
226absl::StatusOr<std::string> YazeProject::SerializeToString() const {
227 std::ostringstream file;
228
229 // Write header comment
230 file << "# yaze Project File\n";
231 file << "# Format Version: 2.0\n";
232 file << "# Generated by YAZE " << metadata.yaze_version << "\n";
233 file << "# Last Modified: " << metadata.last_modified << "\n\n";
234
235 // Project section
236 file << "[project]\n";
237 file << "name=" << name << "\n";
238 file << "description=" << metadata.description << "\n";
239 file << "author=" << metadata.author << "\n";
240 file << "license=" << metadata.license << "\n";
241 file << "version=" << metadata.version << "\n";
242 file << "created_date=" << metadata.created_date << "\n";
243 file << "last_modified=" << metadata.last_modified << "\n";
244 file << "yaze_version=" << metadata.yaze_version << "\n";
245 file << "created_by=" << metadata.created_by << "\n";
246 file << "project_id=" << metadata.project_id << "\n";
247 file << "tags=" << absl::StrJoin(metadata.tags, ",") << "\n\n";
248
249 // Files section
250 file << "[files]\n";
251 file << "rom_filename=" << GetRelativePath(rom_filename) << "\n";
252 file << "rom_backup_folder=" << GetRelativePath(rom_backup_folder) << "\n";
253 file << "code_folder=" << GetRelativePath(code_folder) << "\n";
254 file << "assets_folder=" << GetRelativePath(assets_folder) << "\n";
255 file << "patches_folder=" << GetRelativePath(patches_folder) << "\n";
256 file << "labels_filename=" << GetRelativePath(labels_filename) << "\n";
257 file << "symbols_filename=" << GetRelativePath(symbols_filename) << "\n";
258 file << "output_folder=" << GetRelativePath(output_folder) << "\n";
259 file << "custom_objects_folder=" << GetRelativePath(custom_objects_folder) << "\n";
260 file << "additional_roms=" << absl::StrJoin(additional_roms, ",") << "\n\n";
261
262 // Feature flags section
263 file << "[feature_flags]\n";
264 file << "load_custom_overworld="
265 << (feature_flags.overworld.kLoadCustomOverworld ? "true" : "false")
266 << "\n";
267 file << "apply_zs_custom_overworld_asm="
269 : "false")
270 << "\n";
271 file << "save_dungeon_maps="
272 << (feature_flags.kSaveDungeonMaps ? "true" : "false") << "\n";
273 file << "save_graphics_sheet="
274 << (feature_flags.kSaveGraphicsSheet ? "true" : "false") << "\n";
275 file << "enable_custom_objects="
276 << (feature_flags.kEnableCustomObjects ? "true" : "false") << "\n\n";
277
278 // Workspace settings section
279 file << "[workspace]\n";
280 file << "font_global_scale=" << workspace_settings.font_global_scale << "\n";
281 file << "dark_mode="
282 << (workspace_settings.dark_mode ? "true" : "false") << "\n";
283 file << "ui_theme=" << workspace_settings.ui_theme << "\n";
284 file << "autosave_enabled="
285 << (workspace_settings.autosave_enabled ? "true" : "false") << "\n";
286 file << "autosave_interval_secs="
288 file << "backup_on_save="
289 << (workspace_settings.backup_on_save ? "true" : "false") << "\n";
290 file << "show_grid="
291 << (workspace_settings.show_grid ? "true" : "false") << "\n";
292 file << "show_collision="
293 << (workspace_settings.show_collision ? "true" : "false") << "\n";
294 file << "prefer_hmagic_names="
295 << (workspace_settings.prefer_hmagic_names ? "true" : "false") << "\n";
296 file << "last_layout_preset=" << workspace_settings.last_layout_preset
297 << "\n";
298 file << "saved_layouts="
299 << absl::StrJoin(workspace_settings.saved_layouts, ",") << "\n";
300 file << "recent_files="
301 << absl::StrJoin(workspace_settings.recent_files, ",") << "\n\n";
302
303 // AI Agent settings section
304 file << "[agent_settings]\n";
305 file << "ai_provider=" << agent_settings.ai_provider << "\n";
306 file << "ai_model=" << agent_settings.ai_model << "\n";
307 file << "ollama_host=" << agent_settings.ollama_host << "\n";
308 file << "gemini_api_key=" << agent_settings.gemini_api_key << "\n";
309 file << "custom_system_prompt="
311 file << "use_custom_prompt="
312 << (agent_settings.use_custom_prompt ? "true" : "false") << "\n";
313 file << "show_reasoning="
314 << (agent_settings.show_reasoning ? "true" : "false") << "\n";
315 file << "verbose=" << (agent_settings.verbose ? "true" : "false") << "\n";
316 file << "max_tool_iterations=" << agent_settings.max_tool_iterations << "\n";
317 file << "max_retry_attempts=" << agent_settings.max_retry_attempts << "\n";
318 file << "temperature=" << agent_settings.temperature << "\n";
319 file << "top_p=" << agent_settings.top_p << "\n";
320 file << "max_output_tokens=" << agent_settings.max_output_tokens << "\n";
321 file << "stream_responses="
322 << (agent_settings.stream_responses ? "true" : "false") << "\n";
323 file << "favorite_models="
324 << absl::StrJoin(agent_settings.favorite_models, ",") << "\n";
325 file << "model_chain="
326 << absl::StrJoin(agent_settings.model_chain, ",") << "\n";
327 file << "chain_mode=" << agent_settings.chain_mode << "\n";
328 file << "enable_tool_resources="
329 << (agent_settings.enable_tool_resources ? "true" : "false") << "\n";
330 file << "enable_tool_dungeon="
331 << (agent_settings.enable_tool_dungeon ? "true" : "false") << "\n";
332 file << "enable_tool_overworld="
333 << (agent_settings.enable_tool_overworld ? "true" : "false") << "\n";
334 file << "enable_tool_messages="
335 << (agent_settings.enable_tool_messages ? "true" : "false") << "\n";
336 file << "enable_tool_dialogue="
337 << (agent_settings.enable_tool_dialogue ? "true" : "false") << "\n";
338 file << "enable_tool_gui="
339 << (agent_settings.enable_tool_gui ? "true" : "false") << "\n";
340 file << "enable_tool_music="
341 << (agent_settings.enable_tool_music ? "true" : "false") << "\n";
342 file << "enable_tool_sprite="
343 << (agent_settings.enable_tool_sprite ? "true" : "false") << "\n";
344 file << "enable_tool_emulator="
345 << (agent_settings.enable_tool_emulator ? "true" : "false") << "\n";
346 file << "builder_blueprint_path=" << agent_settings.builder_blueprint_path
347 << "\n\n";
348
349 // Custom keybindings section
351 file << "[keybindings]\n";
352 for (const auto& [key, value] : workspace_settings.custom_keybindings) {
353 file << key << "=" << value << "\n";
354 }
355 file << "\n";
356 }
357
358 // Editor visibility section
360 file << "[editor_visibility]\n";
361 for (const auto& [key, value] : workspace_settings.editor_visibility) {
362 file << key << "=" << (value ? "true" : "false") << "\n";
363 }
364 file << "\n";
365 }
366
367 // Resource labels sections
368 for (const auto& [type, labels] : resource_labels) {
369 if (!labels.empty()) {
370 file << "[labels_" << type << "]\n";
371 for (const auto& [key, value] : labels) {
372 file << key << "=" << value << "\n";
373 }
374 file << "\n";
375 }
376 }
377
378 // Build settings section
379 file << "[build]\n";
380 file << "build_script=" << build_script << "\n";
381 file << "output_folder=" << GetRelativePath(output_folder) << "\n";
382 file << "git_repository=" << git_repository << "\n";
383 file << "track_changes=" << (track_changes ? "true" : "false") << "\n";
384 file << "build_configurations="
385 << absl::StrJoin(build_configurations, ",") << "\n";
386 file << "build_target=" << build_target << "\n";
387 file << "asm_entry_point=" << asm_entry_point << "\n";
388 file << "asm_sources=" << absl::StrJoin(asm_sources, ",") << "\n";
389 file << "last_build_hash=" << last_build_hash << "\n";
390 file << "build_number=" << build_number << "\n\n";
391
392 // Music persistence section (for WASM/offline state)
393 file << "[music]\n";
394 file << "persist_custom_music="
395 << (music_persistence.persist_custom_music ? "true" : "false") << "\n";
396 file << "storage_key=" << music_persistence.storage_key << "\n";
397 file << "last_saved_at=" << music_persistence.last_saved_at << "\n\n";
398
399 // ZScream compatibility section
400 if (!zscream_project_file.empty()) {
401 file << "[zscream_compatibility]\n";
402 file << "original_project_file=" << zscream_project_file << "\n";
403 for (const auto& [key, value] : zscream_mappings) {
404 file << key << "=" << value << "\n";
405 }
406 file << "\n";
407 }
408
409 file << "# End of YAZE Project File\n";
410 return file.str();
411}
412
413absl::Status YazeProject::ParseFromString(const std::string& content) {
414 std::istringstream stream(content);
415 std::string line;
416 std::string current_section;
417
418 while (std::getline(stream, line)) {
419 if (line.empty() || line[0] == '#')
420 continue;
421
422 if (line.front() == '[' && line.back() == ']') {
423 current_section = line.substr(1, line.length() - 2);
424 continue;
425 }
426
427 auto [key, value] = ParseKeyValue(line);
428 if (key.empty())
429 continue;
430
431 if (current_section == "project") {
432 if (key == "name")
433 name = value;
434 else if (key == "description")
435 metadata.description = value;
436 else if (key == "author")
437 metadata.author = value;
438 else if (key == "license")
439 metadata.license = value;
440 else if (key == "version")
441 metadata.version = value;
442 else if (key == "created_date")
443 metadata.created_date = value;
444 else if (key == "last_modified")
445 metadata.last_modified = value;
446 else if (key == "yaze_version")
447 metadata.yaze_version = value;
448 else if (key == "created_by")
449 metadata.created_by = value;
450 else if (key == "tags")
451 metadata.tags = ParseStringList(value);
452 else if (key == "project_id")
453 metadata.project_id = value;
454 } else if (current_section == "files") {
455 if (key == "rom_filename")
456 rom_filename = value;
457 else if (key == "rom_backup_folder")
458 rom_backup_folder = value;
459 else if (key == "code_folder")
460 code_folder = value;
461 else if (key == "assets_folder")
462 assets_folder = value;
463 else if (key == "patches_folder")
464 patches_folder = value;
465 else if (key == "labels_filename")
466 labels_filename = value;
467 else if (key == "symbols_filename")
468 symbols_filename = value;
469 else if (key == "output_folder")
470 output_folder = value;
471 else if (key == "custom_objects_folder")
472 custom_objects_folder = value;
473 else if (key == "additional_roms")
474 additional_roms = ParseStringList(value);
475 } else if (current_section == "feature_flags") {
476 if (key == "load_custom_overworld")
478 else if (key == "apply_zs_custom_overworld_asm")
480 else if (key == "save_dungeon_maps")
481 feature_flags.kSaveDungeonMaps = ParseBool(value);
482 else if (key == "save_graphics_sheet")
483 feature_flags.kSaveGraphicsSheet = ParseBool(value);
484 else if (key == "enable_custom_objects")
485 feature_flags.kEnableCustomObjects = ParseBool(value);
486 } else if (current_section == "workspace") {
487 if (key == "font_global_scale")
488 workspace_settings.font_global_scale = ParseFloat(value);
489 else if (key == "dark_mode")
490 workspace_settings.dark_mode = ParseBool(value);
491 else if (key == "ui_theme")
493 else if (key == "autosave_enabled")
494 workspace_settings.autosave_enabled = ParseBool(value);
495 else if (key == "autosave_interval_secs")
496 workspace_settings.autosave_interval_secs = ParseFloat(value);
497 else if (key == "backup_on_save")
498 workspace_settings.backup_on_save = ParseBool(value);
499 else if (key == "show_grid")
500 workspace_settings.show_grid = ParseBool(value);
501 else if (key == "show_collision")
502 workspace_settings.show_collision = ParseBool(value);
503 else if (key == "prefer_hmagic_names")
504 workspace_settings.prefer_hmagic_names = ParseBool(value);
505 else if (key == "last_layout_preset")
507 else if (key == "saved_layouts")
508 workspace_settings.saved_layouts = ParseStringList(value);
509 else if (key == "recent_files")
510 workspace_settings.recent_files = ParseStringList(value);
511 } else if (current_section == "agent_settings") {
512 if (key == "ai_provider")
514 else if (key == "ai_model")
515 agent_settings.ai_model = value;
516 else if (key == "ollama_host")
518 else if (key == "gemini_api_key")
520 else if (key == "custom_system_prompt")
522 else if (key == "use_custom_prompt")
523 agent_settings.use_custom_prompt = ParseBool(value);
524 else if (key == "show_reasoning")
525 agent_settings.show_reasoning = ParseBool(value);
526 else if (key == "verbose")
527 agent_settings.verbose = ParseBool(value);
528 else if (key == "max_tool_iterations")
529 agent_settings.max_tool_iterations = std::stoi(value);
530 else if (key == "max_retry_attempts")
531 agent_settings.max_retry_attempts = std::stoi(value);
532 else if (key == "temperature")
533 agent_settings.temperature = ParseFloat(value);
534 else if (key == "top_p")
535 agent_settings.top_p = ParseFloat(value);
536 else if (key == "max_output_tokens")
537 agent_settings.max_output_tokens = std::stoi(value);
538 else if (key == "stream_responses")
539 agent_settings.stream_responses = ParseBool(value);
540 else if (key == "favorite_models")
541 agent_settings.favorite_models = ParseStringList(value);
542 else if (key == "model_chain")
543 agent_settings.model_chain = ParseStringList(value);
544 else if (key == "chain_mode")
545 agent_settings.chain_mode = std::stoi(value);
546 else if (key == "enable_tool_resources")
547 agent_settings.enable_tool_resources = ParseBool(value);
548 else if (key == "enable_tool_dungeon")
549 agent_settings.enable_tool_dungeon = ParseBool(value);
550 else if (key == "enable_tool_overworld")
551 agent_settings.enable_tool_overworld = ParseBool(value);
552 else if (key == "enable_tool_messages")
553 agent_settings.enable_tool_messages = ParseBool(value);
554 else if (key == "enable_tool_dialogue")
555 agent_settings.enable_tool_dialogue = ParseBool(value);
556 else if (key == "enable_tool_gui")
557 agent_settings.enable_tool_gui = ParseBool(value);
558 else if (key == "enable_tool_music")
559 agent_settings.enable_tool_music = ParseBool(value);
560 else if (key == "enable_tool_sprite")
561 agent_settings.enable_tool_sprite = ParseBool(value);
562 else if (key == "enable_tool_emulator")
563 agent_settings.enable_tool_emulator = ParseBool(value);
564 else if (key == "builder_blueprint_path")
566 } else if (current_section == "build") {
567 if (key == "build_script")
568 build_script = value;
569 else if (key == "output_folder")
570 output_folder = value;
571 else if (key == "git_repository")
572 git_repository = value;
573 else if (key == "track_changes")
574 track_changes = ParseBool(value);
575 else if (key == "build_configurations")
576 build_configurations = ParseStringList(value);
577 else if (key == "build_target")
578 build_target = value;
579 else if (key == "asm_entry_point")
580 asm_entry_point = value;
581 else if (key == "asm_sources")
582 asm_sources = ParseStringList(value);
583 else if (key == "last_build_hash")
584 last_build_hash = value;
585 else if (key == "build_number")
586 build_number = std::stoi(value);
587 } else if (current_section.rfind("labels_", 0) == 0) {
588 std::string label_type = current_section.substr(7);
589 resource_labels[label_type][key] = value;
590 } else if (current_section == "keybindings") {
592 } else if (current_section == "editor_visibility") {
593 workspace_settings.editor_visibility[key] = ParseBool(value);
594 } else if (current_section == "zscream_compatibility") {
595 if (key == "original_project_file")
596 zscream_project_file = value;
597 else
598 zscream_mappings[key] = value;
599 } else if (current_section == "music") {
600 if (key == "persist_custom_music")
601 music_persistence.persist_custom_music = ParseBool(value);
602 else if (key == "storage_key")
604 else if (key == "last_saved_at")
606 }
607 }
608
609 if (metadata.project_id.empty()) {
611 }
612 if (metadata.created_by.empty()) {
613 metadata.created_by = "YAZE";
614 }
615 if (music_persistence.storage_key.empty()) {
617 }
618
619 return absl::OkStatus();
620}
621
622absl::Status YazeProject::LoadFromYazeFormat(const std::string& project_path) {
623#ifdef __EMSCRIPTEN__
624 auto storage_key = MakeStorageKey("project");
625 auto storage_or = platform::WasmStorage::LoadProject(storage_key);
626 if (storage_or.ok()) {
627 return ParseFromString(storage_or.value());
628 }
629#endif // __EMSCRIPTEN__
630
631 std::ifstream file(project_path);
632 if (!file.is_open()) {
633 return absl::InvalidArgumentError(
634 absl::StrFormat("Cannot open project file: %s", project_path));
635 }
636
637 std::stringstream buffer;
638 buffer << file.rdbuf();
639 file.close();
640 return ParseFromString(buffer.str());
641}
642
644 // Update last modified timestamp
645 auto now = std::chrono::system_clock::now();
646 auto time_t = std::chrono::system_clock::to_time_t(now);
647 std::stringstream ss;
648 ss << std::put_time(std::localtime(&time_t), "%Y-%m-%d %H:%M:%S");
649 metadata.last_modified = ss.str();
650 if (music_persistence.storage_key.empty()) {
652 }
653
654 ASSIGN_OR_RETURN(auto serialized, SerializeToString());
655
656#ifdef __EMSCRIPTEN__
657 auto storage_status =
658 platform::WasmStorage::SaveProject(MakeStorageKey("project"), serialized);
659 if (!storage_status.ok()) {
660 return storage_status;
661 }
662#else
663 if (!filepath.empty()) {
664 std::ofstream file(filepath);
665 if (!file.is_open()) {
666 return absl::InvalidArgumentError(
667 absl::StrFormat("Cannot create project file: %s", filepath));
668 }
669 file << serialized;
670 file.close();
671 }
672#endif
673
674 return absl::OkStatus();
675}
676
678 const std::string& zscream_project_path) {
679 // Basic ZScream project import (to be expanded based on ZScream format)
680 zscream_project_file = zscream_project_path;
682
683 // Extract project name from path
684 std::filesystem::path zs_path(zscream_project_path);
685 name = zs_path.stem().string() + "_imported";
686
687 // Set up basic mapping for common fields
688 zscream_mappings["rom_file"] = "rom_filename";
689 zscream_mappings["source_code"] = "code_folder";
690 zscream_mappings["project_name"] = "name";
691
693
694 // TODO: Implement actual ZScream format parsing when format is known
695 // For now, just create a project structure that can be manually configured
696
697 return absl::OkStatus();
698}
699
700absl::Status YazeProject::ExportForZScream(const std::string& target_path) {
701 // Create a simplified project file that ZScream might understand
702 std::ofstream file(target_path);
703 if (!file.is_open()) {
704 return absl::InvalidArgumentError(
705 absl::StrFormat("Cannot create ZScream project file: %s", target_path));
706 }
707
708 // Write in a simple format that ZScream might understand
709 file << "# ZScream Compatible Project File\n";
710 file << "# Exported from YAZE " << metadata.yaze_version << "\n\n";
711 file << "name=" << name << "\n";
712 file << "rom_file=" << rom_filename << "\n";
713 file << "source_code=" << code_folder << "\n";
714 file << "description=" << metadata.description << "\n";
715 file << "author=" << metadata.author << "\n";
716 file << "created_with=YAZE " << metadata.yaze_version << "\n";
717
718 file.close();
719 return absl::OkStatus();
720}
721
723 // Consolidated loading of all settings from project file
724 // This replaces scattered config loading throughout the application
726}
727
729 // Consolidated saving of all settings to project file
730 return SaveToYazeFormat();
731}
732
735 return Save();
736}
737
738absl::Status YazeProject::Validate() const {
739 std::vector<std::string> errors;
740
741 if (name.empty())
742 errors.push_back("Project name is required");
743 if (filepath.empty())
744 errors.push_back("Project file path is required");
745 if (rom_filename.empty())
746 errors.push_back("ROM file is required");
747
748#ifndef __EMSCRIPTEN__
749 // Check if files exist
750 if (!rom_filename.empty() &&
751 !std::filesystem::exists(GetAbsolutePath(rom_filename))) {
752 errors.push_back("ROM file does not exist: " + rom_filename);
753 }
754
755 if (!code_folder.empty() &&
756 !std::filesystem::exists(GetAbsolutePath(code_folder))) {
757 errors.push_back("Code folder does not exist: " + code_folder);
758 }
759
760 if (!labels_filename.empty() &&
761 !std::filesystem::exists(GetAbsolutePath(labels_filename))) {
762 errors.push_back("Labels file does not exist: " + labels_filename);
763 }
764#endif // __EMSCRIPTEN__
765
766 if (!errors.empty()) {
767 return absl::InvalidArgumentError(absl::StrJoin(errors, "; "));
768 }
769
770 return absl::OkStatus();
771}
772
773std::vector<std::string> YazeProject::GetMissingFiles() const {
774 std::vector<std::string> missing;
775
776#ifndef __EMSCRIPTEN__
777 if (!rom_filename.empty() &&
778 !std::filesystem::exists(GetAbsolutePath(rom_filename))) {
779 missing.push_back(rom_filename);
780 }
781 if (!labels_filename.empty() &&
782 !std::filesystem::exists(GetAbsolutePath(labels_filename))) {
783 missing.push_back(labels_filename);
784 }
785 if (!symbols_filename.empty() &&
786 !std::filesystem::exists(GetAbsolutePath(symbols_filename))) {
787 missing.push_back(symbols_filename);
788 }
789#endif // __EMSCRIPTEN__
790
791 return missing;
792}
793
795#ifdef __EMSCRIPTEN__
796 // In the web build, filesystem layout is virtual; nothing to repair eagerly.
797 return absl::OkStatus();
798#else
799 // Create missing directories
800 std::vector<std::string> folders = {code_folder, assets_folder,
803
804 for (const auto& folder : folders) {
805 if (!folder.empty()) {
806 std::filesystem::path abs_path = GetAbsolutePath(folder);
807 if (!std::filesystem::exists(abs_path)) {
808 std::filesystem::create_directories(abs_path);
809 }
810 }
811 }
812
813 // Create missing files with defaults
814 if (!labels_filename.empty()) {
815 std::filesystem::path abs_labels = GetAbsolutePath(labels_filename);
816 if (!std::filesystem::exists(abs_labels)) {
817 std::ofstream labels_file(abs_labels);
818 labels_file << "# yaze Resource Labels\n";
819 labels_file << "# Format: [type] key=value\n\n";
820 labels_file.close();
821 }
822 }
823
824 return absl::OkStatus();
825#endif
826}
827
828std::string YazeProject::GetDisplayName() const {
829 if (!metadata.description.empty()) {
830 return metadata.description;
831 }
832 return name.empty() ? "Untitled Project" : name;
833}
834
836 const std::string& absolute_path) const {
837 if (absolute_path.empty() || filepath.empty())
838 return absolute_path;
839
840 std::filesystem::path project_dir =
841 std::filesystem::path(filepath).parent_path();
842 std::filesystem::path abs_path(absolute_path);
843
844 try {
845 std::filesystem::path relative =
846 std::filesystem::relative(abs_path, project_dir);
847 return relative.string();
848 } catch (...) {
849 return absolute_path; // Return absolute path if relative conversion fails
850 }
851}
852
854 const std::string& relative_path) const {
855 if (relative_path.empty() || filepath.empty())
856 return relative_path;
857
858 std::filesystem::path project_dir =
859 std::filesystem::path(filepath).parent_path();
860 std::filesystem::path abs_path = project_dir / relative_path;
861
862 return abs_path.string();
863}
864
866 return name.empty() && rom_filename.empty() && code_folder.empty();
867}
868
870 const std::string& project_path) {
871 // TODO: Implement ZScream format parsing when format specification is
872 // available For now, create a basic project that can be manually configured
873
874 std::filesystem::path zs_path(project_path);
875 name = zs_path.stem().string() + "_imported";
876 zscream_project_file = project_path;
877
879
880 return absl::OkStatus();
881}
882
884 if (metadata.project_id.empty()) {
886 }
887
888 // Initialize default feature flags
893 // REMOVED: kLogInstructions (deprecated)
894
895 // Initialize default workspace settings
898 workspace_settings.ui_theme = "default";
900 workspace_settings.autosave_interval_secs = 300.0f; // 5 minutes
904
905 // Initialize default build configurations
906 build_configurations = {"Debug", "Release", "Distribution"};
907 build_target.clear();
908 asm_entry_point = "asm/main.asm";
909 asm_sources = {"asm"};
910 last_build_hash.clear();
911 build_number = 0;
912
913 track_changes = true;
914
918
919 if (metadata.created_by.empty()) {
920 metadata.created_by = "YAZE";
921 }
922}
923
925 auto now = std::chrono::system_clock::now().time_since_epoch();
926 auto timestamp =
927 std::chrono::duration_cast<std::chrono::milliseconds>(now).count();
928 return absl::StrFormat("yaze_project_%lld", timestamp);
929}
930
931// ProjectManager Implementation
932std::vector<ProjectManager::ProjectTemplate>
934 std::vector<ProjectTemplate> templates;
935
936 // ==========================================================================
937 // ZSCustomOverworld Templates (Recommended)
938 // ==========================================================================
939
940 // Vanilla ROM Hack - no ZSO
941 {
943 t.name = "Vanilla ROM Hack";
944 t.description = "Standard ROM editing without custom ASM. Limited to vanilla features.";
952 templates.push_back(t);
953 }
954
955 // ZSCustomOverworld v2 - Basic expansion
956 {
958 t.name = "ZSCustomOverworld v2";
959 t.description = "Basic overworld expansion: custom BG colors, main palettes, parent system.";
960 t.icon = ICON_MD_MAP;
969 t.template_project.metadata.tags = {"zso_v2", "overworld", "expansion"};
970 templates.push_back(t);
971 }
972
973 // ZSCustomOverworld v3 - Full features (Recommended)
974 {
976 t.name = "ZSCustomOverworld v3 (Recommended)";
977 t.description = "Full overworld expansion: wide/tall areas, animated GFX, overlays, all features.";
990 t.template_project.metadata.tags = {"zso_v3", "overworld", "full", "recommended"};
991 templates.push_back(t);
992 }
993
994 // Randomizer Compatible
995 {
997 t.name = "Randomizer Compatible";
998 t.description = "Compatible with ALttP Randomizer. Minimal custom features to avoid conflicts.";
1004 t.template_project.metadata.tags = {"randomizer", "compatible", "minimal"};
1005 templates.push_back(t);
1006 }
1007
1008 // ==========================================================================
1009 // Editor-Focused Templates
1010 // ==========================================================================
1011
1012 // Dungeon Designer
1013 {
1015 t.name = "Dungeon Designer";
1016 t.description = "Focused on dungeon creation and modification.";
1017 t.icon = ICON_MD_DOMAIN;
1021 t.template_project.metadata.tags = {"dungeons", "rooms", "design"};
1022 templates.push_back(t);
1023 }
1024
1025 // Graphics Pack
1026 {
1028 t.name = "Graphics Pack";
1029 t.description = "Project focused on graphics, sprites, and visual modifications.";
1036 t.template_project.metadata.tags = {"graphics", "sprites", "palettes"};
1037 templates.push_back(t);
1038 }
1039
1040 // Complete Overhaul
1041 {
1043 t.name = "Complete Overhaul";
1044 t.description = "Full-scale ROM hack with all features enabled.";
1045 t.icon = ICON_MD_BUILD;
1057 t.template_project.metadata.tags = {"complete", "overhaul", "full-mod"};
1058 templates.push_back(t);
1059 }
1060
1061 return templates;
1062}
1063
1064absl::StatusOr<YazeProject> ProjectManager::CreateFromTemplate(
1065 const std::string& template_name, const std::string& project_name,
1066 const std::string& base_path) {
1067 YazeProject project;
1068 auto status = project.Create(project_name, base_path);
1069 if (!status.ok()) {
1070 return status;
1071 }
1072
1073 // Customize based on template
1074 if (template_name == "Full Overworld Mod") {
1077 project.metadata.description = "Overworld modification project";
1078 project.metadata.tags = {"overworld", "maps", "graphics"};
1079 } else if (template_name == "Dungeon Designer") {
1080 project.feature_flags.kSaveDungeonMaps = true;
1081 project.workspace_settings.show_grid = true;
1082 project.metadata.description = "Dungeon design and modification project";
1083 project.metadata.tags = {"dungeons", "rooms", "design"};
1084 } else if (template_name == "Graphics Pack") {
1085 project.feature_flags.kSaveGraphicsSheet = true;
1086 project.workspace_settings.show_grid = true;
1087 project.metadata.description = "Graphics and sprite modification project";
1088 project.metadata.tags = {"graphics", "sprites", "palettes"};
1089 } else if (template_name == "Complete Overhaul") {
1092 project.feature_flags.kSaveDungeonMaps = true;
1093 project.feature_flags.kSaveGraphicsSheet = true;
1094 project.metadata.description = "Complete ROM overhaul project";
1095 project.metadata.tags = {"complete", "overhaul", "full-mod"};
1096 }
1097
1098 status = project.Save();
1099 if (!status.ok()) {
1100 return status;
1101 }
1102
1103 return project;
1104}
1105
1107 const std::string& directory) {
1108#ifdef __EMSCRIPTEN__
1109 (void)directory;
1110 return {};
1111#else
1112 std::vector<std::string> projects;
1113
1114 try {
1115 for (const auto& entry : std::filesystem::directory_iterator(directory)) {
1116 if (entry.is_regular_file()) {
1117 std::string filename = entry.path().filename().string();
1118 if (filename.ends_with(".yaze") || filename.ends_with(".zsproj")) {
1119 projects.push_back(entry.path().string());
1120 }
1121 }
1122 }
1123 } catch (const std::filesystem::filesystem_error& e) {
1124 // Directory doesn't exist or can't be accessed
1125 }
1126
1127 return projects;
1128#endif // __EMSCRIPTEN__
1129}
1130
1131absl::Status ProjectManager::BackupProject(const YazeProject& project) {
1132#ifdef __EMSCRIPTEN__
1133 (void)project;
1134 return absl::UnimplementedError(
1135 "Project backups are not supported in the web build");
1136#else
1137 if (project.filepath.empty()) {
1138 return absl::InvalidArgumentError("Project has no file path");
1139 }
1140
1141 std::filesystem::path project_path(project.filepath);
1142 std::filesystem::path backup_dir = project_path.parent_path() / "backups";
1143 std::filesystem::create_directories(backup_dir);
1144
1145 auto now = std::chrono::system_clock::now();
1146 auto time_t = std::chrono::system_clock::to_time_t(now);
1147 std::stringstream ss;
1148 ss << std::put_time(std::localtime(&time_t), "%Y%m%d_%H%M%S");
1149
1150 std::string backup_filename = project.name + "_backup_" + ss.str() + ".yaze";
1151 std::filesystem::path backup_path = backup_dir / backup_filename;
1152
1153 try {
1154 std::filesystem::copy_file(project.filepath, backup_path);
1155 } catch (const std::filesystem::filesystem_error& e) {
1156 return absl::InternalError(
1157 absl::StrFormat("Failed to backup project: %s", e.what()));
1158 }
1159
1160 return absl::OkStatus();
1161#endif
1162}
1163
1165 const YazeProject& project) {
1166 return project.Validate();
1167}
1168
1170 const YazeProject& project) {
1171 std::vector<std::string> recommendations;
1172
1173 if (project.rom_filename.empty()) {
1174 recommendations.push_back("Add a ROM file to begin editing");
1175 }
1176
1177 if (project.code_folder.empty()) {
1178 recommendations.push_back("Set up a code folder for assembly patches");
1179 }
1180
1181 if (project.labels_filename.empty()) {
1182 recommendations.push_back("Create a labels file for better organization");
1183 }
1184
1185 if (project.metadata.description.empty()) {
1186 recommendations.push_back("Add a project description for documentation");
1187 }
1188
1189 if (project.git_repository.empty() && project.track_changes) {
1190 recommendations.push_back(
1191 "Consider setting up version control for your project");
1192 }
1193
1194 auto missing_files = project.GetMissingFiles();
1195 if (!missing_files.empty()) {
1196 recommendations.push_back(
1197 "Some project files are missing - use Project > Repair to fix");
1198 }
1199
1200 return recommendations;
1201}
1202
1203// Compatibility implementations for ResourceLabelManager and related classes
1204bool ResourceLabelManager::LoadLabels(const std::string& filename) {
1205 filename_ = filename;
1206 std::ifstream file(filename);
1207 if (!file.is_open()) {
1208 labels_loaded_ = false;
1209 return false;
1210 }
1211
1212 labels_.clear();
1213 std::string line;
1214 std::string current_type = "";
1215
1216 while (std::getline(file, line)) {
1217 if (line.empty() || line[0] == '#')
1218 continue;
1219
1220 // Check for type headers [type_name]
1221 if (line[0] == '[' && line.back() == ']') {
1222 current_type = line.substr(1, line.length() - 2);
1223 continue;
1224 }
1225
1226 // Parse key=value pairs
1227 size_t eq_pos = line.find('=');
1228 if (eq_pos != std::string::npos && !current_type.empty()) {
1229 std::string key = line.substr(0, eq_pos);
1230 std::string value = line.substr(eq_pos + 1);
1231 labels_[current_type][key] = value;
1232 }
1233 }
1234
1235 file.close();
1236 labels_loaded_ = true;
1237 return true;
1238}
1239
1241 if (filename_.empty())
1242 return false;
1243
1244 std::ofstream file(filename_);
1245 if (!file.is_open())
1246 return false;
1247
1248 file << "# yaze Resource Labels\n";
1249 file << "# Format: [type] followed by key=value pairs\n\n";
1250
1251 for (const auto& [type, type_labels] : labels_) {
1252 if (!type_labels.empty()) {
1253 file << "[" << type << "]\n";
1254 for (const auto& [key, value] : type_labels) {
1255 file << key << "=" << value << "\n";
1256 }
1257 file << "\n";
1258 }
1259 }
1260
1261 file.close();
1262 return true;
1263}
1264
1266 if (!p_open || !*p_open)
1267 return;
1268
1269 // Basic implementation - can be enhanced later
1270 if (ImGui::Begin("Resource Labels", p_open)) {
1271 ImGui::Text("Resource Labels Manager");
1272 ImGui::Text("Labels loaded: %s", labels_loaded_ ? "Yes" : "No");
1273 ImGui::Text("Total types: %zu", labels_.size());
1274
1275 for (const auto& [type, type_labels] : labels_) {
1276 if (ImGui::TreeNode(type.c_str())) {
1277 ImGui::Text("Labels: %zu", type_labels.size());
1278 for (const auto& [key, value] : type_labels) {
1279 ImGui::Text("%s = %s", key.c_str(), value.c_str());
1280 }
1281 ImGui::TreePop();
1282 }
1283 }
1284 }
1285 ImGui::End();
1286}
1287
1288void ResourceLabelManager::EditLabel(const std::string& type,
1289 const std::string& key,
1290 const std::string& newValue) {
1291 labels_[type][key] = newValue;
1292}
1293
1295 bool selected, const std::string& type, const std::string& key,
1296 const std::string& defaultValue) {
1297 // Basic implementation
1298 if (ImGui::Selectable(
1299 absl::StrFormat("%s: %s", key.c_str(), GetLabel(type, key).c_str())
1300 .c_str(),
1301 selected)) {
1302 // Handle selection
1303 }
1304}
1305
1306std::string ResourceLabelManager::GetLabel(const std::string& type,
1307 const std::string& key) {
1308 auto type_it = labels_.find(type);
1309 if (type_it == labels_.end())
1310 return "";
1311
1312 auto label_it = type_it->second.find(key);
1313 if (label_it == type_it->second.end())
1314 return "";
1315
1316 return label_it->second;
1317}
1318
1320 const std::string& type, const std::string& key,
1321 const std::string& defaultValue) {
1322 auto existing = GetLabel(type, key);
1323 if (!existing.empty())
1324 return existing;
1325
1326 labels_[type][key] = defaultValue;
1327 return defaultValue;
1328}
1329
1330// ============================================================================
1331// Embedded Labels Support
1332// ============================================================================
1333
1335 const std::unordered_map<std::string,
1336 std::unordered_map<std::string, std::string>>&
1337 labels) {
1338 try {
1339 // Load all default Zelda3 resource names into resource_labels
1340 // We merge them with existing labels, prioritizing existing overrides?
1341 // Or just overwrite? The previous code was:
1342 // resource_labels = zelda3::Zelda3Labels::ToResourceLabels();
1343 // which implies overwriting. But we want to keep overrides if possible.
1344 // However, this is usually called on load.
1345
1346 // Let's overwrite for now to match previous behavior, assuming overrides
1347 // are loaded afterwards or this is initial setup.
1348 // Actually, if we load project then init embedded labels, we might lose overrides.
1349 // But typically overrides are loaded from the project file *into* resource_labels.
1350 // If we call this, we might clobber them.
1351 // The previous implementation clobbered resource_labels.
1352
1353 // However, if we want to support overrides + embedded, we should merge.
1354 // But `resource_labels` was treated as "overrides" in the old code?
1355 // No, `resource_labels` was the container for loaded labels.
1356
1357 // If I look at `LoadFromYazeFormat`:
1358 // It parses `[labels_type]` into `resource_labels`.
1359
1360 // If `use_embedded_labels` is true, `InitializeEmbeddedLabels` is called?
1361 // I need to check when `InitializeEmbeddedLabels` is called.
1362
1363 resource_labels = labels;
1364 use_embedded_labels = true;
1365
1366 LOG_DEBUG("Project", "Initialized embedded labels:");
1367 LOG_DEBUG("Project", " - %d room names", resource_labels["room"].size());
1368 LOG_DEBUG("Project", " - %d entrance names",
1369 resource_labels["entrance"].size());
1370 LOG_DEBUG("Project", " - %d sprite names",
1371 resource_labels["sprite"].size());
1372 LOG_DEBUG("Project", " - %d overlord names",
1373 resource_labels["overlord"].size());
1374 LOG_DEBUG("Project", " - %d item names", resource_labels["item"].size());
1375 LOG_DEBUG("Project", " - %d music names",
1376 resource_labels["music"].size());
1377 LOG_DEBUG("Project", " - %d graphics names",
1378 resource_labels["graphics"].size());
1379 LOG_DEBUG("Project", " - %d room effect names",
1380 resource_labels["room_effect"].size());
1381 LOG_DEBUG("Project", " - %d room tag names",
1382 resource_labels["room_tag"].size());
1383 LOG_DEBUG("Project", " - %d tile type names",
1384 resource_labels["tile_type"].size());
1385
1386 return absl::OkStatus();
1387 } catch (const std::exception& e) {
1388 return absl::InternalError(
1389 absl::StrCat("Failed to initialize embedded labels: ", e.what()));
1390 }
1391}
1392
1393std::string YazeProject::GetLabel(const std::string& resource_type, int id,
1394 const std::string& default_value) const {
1395 // First check if we have a custom label override
1396 auto type_it = resource_labels.find(resource_type);
1397 if (type_it != resource_labels.end()) {
1398 auto label_it = type_it->second.find(std::to_string(id));
1399 if (label_it != type_it->second.end()) {
1400 return label_it->second;
1401 }
1402 }
1403
1404 return default_value.empty() ? resource_type + "_" + std::to_string(id)
1405 : default_value;
1406}
1407
1408absl::Status YazeProject::ImportLabelsFromZScream(const std::string& filepath) {
1409#ifdef __EMSCRIPTEN__
1410 (void)filepath;
1411 return absl::UnimplementedError(
1412 "File-based label import is not supported in the web build");
1413#else
1414 std::ifstream file(filepath);
1415 if (!file.is_open()) {
1416 return absl::InvalidArgumentError(
1417 absl::StrFormat("Cannot open labels file: %s", filepath));
1418 }
1419
1420 std::stringstream buffer;
1421 buffer << file.rdbuf();
1422 file.close();
1423
1424 return ImportLabelsFromZScreamContent(buffer.str());
1425#endif
1426}
1427
1429 const std::string& content) {
1430 // Initialize the global provider with our labels
1431 auto& provider = zelda3::GetResourceLabels();
1432 provider.SetProjectLabels(&resource_labels);
1433 provider.SetPreferHMagicNames(workspace_settings.prefer_hmagic_names);
1434
1435 // Use the provider to parse ZScream format
1436 auto status = provider.ImportFromZScreamFormat(content);
1437 if (!status.ok()) {
1438 return status;
1439 }
1440
1441 LOG_DEBUG("Project", "Imported ZScream labels:");
1442 LOG_DEBUG("Project", " - %d sprite labels", resource_labels["sprite"].size());
1443 LOG_DEBUG("Project", " - %d room labels", resource_labels["room"].size());
1444 LOG_DEBUG("Project", " - %d item labels", resource_labels["item"].size());
1445 LOG_DEBUG("Project", " - %d room tag labels",
1446 resource_labels["room_tag"].size());
1447
1448 return absl::OkStatus();
1449}
1450
1452 auto& provider = zelda3::GetResourceLabels();
1453 provider.SetProjectLabels(&resource_labels);
1454 provider.SetPreferHMagicNames(workspace_settings.prefer_hmagic_names);
1455
1456 LOG_DEBUG("Project",
1457 "Initialized ResourceLabelProvider with project labels");
1458 LOG_DEBUG("Project", " - prefer_hmagic_names: %s",
1459 workspace_settings.prefer_hmagic_names ? "true" : "false");
1460}
1461
1462// ============================================================================
1463// JSON Format Support (Optional)
1464// ============================================================================
1465
1466#ifdef YAZE_ENABLE_JSON_PROJECT_FORMAT
1467
1468absl::Status YazeProject::LoadFromJsonFormat(const std::string& project_path) {
1469#ifdef __EMSCRIPTEN__
1470 return absl::UnimplementedError(
1471 "JSON project format loading is not supported in the web build");
1472#endif
1473 std::ifstream file(project_path);
1474 if (!file.is_open()) {
1475 return absl::InvalidArgumentError(
1476 absl::StrFormat("Cannot open JSON project file: %s", project_path));
1477 }
1478
1479 try {
1480 json j;
1481 file >> j;
1482
1483 // Parse project metadata
1484 if (j.contains("yaze_project")) {
1485 auto& proj = j["yaze_project"];
1486
1487 if (proj.contains("name"))
1488 name = proj["name"].get<std::string>();
1489 if (proj.contains("description"))
1490 metadata.description = proj["description"].get<std::string>();
1491 if (proj.contains("author"))
1492 metadata.author = proj["author"].get<std::string>();
1493 if (proj.contains("version"))
1494 metadata.version = proj["version"].get<std::string>();
1495 if (proj.contains("created"))
1496 metadata.created_date = proj["created"].get<std::string>();
1497 if (proj.contains("modified"))
1498 metadata.last_modified = proj["modified"].get<std::string>();
1499 if (proj.contains("created_by"))
1500 metadata.created_by = proj["created_by"].get<std::string>();
1501
1502 // Files
1503 if (proj.contains("rom_filename"))
1504 rom_filename = proj["rom_filename"].get<std::string>();
1505 if (proj.contains("code_folder"))
1506 code_folder = proj["code_folder"].get<std::string>();
1507 if (proj.contains("assets_folder"))
1508 assets_folder = proj["assets_folder"].get<std::string>();
1509 if (proj.contains("patches_folder"))
1510 patches_folder = proj["patches_folder"].get<std::string>();
1511 if (proj.contains("labels_filename"))
1512 labels_filename = proj["labels_filename"].get<std::string>();
1513 if (proj.contains("symbols_filename"))
1514 symbols_filename = proj["symbols_filename"].get<std::string>();
1515
1516 // Embedded labels flag
1517 if (proj.contains("use_embedded_labels")) {
1518 use_embedded_labels = proj["use_embedded_labels"].get<bool>();
1519 }
1520
1521 // Feature flags
1522 if (proj.contains("feature_flags")) {
1523 auto& flags = proj["feature_flags"];
1524 // REMOVED: kLogInstructions (deprecated - DisassemblyViewer always
1525 // active)
1526 if (flags.contains("kSaveDungeonMaps"))
1528 flags["kSaveDungeonMaps"].get<bool>();
1529 if (flags.contains("kSaveGraphicsSheet"))
1531 flags["kSaveGraphicsSheet"].get<bool>();
1532 }
1533
1534 // Workspace settings
1535 if (proj.contains("workspace_settings")) {
1536 auto& ws = proj["workspace_settings"];
1537 if (ws.contains("auto_save_enabled"))
1539 ws["auto_save_enabled"].get<bool>();
1540 if (ws.contains("auto_save_interval"))
1542 ws["auto_save_interval"].get<float>();
1543 }
1544
1545 if (proj.contains("agent_settings") &&
1546 proj["agent_settings"].is_object()) {
1547 auto& agent = proj["agent_settings"];
1549 agent.value("ai_provider", agent_settings.ai_provider);
1551 agent.value("ai_model", agent_settings.ai_model);
1553 agent.value("ollama_host", agent_settings.ollama_host);
1555 agent.value("gemini_api_key", agent_settings.gemini_api_key);
1557 agent.value("use_custom_prompt", agent_settings.use_custom_prompt);
1559 "custom_system_prompt", agent_settings.custom_system_prompt);
1561 agent.value("show_reasoning", agent_settings.show_reasoning);
1562 agent_settings.verbose = agent.value("verbose", agent_settings.verbose);
1563 agent_settings.max_tool_iterations = agent.value(
1564 "max_tool_iterations", agent_settings.max_tool_iterations);
1565 agent_settings.max_retry_attempts = agent.value(
1566 "max_retry_attempts", agent_settings.max_retry_attempts);
1568 agent.value("temperature", agent_settings.temperature);
1569 agent_settings.top_p = agent.value("top_p", agent_settings.top_p);
1571 agent.value("max_output_tokens", agent_settings.max_output_tokens);
1573 agent.value("stream_responses", agent_settings.stream_responses);
1574 if (agent.contains("favorite_models") &&
1575 agent["favorite_models"].is_array()) {
1577 for (const auto& model : agent["favorite_models"]) {
1578 if (model.is_string())
1580 model.get<std::string>());
1581 }
1582 }
1583 if (agent.contains("model_chain") && agent["model_chain"].is_array()) {
1585 for (const auto& model : agent["model_chain"]) {
1586 if (model.is_string())
1587 agent_settings.model_chain.push_back(model.get<std::string>());
1588 }
1589 }
1591 agent.value("chain_mode", agent_settings.chain_mode);
1593 "enable_tool_resources", agent_settings.enable_tool_resources);
1594 agent_settings.enable_tool_dungeon = agent.value(
1595 "enable_tool_dungeon", agent_settings.enable_tool_dungeon);
1597 "enable_tool_overworld", agent_settings.enable_tool_overworld);
1599 "enable_tool_messages", agent_settings.enable_tool_messages);
1601 "enable_tool_dialogue", agent_settings.enable_tool_dialogue);
1603 agent.value("enable_tool_gui", agent_settings.enable_tool_gui);
1605 agent.value("enable_tool_music", agent_settings.enable_tool_music);
1606 agent_settings.enable_tool_sprite = agent.value(
1607 "enable_tool_sprite", agent_settings.enable_tool_sprite);
1609 "enable_tool_emulator", agent_settings.enable_tool_emulator);
1611 "builder_blueprint_path", agent_settings.builder_blueprint_path);
1612 }
1613
1614 // Build settings
1615 if (proj.contains("build_script"))
1616 build_script = proj["build_script"].get<std::string>();
1617 if (proj.contains("output_folder"))
1618 output_folder = proj["output_folder"].get<std::string>();
1619 if (proj.contains("git_repository"))
1620 git_repository = proj["git_repository"].get<std::string>();
1621 if (proj.contains("track_changes"))
1622 track_changes = proj["track_changes"].get<bool>();
1623 }
1624
1625 return absl::OkStatus();
1626 } catch (const json::exception& e) {
1627 return absl::InvalidArgumentError(
1628 absl::StrFormat("JSON parse error: %s", e.what()));
1629 }
1630}
1631
1632absl::Status YazeProject::SaveToJsonFormat() {
1633#ifdef __EMSCRIPTEN__
1634 return absl::UnimplementedError(
1635 "JSON project format saving is not supported in the web build");
1636#endif
1637 json j;
1638 auto& proj = j["yaze_project"];
1639
1640 // Metadata
1641 proj["version"] = metadata.version;
1642 proj["name"] = name;
1643 proj["author"] = metadata.author;
1644 proj["created_by"] = metadata.created_by;
1645 proj["description"] = metadata.description;
1646 proj["created"] = metadata.created_date;
1647 proj["modified"] = metadata.last_modified;
1648
1649 // Files
1650 proj["rom_filename"] = rom_filename;
1651 proj["code_folder"] = code_folder;
1652 proj["assets_folder"] = assets_folder;
1653 proj["patches_folder"] = patches_folder;
1654 proj["labels_filename"] = labels_filename;
1655 proj["symbols_filename"] = symbols_filename;
1656 proj["output_folder"] = output_folder;
1657
1658 // Embedded labels
1659 proj["use_embedded_labels"] = use_embedded_labels;
1660
1661 // Feature flags
1662 // REMOVED: kLogInstructions (deprecated)
1663 proj["feature_flags"]["kSaveDungeonMaps"] = feature_flags.kSaveDungeonMaps;
1664 proj["feature_flags"]["kSaveGraphicsSheet"] =
1666
1667 // Workspace settings
1668 proj["workspace_settings"]["auto_save_enabled"] =
1670 proj["workspace_settings"]["auto_save_interval"] =
1672
1673 auto& agent = proj["agent_settings"];
1674 agent["ai_provider"] = agent_settings.ai_provider;
1675 agent["ai_model"] = agent_settings.ai_model;
1676 agent["ollama_host"] = agent_settings.ollama_host;
1677 agent["gemini_api_key"] = agent_settings.gemini_api_key;
1678 agent["use_custom_prompt"] = agent_settings.use_custom_prompt;
1679 agent["custom_system_prompt"] = agent_settings.custom_system_prompt;
1680 agent["show_reasoning"] = agent_settings.show_reasoning;
1681 agent["verbose"] = agent_settings.verbose;
1682 agent["max_tool_iterations"] = agent_settings.max_tool_iterations;
1683 agent["max_retry_attempts"] = agent_settings.max_retry_attempts;
1684 agent["temperature"] = agent_settings.temperature;
1685 agent["top_p"] = agent_settings.top_p;
1686 agent["max_output_tokens"] = agent_settings.max_output_tokens;
1687 agent["stream_responses"] = agent_settings.stream_responses;
1688 agent["favorite_models"] = agent_settings.favorite_models;
1689 agent["model_chain"] = agent_settings.model_chain;
1690 agent["chain_mode"] = agent_settings.chain_mode;
1691 agent["enable_tool_resources"] = agent_settings.enable_tool_resources;
1692 agent["enable_tool_dungeon"] = agent_settings.enable_tool_dungeon;
1693 agent["enable_tool_overworld"] = agent_settings.enable_tool_overworld;
1694 agent["enable_tool_messages"] = agent_settings.enable_tool_messages;
1695 agent["enable_tool_dialogue"] = agent_settings.enable_tool_dialogue;
1696 agent["enable_tool_gui"] = agent_settings.enable_tool_gui;
1697 agent["enable_tool_music"] = agent_settings.enable_tool_music;
1698 agent["enable_tool_sprite"] = agent_settings.enable_tool_sprite;
1699 agent["enable_tool_emulator"] = agent_settings.enable_tool_emulator;
1700 agent["builder_blueprint_path"] = agent_settings.builder_blueprint_path;
1701
1702 // Build settings
1703 proj["build_script"] = build_script;
1704 proj["git_repository"] = git_repository;
1705 proj["track_changes"] = track_changes;
1706
1707 // Write to file
1708 std::ofstream file(filepath);
1709 if (!file.is_open()) {
1710 return absl::InvalidArgumentError(
1711 absl::StrFormat("Cannot write JSON project file: %s", filepath));
1712 }
1713
1714 file << j.dump(2); // Pretty print with 2-space indent
1715 return absl::OkStatus();
1716}
1717
1718#endif // YAZE_ENABLE_JSON_PROJECT_FORMAT
1719
1720// RecentFilesManager implementation
1722 auto config_dir = util::PlatformPaths::GetConfigDirectory();
1723 if (!config_dir.ok()) {
1724 return ""; // Or handle error appropriately
1725 }
1726 return (*config_dir / kRecentFilesFilename).string();
1727}
1728
1730#ifdef __EMSCRIPTEN__
1731 auto status = platform::WasmStorage::SaveProject(
1732 kRecentFilesFilename, absl::StrJoin(recent_files_, "\n"));
1733 if (!status.ok()) {
1734 LOG_WARN("RecentFilesManager", "Could not persist recent files: %s",
1735 status.ToString().c_str());
1736 }
1737 return;
1738#endif
1739 // Ensure config directory exists
1740 auto config_dir_status = util::PlatformPaths::GetConfigDirectory();
1741 if (!config_dir_status.ok()) {
1742 LOG_ERROR("Project", "Failed to get or create config directory: %s",
1743 config_dir_status.status().ToString().c_str());
1744 return;
1745 }
1746
1747 std::string filepath = GetFilePath();
1748 std::ofstream file(filepath);
1749 if (!file.is_open()) {
1750 LOG_WARN("RecentFilesManager", "Could not save recent files to %s",
1751 filepath.c_str());
1752 return;
1753 }
1754
1755 for (const auto& file_path : recent_files_) {
1756 file << file_path << std::endl;
1757 }
1758}
1759
1761#ifdef __EMSCRIPTEN__
1762 auto storage_or = platform::WasmStorage::LoadProject(kRecentFilesFilename);
1763 if (!storage_or.ok()) {
1764 return;
1765 }
1766 recent_files_.clear();
1767 std::istringstream stream(storage_or.value());
1768 std::string line;
1769 while (std::getline(stream, line)) {
1770 if (!line.empty()) {
1771 recent_files_.push_back(line);
1772 }
1773 }
1774#else
1775 std::string filepath = GetFilePath();
1776 std::ifstream file(filepath);
1777 if (!file.is_open()) {
1778 // File doesn't exist yet, which is fine
1779 return;
1780 }
1781
1782 recent_files_.clear();
1783 std::string line;
1784 while (std::getline(file, line)) {
1785 if (!line.empty()) {
1786 recent_files_.push_back(line);
1787 }
1788 }
1789#endif
1790}
1791
1792} // namespace project
1793} // namespace yaze
static std::vector< std::string > FindProjectsInDirectory(const std::string &directory)
Definition project.cc:1106
static absl::Status ValidateProjectStructure(const YazeProject &project)
Definition project.cc:1164
static absl::StatusOr< YazeProject > CreateFromTemplate(const std::string &template_name, const std::string &project_name, const std::string &base_path)
Definition project.cc:1064
static std::vector< std::string > GetRecommendedFixesForProject(const YazeProject &project)
Definition project.cc:1169
static std::vector< ProjectTemplate > GetProjectTemplates()
Definition project.cc:933
static absl::Status BackupProject(const YazeProject &project)
Definition project.cc:1131
std::string GetFilePath() const
Definition project.cc:1721
std::vector< std::string > recent_files_
Definition project.h:358
static absl::StatusOr< std::filesystem::path > GetConfigDirectory()
Get the user-specific configuration directory for YAZE.
#define ICON_MD_SHUFFLE
Definition icons.h:1738
#define ICON_MD_TERRAIN
Definition icons.h:1952
#define ICON_MD_MAP
Definition icons.h:1173
#define ICON_MD_VIDEOGAME_ASSET
Definition icons.h:2076
#define ICON_MD_DOMAIN
Definition icons.h:603
#define ICON_MD_BUILD
Definition icons.h:328
#define ICON_MD_PALETTE
Definition icons.h:1370
#define LOG_DEBUG(category, format,...)
Definition log.h:103
#define LOG_ERROR(category, format,...)
Definition log.h:109
#define LOG_WARN(category, format,...)
Definition log.h:107
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
float ParseFloat(const std::string &value)
Definition project.cc:59
bool ParseBool(const std::string &value)
Definition project.cc:55
std::string SanitizeStorageKey(absl::string_view input)
Definition project.cc:84
std::vector< std::string > ParseStringList(const std::string &value)
Definition project.cc:67
const std::string kRecentFilesFilename
Definition project.h:305
ResourceLabelProvider & GetResourceLabels()
Get the global ResourceLabelProvider instance.
struct yaze::core::FeatureFlags::Flags::Overworld overworld
std::vector< std::string > tags
Definition project.h:39
std::string CreateOrGetLabel(const std::string &type, const std::string &key, const std::string &defaultValue)
Definition project.cc:1319
std::string GetLabel(const std::string &type, const std::string &key)
Definition project.cc:1306
void EditLabel(const std::string &type, const std::string &key, const std::string &newValue)
Definition project.cc:1288
bool LoadLabels(const std::string &filename)
Definition project.cc:1204
void SelectableLabelWithNameEdit(bool selected, const std::string &type, const std::string &key, const std::string &defaultValue)
Definition project.cc:1294
std::unordered_map< std::string, std::unordered_map< std::string, std::string > > labels_
Definition project.h:301
std::map< std::string, std::string > custom_keybindings
Definition project.h:75
std::vector< std::string > saved_layouts
Definition project.h:61
std::map< std::string, bool > editor_visibility
Definition project.h:77
std::vector< std::string > recent_files
Definition project.h:76
std::vector< std::string > favorite_models
Definition project.h:145
std::vector< std::string > model_chain
Definition project.h:146
Modern project structure with comprehensive settings consolidation.
Definition project.h:84
std::string rom_backup_folder
Definition project.h:93
absl::Status ResetToDefaults()
Definition project.cc:733
std::string custom_objects_folder
Definition project.h:102
absl::Status RepairProject()
Definition project.cc:794
std::string MakeStorageKey(absl::string_view suffix) const
Definition project.cc:210
struct yaze::project::YazeProject::MusicPersistence music_persistence
absl::StatusOr< std::string > SerializeToString() const
Definition project.cc:226
std::string zscream_project_file
Definition project.h:161
absl::Status ExportForZScream(const std::string &target_path)
Definition project.cc:700
ProjectMetadata metadata
Definition project.h:86
absl::Status ImportZScreamProject(const std::string &zscream_project_path)
Definition project.cc:677
absl::Status SaveAllSettings()
Definition project.cc:728
absl::Status ImportLabelsFromZScreamContent(const std::string &content)
Import labels from ZScream format content directly.
Definition project.cc:1428
std::string git_repository
Definition project.h:123
void InitializeResourceLabelProvider()
Initialize the global ResourceLabelProvider with this project's labels.
Definition project.cc:1451
std::string rom_filename
Definition project.h:92
absl::Status ParseFromString(const std::string &content)
Definition project.cc:413
std::vector< std::string > additional_roms
Definition project.h:94
std::string patches_folder
Definition project.h:99
absl::Status LoadFromYazeFormat(const std::string &project_path)
Definition project.cc:622
std::unordered_map< std::string, std::unordered_map< std::string, std::string > > resource_labels
Definition project.h:108
std::string GenerateProjectId() const
Definition project.cc:924
absl::Status Create(const std::string &project_name, const std::string &base_path)
Definition project.cc:99
std::string assets_folder
Definition project.h:98
absl::Status SaveToYazeFormat()
Definition project.cc:643
absl::Status LoadAllSettings()
Definition project.cc:722
std::string labels_filename
Definition project.h:100
std::vector< std::string > asm_sources
Definition project.h:120
std::string GetDisplayName() const
Definition project.cc:828
std::vector< std::string > GetMissingFiles() const
Definition project.cc:773
WorkspaceSettings workspace_settings
Definition project.h:106
std::string output_folder
Definition project.h:116
std::string asm_entry_point
Definition project.h:119
std::string GetRelativePath(const std::string &absolute_path) const
Definition project.cc:835
absl::Status InitializeEmbeddedLabels(const std::unordered_map< std::string, std::unordered_map< std::string, std::string > > &labels)
Definition project.cc:1334
absl::Status SaveAs(const std::string &new_path)
Definition project.cc:198
struct yaze::project::YazeProject::AgentSettings agent_settings
absl::Status ImportFromZScreamFormat(const std::string &project_path)
Definition project.cc:869
std::string GetAbsolutePath(const std::string &relative_path) const
Definition project.cc:853
std::string GetLabel(const std::string &resource_type, int id, const std::string &default_value="") const
Definition project.cc:1393
absl::Status Open(const std::string &project_path)
Definition project.cc:151
absl::Status ImportLabelsFromZScream(const std::string &filepath)
Import labels from a ZScream DefaultNames.txt file.
Definition project.cc:1408
std::string last_build_hash
Definition project.h:125
ProjectFormat format
Definition project.h:89
std::map< std::string, std::string > zscream_mappings
Definition project.h:162
std::string code_folder
Definition project.h:97
absl::Status Validate() const
Definition project.cc:738
core::FeatureFlags::Flags feature_flags
Definition project.h:105
std::vector< std::string > build_configurations
Definition project.h:117
std::string symbols_filename
Definition project.h:101