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 <algorithm>
4#include <cctype>
5#include <chrono>
6#include <filesystem>
7#include <fstream>
8#include <iomanip>
9#include <sstream>
10
11#include "absl/strings/match.h"
12#include "absl/strings/str_format.h"
13#include "absl/strings/str_join.h"
14#include "absl/strings/str_split.h"
15#include "app/gui/core/icons.h"
16#include "imgui/imgui.h"
17#include "util/file_util.h"
18#include "util/json.h"
19#include "util/log.h"
20#include "util/macro.h"
21#include "util/platform_paths.h"
22#include "yaze_config.h"
24
25#if defined(YAZE_WITH_Z3DK) && __has_include("z3dk_core/config.h")
26#include "z3dk_core/config.h"
27#endif
28
29#ifdef __EMSCRIPTEN__
31#endif
32
33// #ifdef YAZE_ENABLE_JSON_PROJECT_FORMAT
34// #include "nlohmann/json.hpp"
35// using json = nlohmann::json;
36// #endif
37
38namespace yaze {
39namespace project {
40
41namespace {
42std::string ToLowerCopy(std::string value) {
43 std::transform(
44 value.begin(), value.end(), value.begin(),
45 [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
46 return value;
47}
48
49// Helper functions for parsing key-value pairs
50std::pair<std::string, std::string> ParseKeyValue(const std::string& line) {
51 size_t eq_pos = line.find('=');
52 if (eq_pos == std::string::npos)
53 return {"", ""};
54
55 std::string key = line.substr(0, eq_pos);
56 std::string value = line.substr(eq_pos + 1);
57
58 // Trim whitespace
59 key.erase(0, key.find_first_not_of(" \t"));
60 key.erase(key.find_last_not_of(" \t") + 1);
61 value.erase(0, value.find_first_not_of(" \t"));
62 value.erase(value.find_last_not_of(" \t") + 1);
63
64 return {key, value};
65}
66
67bool ParseBool(const std::string& value) {
68 return value == "true" || value == "1" || value == "yes";
69}
70
71float ParseFloat(const std::string& value) {
72 try {
73 return std::stof(value);
74 } catch (...) {
75 return 0.0f;
76 }
77}
78
79std::vector<std::string> ParseStringList(const std::string& value) {
80 std::vector<std::string> result;
81 if (value.empty())
82 return result;
83
84 std::vector<std::string> parts = absl::StrSplit(value, ',');
85 for (const auto& part : parts) {
86 std::string trimmed = std::string(part);
87 trimmed.erase(0, trimmed.find_first_not_of(" \t"));
88 trimmed.erase(trimmed.find_last_not_of(" \t") + 1);
89 if (!trimmed.empty()) {
90 result.push_back(trimmed);
91 }
92 }
93 return result;
94}
95
96std::vector<uint16_t> ParseHexUintList(const std::string& value) {
97 std::vector<uint16_t> result;
98 if (value.empty()) {
99 return result;
100 }
101
102 auto parts = ParseStringList(value);
103 result.reserve(parts.size());
104 for (const auto& part : parts) {
105 std::string token = part;
106 if (token.rfind("0x", 0) == 0 || token.rfind("0X", 0) == 0) {
107 token = token.substr(2);
108 try {
109 result.push_back(static_cast<uint16_t>(std::stoul(token, nullptr, 16)));
110 } catch (...) {
111 // Ignore malformed entries
112 }
113 } else {
114 try {
115 result.push_back(static_cast<uint16_t>(std::stoul(token, nullptr, 10)));
116 } catch (...) {
117 // Ignore malformed entries
118 }
119 }
120 }
121 return result;
122}
123
124std::optional<uint32_t> ParseHexUint32(const std::string& value) {
125 if (value.empty()) {
126 return std::nullopt;
127 }
128 std::string token = value;
129 if (token.rfind("0x", 0) == 0 || token.rfind("0X", 0) == 0) {
130 token = token.substr(2);
131 try {
132 return static_cast<uint32_t>(std::stoul(token, nullptr, 16));
133 } catch (...) {
134 return std::nullopt;
135 }
136 }
137 try {
138 return static_cast<uint32_t>(std::stoul(token, nullptr, 10));
139 } catch (...) {
140 return std::nullopt;
141 }
142}
143
144std::string FormatHexUintList(const std::vector<uint16_t>& values) {
145 return absl::StrJoin(values, ",", [](std::string* out, uint16_t value) {
146 out->append(absl::StrFormat("0x%02X", value));
147 });
148}
149
150std::string FormatHexUint32(uint32_t value) {
151 return absl::StrFormat("0x%06X", value);
152}
153
154std::string SanitizeStorageKey(absl::string_view input) {
155 std::string key(input);
156 for (char& c : key) {
157 if (!std::isalnum(static_cast<unsigned char>(c))) {
158 c = '_';
159 }
160 }
161 if (key.empty()) {
162 key = "project";
163 }
164 return key;
165}
166
167std::pair<std::string, std::string> ParseDefineToken(const std::string& value) {
168 auto [key, parsed_value] = ParseKeyValue(value);
169 if (key.empty()) {
170 return {value, "1"};
171 }
172 return {key, parsed_value.empty() ? "1" : parsed_value};
173}
174
175std::string ResolveOptionalPath(const std::filesystem::path& base_dir,
176 const std::string& value) {
177 if (value.empty()) {
178 return {};
179 }
180 std::filesystem::path path(value);
181 if (path.is_absolute()) {
182 return path.lexically_normal().string();
183 }
184 return (base_dir / path).lexically_normal().string();
185}
186
187std::string BasenameLower(const std::string& path) {
188 return ToLowerCopy(std::filesystem::path(path).filename().string());
189}
190} // namespace
191
192std::string RomRoleToString(RomRole role) {
193 switch (role) {
194 case RomRole::kBase:
195 return "base";
197 return "patched";
199 return "release";
200 case RomRole::kDev:
201 default:
202 return "dev";
203 }
204}
205
206RomRole ParseRomRole(absl::string_view value) {
207 std::string lower = ToLowerCopy(std::string(value));
208 if (lower == "base") {
209 return RomRole::kBase;
210 }
211 if (lower == "patched") {
212 return RomRole::kPatched;
213 }
214 if (lower == "release") {
215 return RomRole::kRelease;
216 }
217 return RomRole::kDev;
218}
219
221 switch (policy) {
223 return "allow";
225 return "block";
227 default:
228 return "warn";
229 }
230}
231
232RomWritePolicy ParseRomWritePolicy(absl::string_view value) {
233 std::string lower = ToLowerCopy(std::string(value));
234 if (lower == "allow") {
236 }
237 if (lower == "block") {
239 }
241}
242
243// YazeProject Implementation
244absl::Status YazeProject::Create(const std::string& project_name,
245 const std::string& base_path) {
246 name = project_name;
247 filepath = base_path + "/" + project_name + ".yaze";
248
249 // Initialize metadata
250 auto now = std::chrono::system_clock::now();
251 auto time_t = std::chrono::system_clock::to_time_t(now);
252 std::stringstream ss;
253 ss << std::put_time(std::localtime(&time_t), "%Y-%m-%d %H:%M:%S");
254
255 metadata.created_date = ss.str();
256 metadata.last_modified = ss.str();
258 metadata.version = "2.0";
259 metadata.created_by = "YAZE";
261
263
264#ifndef __EMSCRIPTEN__
265 // Create project directory structure
266 std::filesystem::path project_dir(base_path + "/" + project_name);
267 std::filesystem::create_directories(project_dir);
268 std::filesystem::create_directories(project_dir / "code");
269 std::filesystem::create_directories(project_dir / "assets");
270 std::filesystem::create_directories(project_dir / "patches");
271 std::filesystem::create_directories(project_dir / "backups");
272 std::filesystem::create_directories(project_dir / "output");
273
274 // Set folder paths
275 code_folder = (project_dir / "code").string();
276 assets_folder = (project_dir / "assets").string();
277 patches_folder = (project_dir / "patches").string();
278 rom_backup_folder = (project_dir / "backups").string();
279 output_folder = (project_dir / "output").string();
280 labels_filename = (project_dir / "labels.txt").string();
281 symbols_filename = (project_dir / "symbols.txt").string();
282#else
283 // WASM: keep paths relative; persistence handled by WasmStorage/IDBFS
284 code_folder = "code";
285 assets_folder = "assets";
286 patches_folder = "patches";
287 rom_backup_folder = "backups";
288 output_folder = "output";
289 labels_filename = "labels.txt";
290 symbols_filename = "symbols.txt";
291#endif
292
293 return Save();
294}
295
296// static
297std::string YazeProject::ResolveBundleRoot(const std::string& path) {
298 if (path.empty()) {
299 return {};
300 }
301
302 // Walk up the path hierarchy looking for a directory whose filename
303 // (extension) is ".yazeproj". The first match from the leaf upward wins.
304 std::error_code ec;
305 auto current = std::filesystem::path(path).lexically_normal();
306
307 for (; !current.empty(); current = current.parent_path()) {
308 if (current.extension() == ".yazeproj") {
309 // Must be an existing directory to count as a valid bundle root.
310 if (std::filesystem::is_directory(current, ec) && !ec) {
311 return current.string();
312 }
313 }
314 // Guard against infinite loop at the filesystem root.
315 if (current == current.parent_path()) {
316 break;
317 }
318 }
319
320 return {};
321}
322
323absl::Status YazeProject::Open(const std::string& project_path) {
324 // Resolve bundle root: if the user opened a file *inside* a .yazeproj
325 // bundle, normalize to the bundle root directory so the existing
326 // .yazeproj handling takes over.
327 std::string resolved_path = project_path;
328 const std::string bundle_root = ResolveBundleRoot(project_path);
329 if (!bundle_root.empty() && bundle_root != project_path) {
330 // The user pointed at a file inside a bundle; redirect to the root.
331 resolved_path = bundle_root;
332 }
333
334 filepath = resolved_path;
335
336#ifdef __EMSCRIPTEN__
337 // Prefer persistent storage in WASM builds
338 auto storage_key = MakeStorageKey("project");
339 auto storage_or = platform::WasmStorage::LoadProject(storage_key);
340 if (storage_or.ok()) {
341 return ParseFromString(storage_or.value());
342 }
343#endif
344
345 // Determine format and load accordingly
346 absl::Status load_status;
347 if (resolved_path.ends_with(".yazeproj")) {
349
350 const std::filesystem::path bundle_path(resolved_path);
351 std::error_code ec;
352 if (!std::filesystem::exists(bundle_path, ec) || ec ||
353 !std::filesystem::is_directory(bundle_path, ec) || ec) {
354 return absl::InvalidArgumentError(
355 absl::StrFormat("Project bundle does not exist: %s", resolved_path));
356 }
357
358 // Bundle convention: store the actual project config at the root so both
359 // desktop and iOS can open the same `.yazeproj` directory.
360 const std::filesystem::path project_file = bundle_path / "project.yaze";
361 filepath = project_file.string();
362
363 if (!std::filesystem::exists(project_file, ec) || ec) {
364 // Create a minimal portable project file for the bundle if missing.
366 name = bundle_path.stem().string();
367
368 // Initialize metadata timestamps (Create() normally does this).
369 auto now = std::chrono::system_clock::now();
370 auto time_t = std::chrono::system_clock::to_time_t(now);
371 std::stringstream ss;
372 ss << std::put_time(std::localtime(&time_t), "%Y-%m-%d %H:%M:%S");
373 if (metadata.created_date.empty()) {
374 metadata.created_date = ss.str();
375 }
376 metadata.last_modified = ss.str();
377 if (metadata.yaze_version.empty()) {
379 }
380 if (metadata.version.empty()) {
381 metadata.version = "2.0";
382 }
383 if (metadata.created_by.empty()) {
384 metadata.created_by = "YAZE";
385 }
386 if (metadata.project_id.empty()) {
388 }
389
390 // Bundle layout defaults (paths stored as absolute; serializer writes
391 // relative values for portability).
392 const std::filesystem::path rom_candidate = bundle_path / "rom";
393 // Always set the expected bundle ROM path even if the file is not yet
394 // present on disk (e.g. still downloading from iCloud). The load
395 // attempt in LoadProjectWithRom() handles the missing-file case without
396 // corrupting the project by saving a temporary path.
397 rom_filename = rom_candidate.string();
398
399 const std::filesystem::path project_dir = bundle_path / "project";
400 const std::filesystem::path code_dir = bundle_path / "code";
401 if (std::filesystem::exists(project_dir, ec) &&
402 std::filesystem::is_directory(project_dir, ec) && !ec) {
403 code_folder = project_dir.string();
404 } else if (std::filesystem::exists(code_dir, ec) &&
405 std::filesystem::is_directory(code_dir, ec) && !ec) {
406 code_folder = code_dir.string();
407 }
408
409 assets_folder = (bundle_path / "assets").string();
410 patches_folder = (bundle_path / "patches").string();
411 rom_backup_folder = (bundle_path / "backups").string();
412 output_folder = (bundle_path / "output").string();
413 labels_filename = (bundle_path / "labels.txt").string();
414 symbols_filename = (bundle_path / "symbols.txt").string();
415
416 load_status = SaveToYazeFormat();
417 } else {
418 load_status = LoadFromYazeFormat(project_file.string());
419 }
420 } else if (resolved_path.ends_with(".yaze")) {
422
423 // Try to detect if it's JSON format by peeking at first character
424 std::ifstream file(resolved_path);
425 if (file.is_open()) {
426 std::stringstream buffer;
427 buffer << file.rdbuf();
428 std::string content = buffer.str();
429
430#ifdef YAZE_ENABLE_JSON_PROJECT_FORMAT
431 if (!content.empty() && content.front() == '{') {
432 LOG_DEBUG("Project", "Detected JSON format project file");
433 load_status = LoadFromJsonFormat(resolved_path);
434 } else {
435 load_status = ParseFromString(content);
436 }
437#else
438 load_status = ParseFromString(content);
439#endif
440 } else {
441 return absl::InvalidArgumentError(
442 absl::StrFormat("Cannot open project file: %s", resolved_path));
443 }
444 } else if (resolved_path.ends_with(".zsproj")) {
446 load_status = ImportFromZScreamFormat(resolved_path);
447 } else {
448 return absl::InvalidArgumentError("Unsupported project file format");
449 }
450
451 if (!load_status.ok()) {
452 return load_status;
453 }
454
455 // Normalize project-relative paths so downstream code never depends on the
456 // process working directory (important for iOS and portable bundles).
458
459 // Auto-load z3dk project config if discoverable.
461
462 // Auto-load hack manifest if configured or discoverable
464
465 return absl::OkStatus();
466}
467
468absl::Status YazeProject::Save() {
469 return SaveToYazeFormat();
470}
471
472absl::Status YazeProject::SaveAs(const std::string& new_path) {
473 std::string old_filepath = filepath;
474 filepath = new_path;
475
476 auto status = Save();
477 if (!status.ok()) {
478 filepath = old_filepath; // Restore on failure
479 }
480
481 return status;
482}
483
484std::string YazeProject::MakeStorageKey(absl::string_view suffix) const {
485 std::string base;
486 if (!metadata.project_id.empty()) {
487 base = metadata.project_id;
488 } else if (!name.empty()) {
489 base = name;
490 } else if (!filepath.empty()) {
491 base = std::filesystem::path(filepath).stem().string();
492 }
493 base = SanitizeStorageKey(base);
494 if (suffix.empty()) {
495 return base;
496 }
497 return absl::StrFormat("%s_%s", base, suffix);
498}
499
500absl::StatusOr<std::string> YazeProject::SerializeToString() const {
501 std::ostringstream file;
502
503 // Write header comment
504 file << "# yaze Project File\n";
505 file << "# Format Version: 2.0\n";
506 file << "# Generated by YAZE " << metadata.yaze_version << "\n";
507 file << "# Last Modified: " << metadata.last_modified << "\n\n";
508
509 // Project section
510 file << "[project]\n";
511 file << "name=" << name << "\n";
512 file << "description=" << metadata.description << "\n";
513 file << "author=" << metadata.author << "\n";
514 file << "license=" << metadata.license << "\n";
515 file << "version=" << metadata.version << "\n";
516 file << "created_date=" << metadata.created_date << "\n";
517 file << "last_modified=" << metadata.last_modified << "\n";
518 file << "yaze_version=" << metadata.yaze_version << "\n";
519 file << "created_by=" << metadata.created_by << "\n";
520 file << "project_id=" << metadata.project_id << "\n";
521 file << "tags=" << absl::StrJoin(metadata.tags, ",") << "\n\n";
522
523 // Files section
524 file << "[files]\n";
525 file << "rom_filename=" << GetRelativePath(rom_filename) << "\n";
526 file << "rom_backup_folder=" << GetRelativePath(rom_backup_folder) << "\n";
527 file << "code_folder=" << GetRelativePath(code_folder) << "\n";
528 file << "assets_folder=" << GetRelativePath(assets_folder) << "\n";
529 file << "patches_folder=" << GetRelativePath(patches_folder) << "\n";
530 file << "labels_filename=" << GetRelativePath(labels_filename) << "\n";
531 file << "symbols_filename=" << GetRelativePath(symbols_filename) << "\n";
532 file << "output_folder=" << GetRelativePath(output_folder) << "\n";
533 file << "custom_objects_folder=" << GetRelativePath(custom_objects_folder)
534 << "\n";
535 file << "hack_manifest_file=" << GetRelativePath(hack_manifest_file) << "\n";
536 file << "additional_roms=" << absl::StrJoin(additional_roms, ",") << "\n\n";
537
538 // ROM metadata section
539 file << "[rom]\n";
540 file << "role=" << RomRoleToString(rom_metadata.role) << "\n";
541 file << "expected_hash=" << rom_metadata.expected_hash << "\n";
542 file << "write_policy=" << RomWritePolicyToString(rom_metadata.write_policy)
543 << "\n\n";
544
545 // Feature flags section
546 file << "[feature_flags]\n";
547 file << "load_custom_overworld="
548 << (feature_flags.overworld.kLoadCustomOverworld ? "true" : "false")
549 << "\n";
550 file << "apply_zs_custom_overworld_asm="
552 : "false")
553 << "\n";
554 file << "save_dungeon_maps="
555 << (feature_flags.kSaveDungeonMaps ? "true" : "false") << "\n";
556 file << "save_overworld_maps="
557 << (feature_flags.overworld.kSaveOverworldMaps ? "true" : "false")
558 << "\n";
559 file << "save_overworld_entrances="
560 << (feature_flags.overworld.kSaveOverworldEntrances ? "true" : "false")
561 << "\n";
562 file << "save_overworld_exits="
563 << (feature_flags.overworld.kSaveOverworldExits ? "true" : "false")
564 << "\n";
565 file << "save_overworld_items="
566 << (feature_flags.overworld.kSaveOverworldItems ? "true" : "false")
567 << "\n";
568 file << "save_overworld_properties="
569 << (feature_flags.overworld.kSaveOverworldProperties ? "true" : "false")
570 << "\n";
571 file << "save_dungeon_objects="
572 << (feature_flags.dungeon.kSaveObjects ? "true" : "false") << "\n";
573 file << "save_dungeon_sprites="
574 << (feature_flags.dungeon.kSaveSprites ? "true" : "false") << "\n";
575 file << "save_dungeon_room_headers="
576 << (feature_flags.dungeon.kSaveRoomHeaders ? "true" : "false") << "\n";
577 file << "save_dungeon_torches="
578 << (feature_flags.dungeon.kSaveTorches ? "true" : "false") << "\n";
579 file << "save_dungeon_pits="
580 << (feature_flags.dungeon.kSavePits ? "true" : "false") << "\n";
581 file << "save_dungeon_blocks="
582 << (feature_flags.dungeon.kSaveBlocks ? "true" : "false") << "\n";
583 file << "save_dungeon_collision="
584 << (feature_flags.dungeon.kSaveCollision ? "true" : "false") << "\n";
585 file << "save_dungeon_chests="
586 << (feature_flags.dungeon.kSaveChests ? "true" : "false") << "\n";
587 file << "save_dungeon_pot_items="
588 << (feature_flags.dungeon.kSavePotItems ? "true" : "false") << "\n";
589 file << "save_dungeon_palettes="
590 << (feature_flags.dungeon.kSavePalettes ? "true" : "false") << "\n";
591 file << "save_graphics_sheet="
592 << (feature_flags.kSaveGraphicsSheet ? "true" : "false") << "\n";
593 file << "save_all_palettes="
594 << (feature_flags.kSaveAllPalettes ? "true" : "false") << "\n";
595 file << "save_gfx_groups="
596 << (feature_flags.kSaveGfxGroups ? "true" : "false") << "\n";
597 file << "save_messages=" << (feature_flags.kSaveMessages ? "true" : "false")
598 << "\n";
599 file << "enable_custom_objects="
600 << (feature_flags.kEnableCustomObjects ? "true" : "false") << "\n\n";
601
602 // Workspace settings section
603 file << "[workspace]\n";
604 file << "font_global_scale=" << workspace_settings.font_global_scale << "\n";
605 file << "dark_mode=" << (workspace_settings.dark_mode ? "true" : "false")
606 << "\n";
607 file << "ui_theme=" << workspace_settings.ui_theme << "\n";
608 file << "autosave_enabled="
609 << (workspace_settings.autosave_enabled ? "true" : "false") << "\n";
610 file << "autosave_interval_secs=" << workspace_settings.autosave_interval_secs
611 << "\n";
612 file << "backup_on_save="
613 << (workspace_settings.backup_on_save ? "true" : "false") << "\n";
614 file << "backup_retention_count=" << workspace_settings.backup_retention_count
615 << "\n";
616 file << "backup_keep_daily="
617 << (workspace_settings.backup_keep_daily ? "true" : "false") << "\n";
618 file << "backup_keep_daily_days=" << workspace_settings.backup_keep_daily_days
619 << "\n";
620 file << "show_grid=" << (workspace_settings.show_grid ? "true" : "false")
621 << "\n";
622 file << "show_collision="
623 << (workspace_settings.show_collision ? "true" : "false") << "\n";
624 file << "prefer_hmagic_names="
625 << (workspace_settings.prefer_hmagic_names ? "true" : "false") << "\n";
626 file << "last_layout_preset=" << workspace_settings.last_layout_preset
627 << "\n";
628 file << "saved_layouts="
629 << absl::StrJoin(workspace_settings.saved_layouts, ",") << "\n";
630 file << "recent_files=" << absl::StrJoin(workspace_settings.recent_files, ",")
631 << "\n\n";
632
633 // Dungeon overlay settings section
634 auto track_tiles = dungeon_overlay.track_tiles;
635 if (track_tiles.empty()) {
636 for (uint16_t tile = 0xB0; tile <= 0xBE; ++tile) {
637 track_tiles.push_back(tile);
638 }
639 }
640 auto track_stop_tiles = dungeon_overlay.track_stop_tiles;
641 if (track_stop_tiles.empty()) {
642 track_stop_tiles = {0xB7, 0xB8, 0xB9, 0xBA};
643 }
644 auto track_switch_tiles = dungeon_overlay.track_switch_tiles;
645 if (track_switch_tiles.empty()) {
646 track_switch_tiles = {0xD0, 0xD1, 0xD2, 0xD3};
647 }
648 auto track_object_ids = dungeon_overlay.track_object_ids;
649 if (track_object_ids.empty()) {
650 track_object_ids = {0x31};
651 }
652 auto minecart_sprite_ids = dungeon_overlay.minecart_sprite_ids;
653 if (minecart_sprite_ids.empty()) {
654 minecart_sprite_ids = {0xA3};
655 }
656 file << "[dungeon_overlay]\n";
657 file << "track_tiles=" << FormatHexUintList(track_tiles) << "\n";
658 file << "track_stop_tiles=" << FormatHexUintList(track_stop_tiles) << "\n";
659 file << "track_switch_tiles=" << FormatHexUintList(track_switch_tiles)
660 << "\n";
661 file << "track_object_ids=" << FormatHexUintList(track_object_ids) << "\n";
662 file << "minecart_sprite_ids=" << FormatHexUintList(minecart_sprite_ids)
663 << "\n\n";
664
665 if (!rom_address_overrides.addresses.empty()) {
666 file << "[rom_addresses]\n";
667 for (const auto& [key, value] : rom_address_overrides.addresses) {
668 file << key << "=" << FormatHexUint32(value) << "\n";
669 }
670 file << "\n";
671 }
672
673 if (!custom_object_files.empty()) {
674 file << "[custom_objects]\n";
675 for (const auto& [object_id, files] : custom_object_files) {
676 file << absl::StrFormat("object_0x%X", object_id) << "="
677 << absl::StrJoin(files, ",") << "\n";
678 }
679 file << "\n";
680 }
681
682 // AI Agent settings section
683 file << "[agent_settings]\n";
684 file << "ai_provider=" << agent_settings.ai_provider << "\n";
685 file << "ai_model=" << agent_settings.ai_model << "\n";
686 file << "ollama_host=" << agent_settings.ollama_host << "\n";
687 file << "gemini_api_key=" << agent_settings.gemini_api_key << "\n";
688 file << "custom_system_prompt="
690 file << "use_custom_prompt="
691 << (agent_settings.use_custom_prompt ? "true" : "false") << "\n";
692 file << "show_reasoning="
693 << (agent_settings.show_reasoning ? "true" : "false") << "\n";
694 file << "verbose=" << (agent_settings.verbose ? "true" : "false") << "\n";
695 file << "max_tool_iterations=" << agent_settings.max_tool_iterations << "\n";
696 file << "max_retry_attempts=" << agent_settings.max_retry_attempts << "\n";
697 file << "temperature=" << agent_settings.temperature << "\n";
698 file << "top_p=" << agent_settings.top_p << "\n";
699 file << "max_output_tokens=" << agent_settings.max_output_tokens << "\n";
700 file << "stream_responses="
701 << (agent_settings.stream_responses ? "true" : "false") << "\n";
702 file << "favorite_models="
703 << absl::StrJoin(agent_settings.favorite_models, ",") << "\n";
704 file << "model_chain=" << absl::StrJoin(agent_settings.model_chain, ",")
705 << "\n";
706 file << "chain_mode=" << agent_settings.chain_mode << "\n";
707 file << "enable_tool_resources="
708 << (agent_settings.enable_tool_resources ? "true" : "false") << "\n";
709 file << "enable_tool_dungeon="
710 << (agent_settings.enable_tool_dungeon ? "true" : "false") << "\n";
711 file << "enable_tool_overworld="
712 << (agent_settings.enable_tool_overworld ? "true" : "false") << "\n";
713 file << "enable_tool_messages="
714 << (agent_settings.enable_tool_messages ? "true" : "false") << "\n";
715 file << "enable_tool_dialogue="
716 << (agent_settings.enable_tool_dialogue ? "true" : "false") << "\n";
717 file << "enable_tool_gui="
718 << (agent_settings.enable_tool_gui ? "true" : "false") << "\n";
719 file << "enable_tool_music="
720 << (agent_settings.enable_tool_music ? "true" : "false") << "\n";
721 file << "enable_tool_sprite="
722 << (agent_settings.enable_tool_sprite ? "true" : "false") << "\n";
723 file << "enable_tool_emulator="
724 << (agent_settings.enable_tool_emulator ? "true" : "false") << "\n";
725 file << "enable_tool_memory_inspector="
726 << (agent_settings.enable_tool_memory_inspector ? "true" : "false")
727 << "\n";
728 file << "builder_blueprint_path=" << agent_settings.builder_blueprint_path
729 << "\n\n";
730
731 // Custom keybindings section
733 file << "[keybindings]\n";
734 for (const auto& [key, value] : workspace_settings.custom_keybindings) {
735 file << key << "=" << value << "\n";
736 }
737 file << "\n";
738 }
739
740 // Editor visibility section
742 file << "[editor_visibility]\n";
743 for (const auto& [key, value] : workspace_settings.editor_visibility) {
744 file << key << "=" << (value ? "true" : "false") << "\n";
745 }
746 file << "\n";
747 }
748
749 // Resource labels sections
750 for (const auto& [type, labels] : resource_labels) {
751 if (!labels.empty()) {
752 file << "[labels_" << type << "]\n";
753 for (const auto& [key, value] : labels) {
754 file << key << "=" << value << "\n";
755 }
756 file << "\n";
757 }
758 }
759
760 // Build settings section
761 file << "[build]\n";
762 file << "build_script=" << build_script << "\n";
763 file << "output_folder=" << GetRelativePath(output_folder) << "\n";
764 file << "git_repository=" << git_repository << "\n";
765 file << "track_changes=" << (track_changes ? "true" : "false") << "\n";
766 file << "build_configurations=" << absl::StrJoin(build_configurations, ",")
767 << "\n";
768 file << "build_target=" << build_target << "\n";
769 file << "asm_entry_point=" << asm_entry_point << "\n";
770 file << "asm_sources=" << absl::StrJoin(asm_sources, ",") << "\n";
771 file << "last_build_hash=" << last_build_hash << "\n";
772 file << "build_number=" << build_number << "\n\n";
773
774 // Music persistence section (for WASM/offline state)
775 file << "[music]\n";
776 file << "persist_custom_music="
777 << (music_persistence.persist_custom_music ? "true" : "false") << "\n";
778 file << "storage_key=" << music_persistence.storage_key << "\n";
779 file << "last_saved_at=" << music_persistence.last_saved_at << "\n\n";
780
781 // ZScream compatibility section
782 if (!zscream_project_file.empty()) {
783 file << "[zscream_compatibility]\n";
784 file << "original_project_file=" << zscream_project_file << "\n";
785 for (const auto& [key, value] : zscream_mappings) {
786 file << key << "=" << value << "\n";
787 }
788 file << "\n";
789 }
790
791 file << "# End of YAZE Project File\n";
792 return file.str();
793}
794
795absl::Status YazeProject::ParseFromString(const std::string& content) {
796 std::istringstream stream(content);
797 std::string line;
798 std::string current_section;
799
800 while (std::getline(stream, line)) {
801 if (line.empty() || line[0] == '#')
802 continue;
803
804 if (line.front() == '[' && line.back() == ']') {
805 current_section = line.substr(1, line.length() - 2);
806 continue;
807 }
808
809 auto [key, value] = ParseKeyValue(line);
810 if (key.empty())
811 continue;
812
813 if (current_section == "project") {
814 if (key == "name")
815 name = value;
816 else if (key == "description")
817 metadata.description = value;
818 else if (key == "author")
819 metadata.author = value;
820 else if (key == "license")
821 metadata.license = value;
822 else if (key == "version")
823 metadata.version = value;
824 else if (key == "created_date")
825 metadata.created_date = value;
826 else if (key == "last_modified")
827 metadata.last_modified = value;
828 else if (key == "yaze_version")
829 metadata.yaze_version = value;
830 else if (key == "created_by")
831 metadata.created_by = value;
832 else if (key == "tags")
833 metadata.tags = ParseStringList(value);
834 else if (key == "project_id")
835 metadata.project_id = value;
836 } else if (current_section == "files") {
837 if (key == "rom_filename")
838 rom_filename = value;
839 else if (key == "rom_backup_folder")
840 rom_backup_folder = value;
841 else if (key == "code_folder")
842 code_folder = value;
843 else if (key == "assets_folder")
844 assets_folder = value;
845 else if (key == "patches_folder")
846 patches_folder = value;
847 else if (key == "labels_filename")
848 labels_filename = value;
849 else if (key == "symbols_filename")
850 symbols_filename = value;
851 else if (key == "output_folder")
852 output_folder = value;
853 else if (key == "custom_objects_folder")
854 custom_objects_folder = value;
855 else if (key == "hack_manifest_file")
856 hack_manifest_file = value;
857 else if (key == "additional_roms")
858 additional_roms = ParseStringList(value);
859 } else if (current_section == "rom") {
860 if (key == "role")
862 else if (key == "expected_hash")
864 else if (key == "write_policy")
866 } else if (current_section == "feature_flags") {
867 if (key == "load_custom_overworld")
869 else if (key == "apply_zs_custom_overworld_asm")
871 else if (key == "save_dungeon_maps")
872 feature_flags.kSaveDungeonMaps = ParseBool(value);
873 else if (key == "save_overworld_maps")
874 feature_flags.overworld.kSaveOverworldMaps = ParseBool(value);
875 else if (key == "save_overworld_entrances")
877 else if (key == "save_overworld_exits")
879 else if (key == "save_overworld_items")
881 else if (key == "save_overworld_properties")
883 else if (key == "save_dungeon_objects")
884 feature_flags.dungeon.kSaveObjects = ParseBool(value);
885 else if (key == "save_dungeon_sprites")
886 feature_flags.dungeon.kSaveSprites = ParseBool(value);
887 else if (key == "save_dungeon_room_headers")
888 feature_flags.dungeon.kSaveRoomHeaders = ParseBool(value);
889 else if (key == "save_dungeon_torches")
890 feature_flags.dungeon.kSaveTorches = ParseBool(value);
891 else if (key == "save_dungeon_pits")
892 feature_flags.dungeon.kSavePits = ParseBool(value);
893 else if (key == "save_dungeon_blocks")
894 feature_flags.dungeon.kSaveBlocks = ParseBool(value);
895 else if (key == "save_dungeon_collision")
896 feature_flags.dungeon.kSaveCollision = ParseBool(value);
897 else if (key == "save_dungeon_chests")
898 feature_flags.dungeon.kSaveChests = ParseBool(value);
899 else if (key == "save_dungeon_pot_items")
900 feature_flags.dungeon.kSavePotItems = ParseBool(value);
901 else if (key == "save_dungeon_palettes")
902 feature_flags.dungeon.kSavePalettes = ParseBool(value);
903 else if (key == "save_graphics_sheet")
904 feature_flags.kSaveGraphicsSheet = ParseBool(value);
905 else if (key == "save_all_palettes")
906 feature_flags.kSaveAllPalettes = ParseBool(value);
907 else if (key == "save_gfx_groups")
908 feature_flags.kSaveGfxGroups = ParseBool(value);
909 else if (key == "save_messages")
910 feature_flags.kSaveMessages = ParseBool(value);
911 else if (key == "enable_custom_objects")
912 feature_flags.kEnableCustomObjects = ParseBool(value);
913 } else if (current_section == "workspace") {
914 if (key == "font_global_scale")
915 workspace_settings.font_global_scale = ParseFloat(value);
916 else if (key == "dark_mode")
917 workspace_settings.dark_mode = ParseBool(value);
918 else if (key == "ui_theme")
920 else if (key == "autosave_enabled")
921 workspace_settings.autosave_enabled = ParseBool(value);
922 else if (key == "autosave_interval_secs")
923 workspace_settings.autosave_interval_secs = ParseFloat(value);
924 else if (key == "backup_on_save")
925 workspace_settings.backup_on_save = ParseBool(value);
926 else if (key == "backup_retention_count")
928 else if (key == "backup_keep_daily")
929 workspace_settings.backup_keep_daily = ParseBool(value);
930 else if (key == "backup_keep_daily_days")
932 else if (key == "show_grid")
933 workspace_settings.show_grid = ParseBool(value);
934 else if (key == "show_collision")
935 workspace_settings.show_collision = ParseBool(value);
936 else if (key == "prefer_hmagic_names")
937 workspace_settings.prefer_hmagic_names = ParseBool(value);
938 else if (key == "last_layout_preset")
940 else if (key == "saved_layouts")
941 workspace_settings.saved_layouts = ParseStringList(value);
942 else if (key == "recent_files")
943 workspace_settings.recent_files = ParseStringList(value);
944 } else if (current_section == "dungeon_overlay") {
945 if (key == "track_tiles")
946 dungeon_overlay.track_tiles = ParseHexUintList(value);
947 else if (key == "track_stop_tiles")
948 dungeon_overlay.track_stop_tiles = ParseHexUintList(value);
949 else if (key == "track_switch_tiles")
950 dungeon_overlay.track_switch_tiles = ParseHexUintList(value);
951 else if (key == "track_object_ids")
952 dungeon_overlay.track_object_ids = ParseHexUintList(value);
953 else if (key == "minecart_sprite_ids")
954 dungeon_overlay.minecart_sprite_ids = ParseHexUintList(value);
955 } else if (current_section == "rom_addresses") {
956 auto parsed = ParseHexUint32(value);
957 if (parsed.has_value()) {
958 rom_address_overrides.addresses[key] = *parsed;
959 }
960 } else if (current_section == "custom_objects") {
961 std::string id_token = key;
962 if (absl::StartsWith(id_token, "object_")) {
963 id_token = id_token.substr(7);
964 }
965 auto parsed = ParseHexUint32(id_token);
966 if (parsed.has_value()) {
967 custom_object_files[static_cast<int>(*parsed)] = ParseStringList(value);
968 }
969 } else if (current_section == "agent_settings") {
970 if (key == "ai_provider")
972 else if (key == "ai_model")
973 agent_settings.ai_model = value;
974 else if (key == "ollama_host")
976 else if (key == "gemini_api_key")
978 else if (key == "custom_system_prompt")
980 else if (key == "use_custom_prompt")
981 agent_settings.use_custom_prompt = ParseBool(value);
982 else if (key == "show_reasoning")
983 agent_settings.show_reasoning = ParseBool(value);
984 else if (key == "verbose")
985 agent_settings.verbose = ParseBool(value);
986 else if (key == "max_tool_iterations")
987 agent_settings.max_tool_iterations = std::stoi(value);
988 else if (key == "max_retry_attempts")
989 agent_settings.max_retry_attempts = std::stoi(value);
990 else if (key == "temperature")
991 agent_settings.temperature = ParseFloat(value);
992 else if (key == "top_p")
993 agent_settings.top_p = ParseFloat(value);
994 else if (key == "max_output_tokens")
995 agent_settings.max_output_tokens = std::stoi(value);
996 else if (key == "stream_responses")
997 agent_settings.stream_responses = ParseBool(value);
998 else if (key == "favorite_models")
999 agent_settings.favorite_models = ParseStringList(value);
1000 else if (key == "model_chain")
1001 agent_settings.model_chain = ParseStringList(value);
1002 else if (key == "chain_mode")
1003 agent_settings.chain_mode = std::stoi(value);
1004 else if (key == "enable_tool_resources")
1005 agent_settings.enable_tool_resources = ParseBool(value);
1006 else if (key == "enable_tool_dungeon")
1007 agent_settings.enable_tool_dungeon = ParseBool(value);
1008 else if (key == "enable_tool_overworld")
1009 agent_settings.enable_tool_overworld = ParseBool(value);
1010 else if (key == "enable_tool_messages")
1011 agent_settings.enable_tool_messages = ParseBool(value);
1012 else if (key == "enable_tool_dialogue")
1013 agent_settings.enable_tool_dialogue = ParseBool(value);
1014 else if (key == "enable_tool_gui")
1015 agent_settings.enable_tool_gui = ParseBool(value);
1016 else if (key == "enable_tool_music")
1017 agent_settings.enable_tool_music = ParseBool(value);
1018 else if (key == "enable_tool_sprite")
1019 agent_settings.enable_tool_sprite = ParseBool(value);
1020 else if (key == "enable_tool_emulator")
1021 agent_settings.enable_tool_emulator = ParseBool(value);
1022 else if (key == "enable_tool_memory_inspector")
1024 else if (key == "builder_blueprint_path")
1026 } else if (current_section == "build") {
1027 if (key == "build_script")
1028 build_script = value;
1029 else if (key == "output_folder")
1030 output_folder = value;
1031 else if (key == "git_repository")
1032 git_repository = value;
1033 else if (key == "track_changes")
1034 track_changes = ParseBool(value);
1035 else if (key == "build_configurations")
1036 build_configurations = ParseStringList(value);
1037 else if (key == "build_target")
1038 build_target = value;
1039 else if (key == "asm_entry_point")
1040 asm_entry_point = value;
1041 else if (key == "asm_sources")
1042 asm_sources = ParseStringList(value);
1043 else if (key == "last_build_hash")
1044 last_build_hash = value;
1045 else if (key == "build_number")
1046 build_number = std::stoi(value);
1047 } else if (current_section.rfind("labels_", 0) == 0) {
1048 std::string label_type = current_section.substr(7);
1049 resource_labels[label_type][key] = value;
1050 } else if (current_section == "keybindings") {
1052 } else if (current_section == "editor_visibility") {
1053 workspace_settings.editor_visibility[key] = ParseBool(value);
1054 } else if (current_section == "zscream_compatibility") {
1055 if (key == "original_project_file")
1056 zscream_project_file = value;
1057 else
1058 zscream_mappings[key] = value;
1059 } else if (current_section == "music") {
1060 if (key == "persist_custom_music")
1061 music_persistence.persist_custom_music = ParseBool(value);
1062 else if (key == "storage_key")
1064 else if (key == "last_saved_at")
1066 }
1067 }
1068
1069 if (metadata.project_id.empty()) {
1071 }
1072 if (metadata.created_by.empty()) {
1073 metadata.created_by = "YAZE";
1074 }
1075 if (music_persistence.storage_key.empty()) {
1077 }
1078
1079 return absl::OkStatus();
1080}
1081
1082absl::Status YazeProject::LoadFromYazeFormat(const std::string& project_path) {
1083#ifdef __EMSCRIPTEN__
1084 auto storage_key = MakeStorageKey("project");
1085 auto storage_or = platform::WasmStorage::LoadProject(storage_key);
1086 if (storage_or.ok()) {
1087 return ParseFromString(storage_or.value());
1088 }
1089#endif // __EMSCRIPTEN__
1090
1091 std::ifstream file(project_path);
1092 if (!file.is_open()) {
1093 return absl::InvalidArgumentError(
1094 absl::StrFormat("Cannot open project file: %s", project_path));
1095 }
1096
1097 std::stringstream buffer;
1098 buffer << file.rdbuf();
1099 file.close();
1100 return ParseFromString(buffer.str());
1101}
1102
1104 // Update last modified timestamp
1105 auto now = std::chrono::system_clock::now();
1106 auto time_t = std::chrono::system_clock::to_time_t(now);
1107 std::stringstream ss;
1108 ss << std::put_time(std::localtime(&time_t), "%Y-%m-%d %H:%M:%S");
1109 metadata.last_modified = ss.str();
1110 if (music_persistence.storage_key.empty()) {
1112 }
1113
1114 // Ensure we serialize clean relative paths even if the user edited fields
1115 // into relative form (and avoid relying on cwd when opening later).
1117
1118 ASSIGN_OR_RETURN(auto serialized, SerializeToString());
1119
1120#ifdef __EMSCRIPTEN__
1121 auto storage_status =
1122 platform::WasmStorage::SaveProject(MakeStorageKey("project"), serialized);
1123 if (!storage_status.ok()) {
1124 return storage_status;
1125 }
1126#else
1127 if (!filepath.empty()) {
1128 std::ofstream file(filepath);
1129 if (!file.is_open()) {
1130 return absl::InvalidArgumentError(
1131 absl::StrFormat("Cannot create project file: %s", filepath));
1132 }
1133 file << serialized;
1134 file.close();
1135 }
1136#endif
1137
1138 return absl::OkStatus();
1139}
1140
1142 const std::string& zscream_project_path) {
1143 // Basic ZScream project import (to be expanded based on ZScream format)
1144 zscream_project_file = zscream_project_path;
1146
1147 // Extract project name from path
1148 std::filesystem::path zs_path(zscream_project_path);
1149 name = zs_path.stem().string() + "_imported";
1150
1151 // Set up basic mapping for common fields
1152 zscream_mappings["rom_file"] = "rom_filename";
1153 zscream_mappings["source_code"] = "code_folder";
1154 zscream_mappings["project_name"] = "name";
1155
1157
1158 // TODO: Implement actual ZScream format parsing when format is known
1159 // For now, just create a project structure that can be manually configured
1160
1161 return absl::OkStatus();
1162}
1163
1164absl::Status YazeProject::ExportForZScream(const std::string& target_path) {
1165 // Create a simplified project file that ZScream might understand
1166 std::ofstream file(target_path);
1167 if (!file.is_open()) {
1168 return absl::InvalidArgumentError(
1169 absl::StrFormat("Cannot create ZScream project file: %s", target_path));
1170 }
1171
1172 // Write in a simple format that ZScream might understand
1173 file << "# ZScream Compatible Project File\n";
1174 file << "# Exported from YAZE " << metadata.yaze_version << "\n\n";
1175 file << "name=" << name << "\n";
1176 file << "rom_file=" << rom_filename << "\n";
1177 file << "source_code=" << code_folder << "\n";
1178 file << "description=" << metadata.description << "\n";
1179 file << "author=" << metadata.author << "\n";
1180 file << "created_with=YAZE " << metadata.yaze_version << "\n";
1181
1182 file.close();
1183 return absl::OkStatus();
1184}
1185
1187 // Consolidated loading of all settings from project file
1188 // This replaces scattered config loading throughout the application
1190}
1191
1193 // Consolidated saving of all settings to project file
1194 return SaveToYazeFormat();
1195}
1196
1199 return Save();
1200}
1201
1202absl::Status YazeProject::Validate() const {
1203 std::vector<std::string> errors;
1204
1205 if (name.empty())
1206 errors.push_back("Project name is required");
1207 if (filepath.empty())
1208 errors.push_back("Project file path is required");
1209 if (rom_filename.empty())
1210 errors.push_back("ROM file is required");
1211
1212#ifndef __EMSCRIPTEN__
1213 // Check if files exist
1214 if (!rom_filename.empty() &&
1215 !std::filesystem::exists(GetAbsolutePath(rom_filename))) {
1216 errors.push_back("ROM file does not exist: " + rom_filename);
1217 }
1218
1219 if (!code_folder.empty() &&
1220 !std::filesystem::exists(GetAbsolutePath(code_folder))) {
1221 errors.push_back("Code folder does not exist: " + code_folder);
1222 }
1223
1224 if (!labels_filename.empty() &&
1225 !std::filesystem::exists(GetAbsolutePath(labels_filename))) {
1226 errors.push_back("Labels file does not exist: " + labels_filename);
1227 }
1228#endif // __EMSCRIPTEN__
1229
1230 if (!errors.empty()) {
1231 return absl::InvalidArgumentError(absl::StrJoin(errors, "; "));
1232 }
1233
1234 return absl::OkStatus();
1235}
1236
1237std::vector<std::string> YazeProject::GetMissingFiles() const {
1238 std::vector<std::string> missing;
1239
1240#ifndef __EMSCRIPTEN__
1241 if (!rom_filename.empty() &&
1242 !std::filesystem::exists(GetAbsolutePath(rom_filename))) {
1243 missing.push_back(rom_filename);
1244 }
1245 if (!labels_filename.empty() &&
1246 !std::filesystem::exists(GetAbsolutePath(labels_filename))) {
1247 missing.push_back(labels_filename);
1248 }
1249 if (!symbols_filename.empty() &&
1250 !std::filesystem::exists(GetAbsolutePath(symbols_filename))) {
1251 missing.push_back(symbols_filename);
1252 }
1253#endif // __EMSCRIPTEN__
1254
1255 return missing;
1256}
1257
1259#ifdef __EMSCRIPTEN__
1260 // In the web build, filesystem layout is virtual; nothing to repair eagerly.
1261 return absl::OkStatus();
1262#else
1263 // Create missing directories
1264 std::vector<std::string> folders = {code_folder, assets_folder,
1267
1268 for (const auto& folder : folders) {
1269 if (!folder.empty()) {
1270 std::filesystem::path abs_path = GetAbsolutePath(folder);
1271 if (!std::filesystem::exists(abs_path)) {
1272 std::filesystem::create_directories(abs_path);
1273 }
1274 }
1275 }
1276
1277 // Create missing files with defaults
1278 if (!labels_filename.empty()) {
1279 std::filesystem::path abs_labels = GetAbsolutePath(labels_filename);
1280 if (!std::filesystem::exists(abs_labels)) {
1281 std::ofstream labels_file(abs_labels);
1282 labels_file << "# yaze Resource Labels\n";
1283 labels_file << "# Format: [type] key=value\n\n";
1284 labels_file.close();
1285 }
1286 }
1287
1288 return absl::OkStatus();
1289#endif
1290}
1291
1292std::string YazeProject::GetDisplayName() const {
1293 if (!metadata.description.empty()) {
1294 return metadata.description;
1295 }
1296 return name.empty() ? "Untitled Project" : name;
1297}
1298
1300 const std::string& absolute_path) const {
1301 if (absolute_path.empty() || filepath.empty())
1302 return absolute_path;
1303
1304 std::filesystem::path project_dir =
1305 std::filesystem::path(filepath).parent_path();
1306 std::filesystem::path abs_path(absolute_path);
1307
1308 try {
1309 std::filesystem::path relative =
1310 std::filesystem::relative(abs_path.lexically_normal(), project_dir);
1311 // Persist relative paths in a platform-neutral format for project files.
1312 return relative.generic_string();
1313 } catch (...) {
1314 // Return normalized absolute path if relative conversion fails.
1315 return abs_path.lexically_normal().generic_string();
1316 }
1317}
1318
1320 const std::string& relative_path) const {
1321 if (relative_path.empty() || filepath.empty())
1322 return relative_path;
1323
1324 std::filesystem::path project_dir =
1325 std::filesystem::path(filepath).parent_path();
1326 std::filesystem::path abs_path(relative_path);
1327 if (abs_path.is_absolute()) {
1328 abs_path = abs_path.lexically_normal();
1329 abs_path.make_preferred();
1330 return abs_path.string();
1331 }
1332 abs_path = (project_dir / abs_path).lexically_normal();
1333 abs_path.make_preferred();
1334
1335 return abs_path.string();
1336}
1337
1339#ifdef __EMSCRIPTEN__
1340 // Web builds rely on a virtual filesystem and often use relative paths.
1341 return;
1342#endif
1343 if (filepath.empty()) {
1344 return;
1345 }
1346
1347 auto normalize = [this](std::string* path) {
1348 if (!path || path->empty()) {
1349 return;
1350 }
1351 *path = GetAbsolutePath(*path);
1352 };
1353
1354 normalize(&rom_filename);
1355 normalize(&rom_backup_folder);
1356 normalize(&code_folder);
1357 normalize(&assets_folder);
1358 normalize(&patches_folder);
1359 normalize(&labels_filename);
1360 normalize(&symbols_filename);
1361 normalize(&custom_objects_folder);
1362 normalize(&hack_manifest_file);
1363 normalize(&output_folder);
1364
1365 for (auto& rom_path : additional_roms) {
1366 if (!rom_path.empty()) {
1367 rom_path = GetAbsolutePath(rom_path);
1368 }
1369 }
1370}
1371
1373 return name.empty() && rom_filename.empty() && code_folder.empty();
1374}
1375
1377 const std::string& project_path) {
1378 // TODO: Implement ZScream format parsing when format specification is
1379 // available For now, create a basic project that can be manually configured
1380
1381 std::filesystem::path zs_path(project_path);
1382 name = zs_path.stem().string() + "_imported";
1383 zscream_project_file = project_path;
1384
1386
1387 return absl::OkStatus();
1388}
1389
1393
1397
1399 absl::string_view artifact_name) const {
1400 std::filesystem::path base_dir;
1401 if (!output_folder.empty()) {
1402 base_dir = output_folder;
1403 } else if (!z3dk_settings.config_path.empty()) {
1404 base_dir = std::filesystem::path(z3dk_settings.config_path).parent_path();
1405 } else if (!code_folder.empty()) {
1406 base_dir = code_folder;
1407 } else if (!filepath.empty()) {
1408 base_dir = std::filesystem::path(filepath).parent_path();
1409 }
1410
1411 if (base_dir.empty()) {
1412 return std::string(artifact_name);
1413 }
1414 return (base_dir / std::string(artifact_name)).lexically_normal().string();
1415}
1416
1419
1420#if defined(YAZE_WITH_Z3DK) && __has_include("z3dk_core/config.h")
1421 std::vector<std::filesystem::path> candidates;
1422 auto add_candidate = [&candidates](const std::filesystem::path& candidate) {
1423 if (candidate.empty()) {
1424 return;
1425 }
1426 auto normalized = candidate.lexically_normal();
1427 if (std::find(candidates.begin(), candidates.end(), normalized) ==
1428 candidates.end()) {
1429 candidates.push_back(normalized);
1430 }
1431 };
1432
1433 if (!code_folder.empty()) {
1434 std::filesystem::path code_path(code_folder);
1435 if (!std::filesystem::is_directory(code_path)) {
1436 code_path = code_path.parent_path();
1437 }
1438 add_candidate(code_path / "z3dk.toml");
1439 }
1440
1441 if (!hack_manifest_file.empty()) {
1442 add_candidate(std::filesystem::path(hack_manifest_file).parent_path() /
1443 "z3dk.toml");
1444 }
1445
1446 if (!filepath.empty()) {
1447 add_candidate(std::filesystem::path(filepath).parent_path() / "z3dk.toml");
1448 }
1449
1450 for (const auto& candidate : candidates) {
1451 if (!std::filesystem::exists(candidate)) {
1452 continue;
1453 }
1454
1455 std::string error;
1456 z3dk::Config config = z3dk::LoadConfigFile(candidate.string(), &error);
1457 if (!error.empty()) {
1458 LOG_WARN("Project", "Failed to parse z3dk config '%s': %s",
1459 candidate.string().c_str(), error.c_str());
1460 continue;
1461 }
1462
1463 const std::filesystem::path base_dir = candidate.parent_path();
1464 z3dk_settings.loaded = true;
1465 z3dk_settings.config_path = candidate.string();
1466 if (config.preset.has_value()) {
1467 z3dk_settings.preset = *config.preset;
1468 }
1469
1470 z3dk_settings.include_paths.reserve(config.include_paths.size());
1471 for (const auto& include_path : config.include_paths) {
1472 z3dk_settings.include_paths.push_back(
1473 ResolveOptionalPath(base_dir, include_path));
1474 }
1475
1476 z3dk_settings.defines.reserve(config.defines.size());
1477 for (const auto& define : config.defines) {
1478 z3dk_settings.defines.push_back(ParseDefineToken(define));
1479 }
1480
1481 z3dk_settings.main_files.reserve(config.main_files.size());
1482 for (const auto& main_file : config.main_files) {
1483 z3dk_settings.main_files.push_back(
1484 ResolveOptionalPath(base_dir, main_file));
1485 }
1486
1487 if (config.std_includes_path.has_value()) {
1489 ResolveOptionalPath(base_dir, *config.std_includes_path);
1490 }
1491 if (config.std_defines_path.has_value()) {
1493 ResolveOptionalPath(base_dir, *config.std_defines_path);
1494 }
1495 if (config.mapper.has_value()) {
1496 z3dk_settings.mapper = *config.mapper;
1497 }
1498 if (config.rom_size.has_value()) {
1499 z3dk_settings.rom_size = *config.rom_size;
1500 }
1501 if (config.symbols_format.has_value()) {
1502 z3dk_settings.symbols_format = *config.symbols_format;
1503 }
1504 z3dk_settings.lsp_log_enabled = config.lsp_log_enabled;
1505 if (config.lsp_log_path.has_value()) {
1507 ResolveOptionalPath(base_dir, *config.lsp_log_path);
1508 }
1509
1510 z3dk_settings.emits.reserve(config.emits.size());
1511 for (const auto& emit_path : config.emits) {
1512 z3dk_settings.emits.push_back(ResolveOptionalPath(base_dir, emit_path));
1513 }
1514
1515 for (const auto& range : config.prohibited_memory_ranges) {
1517 {.start = range.start, .end = range.end, .reason = range.reason});
1518 }
1519
1521 config.warn_unused_symbols.value_or(true);
1523 config.warn_branch_outside_bank.value_or(true);
1524 z3dk_settings.warn_unknown_width = config.warn_unknown_width.value_or(true);
1525 z3dk_settings.warn_org_collision = config.warn_org_collision.value_or(true);
1527 config.warn_unauthorized_hook.value_or(true);
1528 z3dk_settings.warn_stack_balance = config.warn_stack_balance.value_or(true);
1529 z3dk_settings.warn_hook_return = config.warn_hook_return.value_or(true);
1530
1531 if (config.rom_path.has_value()) {
1532 z3dk_settings.rom_path = ResolveOptionalPath(base_dir, *config.rom_path);
1533 }
1534 if (config.symbols_path.has_value()) {
1536 ResolveOptionalPath(base_dir, *config.symbols_path);
1537 }
1538
1539 for (const auto& emit_path : z3dk_settings.emits) {
1540 const std::string basename = BasenameLower(emit_path);
1541 if (basename.ends_with(".mlb") &&
1544 } else if (basename == "sourcemap.json") {
1546 } else if (basename == "annotations.json") {
1548 } else if (basename == "hooks.json") {
1550 } else if (basename == "lint.json") {
1552 }
1553 }
1554
1556 if (!z3dk_settings.symbols_path.empty() &&
1557 BasenameLower(z3dk_settings.symbols_path).ends_with(".mlb")) {
1559 } else {
1561 GetZ3dkArtifactPath("symbols.mlb");
1562 }
1563 }
1566 GetZ3dkArtifactPath("sourcemap.json");
1567 }
1570 GetZ3dkArtifactPath("annotations.json");
1571 }
1574 GetZ3dkArtifactPath("hooks.json");
1575 }
1578 }
1579
1580 LOG_INFO("Project",
1581 "Loaded z3dk config from %s (%zu include paths, %zu defines)",
1582 z3dk_settings.config_path.c_str(),
1584 return;
1585 }
1586#endif
1587}
1588
1590#ifdef __EMSCRIPTEN__
1593 return; // Hack manifests not supported in web builds
1594#endif
1595
1596 // Clear previous state so we never keep a stale manifest across project loads.
1599
1600 std::filesystem::path loaded_manifest_path;
1601 auto load_manifest = [&](const std::filesystem::path& candidate,
1602 bool update_project_setting) -> bool {
1603 if (candidate.empty() || !std::filesystem::exists(candidate)) {
1604 return false;
1605 }
1606 auto status = hack_manifest.LoadFromFile(candidate.string());
1607 if (!status.ok()) {
1608 LOG_WARN("Project", "Failed to load hack manifest %s: %s",
1609 candidate.string().c_str(),
1610 std::string(status.message()).c_str());
1611 return false;
1612 }
1613 loaded_manifest_path = candidate;
1614 if (update_project_setting) {
1615 hack_manifest_file = GetRelativePath(candidate.string());
1616 }
1617 LOG_DEBUG("Project", "Loaded hack manifest: %s",
1618 candidate.string().c_str());
1620 return true;
1621 };
1622
1623 // Priority 1: Explicit hack_manifest_file setting from project.
1624 if (!hack_manifest_file.empty()) {
1625 (void)load_manifest(GetAbsolutePath(hack_manifest_file), false);
1626 }
1627
1628 // Priority 2: Auto-discover hack_manifest.json in code_folder.
1629 if (!hack_manifest.loaded() && !code_folder.empty()) {
1630 auto code_path = GetAbsolutePath(code_folder);
1631 auto candidate = std::filesystem::path(code_path) / "hack_manifest.json";
1632 (void)load_manifest(candidate, true);
1633 }
1634
1635 // Priority 3: Fallback to the project file directory (or its parent).
1636 if (!hack_manifest.loaded() && !filepath.empty()) {
1637 const std::filesystem::path project_dir =
1638 std::filesystem::path(filepath).parent_path();
1639 (void)load_manifest(project_dir / "hack_manifest.json", true);
1640 if (!hack_manifest.loaded() && project_dir.has_parent_path()) {
1641 (void)load_manifest(project_dir.parent_path() / "hack_manifest.json",
1642 true);
1643 }
1644 }
1645
1646 if (!hack_manifest.loaded()) {
1647 return;
1648 }
1649
1651
1652 auto try_load_registry = [&](const std::filesystem::path& base) -> bool {
1653 if (base.empty()) {
1654 return false;
1655 }
1656 const auto planning = base / "Docs" / "Dev" / "Planning";
1657 if (!std::filesystem::exists(planning)) {
1658 return false;
1659 }
1660 auto status = hack_manifest.LoadProjectRegistry(base.string());
1661 if (!status.ok()) {
1662 LOG_WARN("Project", "Failed to load project registry from %s: %s",
1663 base.string().c_str(), std::string(status.message()).c_str());
1664 return false;
1665 }
1667 };
1668
1669 bool registry_loaded = false;
1670
1671 // Prefer configured code_folder when valid.
1672 if (!code_folder.empty()) {
1673 registry_loaded =
1674 try_load_registry(std::filesystem::path(GetAbsolutePath(code_folder)));
1675 }
1676
1677 // Fallback to the manifest directory if code_folder is stale/misconfigured.
1678 if (!registry_loaded && !loaded_manifest_path.empty()) {
1679 registry_loaded = try_load_registry(loaded_manifest_path.parent_path());
1680 }
1681
1682 // Last fallback: project directory.
1683 if (!registry_loaded && !filepath.empty()) {
1684 registry_loaded =
1685 try_load_registry(std::filesystem::path(filepath).parent_path());
1686 }
1687
1688 if (!registry_loaded) {
1689 LOG_WARN("Project",
1690 "Hack manifest loaded but project registry was not found "
1691 "(code_folder='%s', manifest='%s')",
1692 code_folder.c_str(), loaded_manifest_path.string().c_str());
1693 return;
1694 }
1695
1696 // Inject all Oracle resource labels into project resource_labels.
1697 size_t injected = 0;
1698 for (const auto& [type_key, labels] :
1700 for (const auto& [id_str, label] : labels) {
1701 resource_labels[type_key][id_str] = label;
1702 ++injected;
1703 }
1704 }
1705 LOG_DEBUG("Project", "Loaded project registry: %zu resource labels injected",
1706 injected);
1707}
1708
1710 if (metadata.project_id.empty()) {
1712 }
1713
1714 // Initialize default feature flags
1729 // REMOVED: kLogInstructions (deprecated)
1730
1731 // Initialize default workspace settings
1734 workspace_settings.ui_theme = "default";
1736 workspace_settings.autosave_interval_secs = 300.0f; // 5 minutes
1743
1744 // Initialize default dungeon overlay settings (minecart tracks)
1746 for (uint16_t tile = 0xB0; tile <= 0xBE; ++tile) {
1747 dungeon_overlay.track_tiles.push_back(tile);
1748 }
1749 dungeon_overlay.track_stop_tiles = {0xB7, 0xB8, 0xB9, 0xBA};
1750 dungeon_overlay.track_switch_tiles = {0xD0, 0xD1, 0xD2, 0xD3};
1753
1757
1758 // Initialize default build configurations
1759 build_configurations = {"Debug", "Release", "Distribution"};
1760 build_target.clear();
1761 asm_entry_point = "asm/main.asm";
1762 asm_sources = {"asm"};
1763 last_build_hash.clear();
1764 build_number = 0;
1765
1766 track_changes = true;
1767
1771
1772 if (metadata.created_by.empty()) {
1773 metadata.created_by = "YAZE";
1774 }
1775}
1776
1778 auto now = std::chrono::system_clock::now().time_since_epoch();
1779 auto timestamp =
1780 std::chrono::duration_cast<std::chrono::milliseconds>(now).count();
1781 return absl::StrFormat("yaze_project_%lld", timestamp);
1782}
1783
1784// ProjectManager Implementation
1785std::vector<ProjectManager::ProjectTemplate>
1787 std::vector<ProjectTemplate> templates;
1788
1789 // ==========================================================================
1790 // ZSCustomOverworld Templates (Recommended)
1791 // ==========================================================================
1792
1793 // Vanilla ROM Hack - no ZSO
1794 {
1796 t.name = "Vanilla ROM Hack";
1797 t.description =
1798 "Standard ROM editing without custom ASM. Limited to vanilla features.";
1802 false;
1807 templates.push_back(t);
1808 }
1809
1810 // ZSCustomOverworld v2 - Basic expansion
1811 {
1813 t.name = "ZSCustomOverworld v2";
1814 t.description =
1815 "Basic overworld expansion: custom BG colors, main palettes, parent "
1816 "system.";
1817 t.icon = ICON_MD_MAP;
1820 true;
1827 t.template_project.metadata.tags = {"zso_v2", "overworld", "expansion"};
1828 templates.push_back(t);
1829 }
1830
1831 // ZSCustomOverworld v3 - Full features (Recommended)
1832 {
1834 t.name = "ZSCustomOverworld v3 (Recommended)";
1835 t.description =
1836 "Full overworld expansion: wide/tall areas, animated GFX, overlays, "
1837 "all features.";
1841 true;
1851 t.template_project.metadata.tags = {"zso_v3", "overworld", "full",
1852 "recommended"};
1853 templates.push_back(t);
1854 }
1855
1856 // Randomizer Compatible
1857 {
1859 t.name = "Randomizer Compatible";
1860 t.description =
1861 "Compatible with ALttP Randomizer. Minimal custom features to avoid "
1862 "conflicts.";
1866 false;
1869 t.template_project.metadata.tags = {"randomizer", "compatible", "minimal"};
1870 templates.push_back(t);
1871 }
1872
1873 // ==========================================================================
1874 // Editor-Focused Templates
1875 // ==========================================================================
1876
1877 // Dungeon Designer
1878 {
1880 t.name = "Dungeon Designer";
1881 t.description = "Focused on dungeon creation and modification.";
1882 t.icon = ICON_MD_DOMAIN;
1886 "dungeon_default";
1887 t.template_project.metadata.tags = {"dungeons", "rooms", "design"};
1888 templates.push_back(t);
1889 }
1890
1891 // Graphics Pack
1892 {
1894 t.name = "Graphics Pack";
1895 t.description =
1896 "Project focused on graphics, sprites, and visual modifications.";
1903 "graphics_default";
1904 t.template_project.metadata.tags = {"graphics", "sprites", "palettes"};
1905 templates.push_back(t);
1906 }
1907
1908 // Complete Overhaul
1909 {
1911 t.name = "Complete Overhaul";
1912 t.description = "Full-scale ROM hack with all features enabled.";
1913 t.icon = ICON_MD_BUILD;
1916 true;
1926 t.template_project.metadata.tags = {"complete", "overhaul", "full-mod"};
1927 templates.push_back(t);
1928 }
1929
1930 return templates;
1931}
1932
1933absl::StatusOr<YazeProject> ProjectManager::CreateFromTemplate(
1934 const std::string& template_name, const std::string& project_name,
1935 const std::string& base_path) {
1936 YazeProject project;
1937 auto status = project.Create(project_name, base_path);
1938 if (!status.ok()) {
1939 return status;
1940 }
1941
1942 // Customize based on template
1943 if (template_name == "Full Overworld Mod") {
1946 project.metadata.description = "Overworld modification project";
1947 project.metadata.tags = {"overworld", "maps", "graphics"};
1948 } else if (template_name == "Dungeon Designer") {
1949 project.feature_flags.kSaveDungeonMaps = true;
1950 project.workspace_settings.show_grid = true;
1951 project.metadata.description = "Dungeon design and modification project";
1952 project.metadata.tags = {"dungeons", "rooms", "design"};
1953 } else if (template_name == "Graphics Pack") {
1954 project.feature_flags.kSaveGraphicsSheet = true;
1955 project.workspace_settings.show_grid = true;
1956 project.metadata.description = "Graphics and sprite modification project";
1957 project.metadata.tags = {"graphics", "sprites", "palettes"};
1958 } else if (template_name == "Complete Overhaul") {
1961 project.feature_flags.kSaveDungeonMaps = true;
1962 project.feature_flags.kSaveGraphicsSheet = true;
1963 project.metadata.description = "Complete ROM overhaul project";
1964 project.metadata.tags = {"complete", "overhaul", "full-mod"};
1965 }
1966
1967 status = project.Save();
1968 if (!status.ok()) {
1969 return status;
1970 }
1971
1972 return project;
1973}
1974
1976 const std::string& directory) {
1977#ifdef __EMSCRIPTEN__
1978 (void)directory;
1979 return {};
1980#else
1981 std::vector<std::string> projects;
1982
1983 try {
1984 for (const auto& entry : std::filesystem::directory_iterator(directory)) {
1985 if (entry.is_regular_file()) {
1986 std::string filename = entry.path().filename().string();
1987 if (filename.ends_with(".yaze") || filename.ends_with(".zsproj")) {
1988 projects.push_back(entry.path().string());
1989 }
1990 } else if (entry.is_directory()) {
1991 std::string filename = entry.path().filename().string();
1992 if (filename.ends_with(".yazeproj")) {
1993 projects.push_back(entry.path().string());
1994 }
1995 }
1996 }
1997 } catch (const std::filesystem::filesystem_error& e) {
1998 // Directory doesn't exist or can't be accessed
1999 }
2000
2001 return projects;
2002#endif // __EMSCRIPTEN__
2003}
2004
2005absl::Status ProjectManager::BackupProject(const YazeProject& project) {
2006#ifdef __EMSCRIPTEN__
2007 (void)project;
2008 return absl::UnimplementedError(
2009 "Project backups are not supported in the web build");
2010#else
2011 if (project.filepath.empty()) {
2012 return absl::InvalidArgumentError("Project has no file path");
2013 }
2014
2015 std::filesystem::path project_path(project.filepath);
2016 std::filesystem::path backup_dir = project_path.parent_path() / "backups";
2017 std::filesystem::create_directories(backup_dir);
2018
2019 auto now = std::chrono::system_clock::now();
2020 auto time_t = std::chrono::system_clock::to_time_t(now);
2021 std::stringstream ss;
2022 ss << std::put_time(std::localtime(&time_t), "%Y%m%d_%H%M%S");
2023
2024 std::string backup_filename = project.name + "_backup_" + ss.str() + ".yaze";
2025 std::filesystem::path backup_path = backup_dir / backup_filename;
2026
2027 try {
2028 std::filesystem::copy_file(project.filepath, backup_path);
2029 } catch (const std::filesystem::filesystem_error& e) {
2030 return absl::InternalError(
2031 absl::StrFormat("Failed to backup project: %s", e.what()));
2032 }
2033
2034 return absl::OkStatus();
2035#endif
2036}
2037
2039 const YazeProject& project) {
2040 return project.Validate();
2041}
2042
2044 const YazeProject& project) {
2045 std::vector<std::string> recommendations;
2046
2047 if (project.rom_filename.empty()) {
2048 recommendations.push_back("Add a ROM file to begin editing");
2049 }
2050
2051 if (project.code_folder.empty()) {
2052 recommendations.push_back("Set up a code folder for assembly patches");
2053 }
2054
2055 if (project.labels_filename.empty()) {
2056 recommendations.push_back("Create a labels file for better organization");
2057 }
2058
2059 if (project.metadata.description.empty()) {
2060 recommendations.push_back("Add a project description for documentation");
2061 }
2062
2063 if (project.git_repository.empty() && project.track_changes) {
2064 recommendations.push_back(
2065 "Consider setting up version control for your project");
2066 }
2067
2068 auto missing_files = project.GetMissingFiles();
2069 if (!missing_files.empty()) {
2070 recommendations.push_back(
2071 "Some project files are missing - use Project > Repair to fix");
2072 }
2073
2074 return recommendations;
2075}
2076
2077// Compatibility implementations for ResourceLabelManager and related classes
2078bool ResourceLabelManager::LoadLabels(const std::string& filename) {
2079 filename_ = filename;
2080 std::ifstream file(filename);
2081 if (!file.is_open()) {
2082 labels_loaded_ = false;
2083 return false;
2084 }
2085
2086 labels_.clear();
2087 std::string line;
2088 std::string current_type = "";
2089
2090 while (std::getline(file, line)) {
2091 if (line.empty() || line[0] == '#')
2092 continue;
2093
2094 // Check for type headers [type_name]
2095 if (line[0] == '[' && line.back() == ']') {
2096 current_type = line.substr(1, line.length() - 2);
2097 continue;
2098 }
2099
2100 // Parse key=value pairs
2101 size_t eq_pos = line.find('=');
2102 if (eq_pos != std::string::npos && !current_type.empty()) {
2103 std::string key = line.substr(0, eq_pos);
2104 std::string value = line.substr(eq_pos + 1);
2105 labels_[current_type][key] = value;
2106 }
2107 }
2108
2109 file.close();
2110 labels_loaded_ = true;
2111 return true;
2112}
2113
2115 if (filename_.empty())
2116 return false;
2117
2118 std::ofstream file(filename_);
2119 if (!file.is_open())
2120 return false;
2121
2122 file << "# yaze Resource Labels\n";
2123 file << "# Format: [type] followed by key=value pairs\n\n";
2124
2125 for (const auto& [type, type_labels] : labels_) {
2126 if (!type_labels.empty()) {
2127 file << "[" << type << "]\n";
2128 for (const auto& [key, value] : type_labels) {
2129 file << key << "=" << value << "\n";
2130 }
2131 file << "\n";
2132 }
2133 }
2134
2135 file.close();
2136 return true;
2137}
2138
2140 if (!p_open || !*p_open)
2141 return;
2142
2143 // Basic implementation - can be enhanced later
2144 if (ImGui::Begin("Resource Labels", p_open)) {
2145 ImGui::Text("Resource Labels Manager");
2146 ImGui::Text("Labels loaded: %s", labels_loaded_ ? "Yes" : "No");
2147 ImGui::Text("Total types: %zu", labels_.size());
2148
2149 for (const auto& [type, type_labels] : labels_) {
2150 if (ImGui::TreeNode(type.c_str())) {
2151 ImGui::Text("Labels: %zu", type_labels.size());
2152 for (const auto& [key, value] : type_labels) {
2153 ImGui::Text("%s = %s", key.c_str(), value.c_str());
2154 }
2155 ImGui::TreePop();
2156 }
2157 }
2158 }
2159 ImGui::End();
2160}
2161
2162void ResourceLabelManager::EditLabel(const std::string& type,
2163 const std::string& key,
2164 const std::string& newValue) {
2165 labels_[type][key] = newValue;
2166}
2167
2169 bool selected, const std::string& type, const std::string& key,
2170 const std::string& defaultValue) {
2171 // Basic implementation
2172 if (ImGui::Selectable(
2173 absl::StrFormat("%s: %s", key.c_str(), GetLabel(type, key).c_str())
2174 .c_str(),
2175 selected)) {
2176 // Handle selection
2177 }
2178}
2179
2180std::string ResourceLabelManager::GetLabel(const std::string& type,
2181 const std::string& key) {
2182 auto type_it = labels_.find(type);
2183 if (type_it == labels_.end())
2184 return "";
2185
2186 auto label_it = type_it->second.find(key);
2187 if (label_it == type_it->second.end())
2188 return "";
2189
2190 return label_it->second;
2191}
2192
2194 const std::string& type, const std::string& key,
2195 const std::string& defaultValue) {
2196 auto existing = GetLabel(type, key);
2197 if (!existing.empty())
2198 return existing;
2199
2200 labels_[type][key] = defaultValue;
2201 return defaultValue;
2202}
2203
2204// ============================================================================
2205// Embedded Labels Support
2206// ============================================================================
2207
2209 const std::unordered_map<
2210 std::string, std::unordered_map<std::string, std::string>>& labels) {
2211 try {
2212 // Load all default Zelda3 resource names into resource_labels
2213 // We merge them with existing labels, prioritizing existing overrides?
2214 // Or just overwrite? The previous code was:
2215 // resource_labels = zelda3::Zelda3Labels::ToResourceLabels();
2216 // which implies overwriting. But we want to keep overrides if possible.
2217 // However, this is usually called on load.
2218
2219 // Let's overwrite for now to match previous behavior, assuming overrides
2220 // are loaded afterwards or this is initial setup.
2221 // Actually, if we load project then init embedded labels, we might lose overrides.
2222 // But typically overrides are loaded from the project file *into* resource_labels.
2223 // If we call this, we might clobber them.
2224 // The previous implementation clobbered resource_labels.
2225
2226 // However, if we want to support overrides + embedded, we should merge.
2227 // But `resource_labels` was treated as "overrides" in the old code?
2228 // No, `resource_labels` was the container for loaded labels.
2229
2230 // If I look at `LoadFromYazeFormat`:
2231 // It parses `[labels_type]` into `resource_labels`.
2232
2233 // If `use_embedded_labels` is true, `InitializeEmbeddedLabels` is called?
2234 // I need to check when `InitializeEmbeddedLabels` is called.
2235
2236 resource_labels = labels;
2237 use_embedded_labels = true;
2238
2239 LOG_DEBUG("Project", "Initialized embedded labels:");
2240 LOG_DEBUG("Project", " - %d room names", resource_labels["room"].size());
2241 LOG_DEBUG("Project", " - %d entrance names",
2242 resource_labels["entrance"].size());
2243 LOG_DEBUG("Project", " - %d sprite names",
2244 resource_labels["sprite"].size());
2245 LOG_DEBUG("Project", " - %d overlord names",
2246 resource_labels["overlord"].size());
2247 LOG_DEBUG("Project", " - %d item names", resource_labels["item"].size());
2248 LOG_DEBUG("Project", " - %d music names",
2249 resource_labels["music"].size());
2250 LOG_DEBUG("Project", " - %d graphics names",
2251 resource_labels["graphics"].size());
2252 LOG_DEBUG("Project", " - %d room effect names",
2253 resource_labels["room_effect"].size());
2254 LOG_DEBUG("Project", " - %d room tag names",
2255 resource_labels["room_tag"].size());
2256 LOG_DEBUG("Project", " - %d tile type names",
2257 resource_labels["tile_type"].size());
2258
2259 return absl::OkStatus();
2260 } catch (const std::exception& e) {
2261 return absl::InternalError(
2262 absl::StrCat("Failed to initialize embedded labels: ", e.what()));
2263 }
2264}
2265
2266std::string YazeProject::GetLabel(const std::string& resource_type, int id,
2267 const std::string& default_value) const {
2268 // First check if we have a custom label override
2269 auto type_it = resource_labels.find(resource_type);
2270 if (type_it != resource_labels.end()) {
2271 auto label_it = type_it->second.find(std::to_string(id));
2272 if (label_it != type_it->second.end()) {
2273 return label_it->second;
2274 }
2275 }
2276
2277 return default_value.empty() ? resource_type + "_" + std::to_string(id)
2278 : default_value;
2279}
2280
2281absl::Status YazeProject::ImportLabelsFromZScream(const std::string& filepath) {
2282#ifdef __EMSCRIPTEN__
2283 (void)filepath;
2284 return absl::UnimplementedError(
2285 "File-based label import is not supported in the web build");
2286#else
2287 std::ifstream file(filepath);
2288 if (!file.is_open()) {
2289 return absl::InvalidArgumentError(
2290 absl::StrFormat("Cannot open labels file: %s", filepath));
2291 }
2292
2293 std::stringstream buffer;
2294 buffer << file.rdbuf();
2295 file.close();
2296
2297 return ImportLabelsFromZScreamContent(buffer.str());
2298#endif
2299}
2300
2302 const std::string& content) {
2303 // Initialize the global provider with our labels
2304 auto& provider = zelda3::GetResourceLabels();
2305 provider.SetProjectLabels(&resource_labels);
2306 provider.SetPreferHMagicNames(workspace_settings.prefer_hmagic_names);
2307
2308 // Use the provider to parse ZScream format
2309 auto status = provider.ImportFromZScreamFormat(content);
2310 if (!status.ok()) {
2311 return status;
2312 }
2313
2314 LOG_DEBUG("Project", "Imported ZScream labels:");
2315 LOG_DEBUG("Project", " - %d sprite labels",
2316 resource_labels["sprite"].size());
2317 LOG_DEBUG("Project", " - %d room labels", resource_labels["room"].size());
2318 LOG_DEBUG("Project", " - %d item labels", resource_labels["item"].size());
2319 LOG_DEBUG("Project", " - %d room tag labels",
2320 resource_labels["room_tag"].size());
2321
2322 return absl::OkStatus();
2323}
2324
2326 auto& provider = zelda3::GetResourceLabels();
2327 provider.SetProjectLabels(&resource_labels);
2328 provider.SetPreferHMagicNames(workspace_settings.prefer_hmagic_names);
2329 provider.SetHackManifest(hack_manifest.loaded() ? &hack_manifest : nullptr);
2330
2331 LOG_DEBUG("Project", "Initialized ResourceLabelProvider with project labels");
2332 LOG_DEBUG("Project", " - prefer_hmagic_names: %s",
2333 workspace_settings.prefer_hmagic_names ? "true" : "false");
2334 LOG_DEBUG("Project", " - hack_manifest: %s",
2335 hack_manifest.loaded() ? "loaded" : "not loaded");
2336}
2337
2338// ============================================================================
2339// JSON Format Support (Optional)
2340// ============================================================================
2341
2342#ifdef YAZE_ENABLE_JSON_PROJECT_FORMAT
2343
2344absl::Status YazeProject::LoadFromJsonFormat(const std::string& project_path) {
2345#ifdef __EMSCRIPTEN__
2346 return absl::UnimplementedError(
2347 "JSON project format loading is not supported in the web build");
2348#endif
2349 std::ifstream file(project_path);
2350 if (!file.is_open()) {
2351 return absl::InvalidArgumentError(
2352 absl::StrFormat("Cannot open JSON project file: %s", project_path));
2353 }
2354
2355 try {
2356 json j;
2357 file >> j;
2358
2359 // Parse project metadata
2360 if (j.contains("yaze_project")) {
2361 auto& proj = j["yaze_project"];
2362
2363 if (proj.contains("name"))
2364 name = proj["name"].get<std::string>();
2365 if (proj.contains("description"))
2366 metadata.description = proj["description"].get<std::string>();
2367 if (proj.contains("author"))
2368 metadata.author = proj["author"].get<std::string>();
2369 if (proj.contains("version"))
2370 metadata.version = proj["version"].get<std::string>();
2371 if (proj.contains("created"))
2372 metadata.created_date = proj["created"].get<std::string>();
2373 if (proj.contains("modified"))
2374 metadata.last_modified = proj["modified"].get<std::string>();
2375 if (proj.contains("created_by"))
2376 metadata.created_by = proj["created_by"].get<std::string>();
2377
2378 // Files
2379 if (proj.contains("rom_filename"))
2380 rom_filename = proj["rom_filename"].get<std::string>();
2381 if (proj.contains("rom_backup_folder"))
2382 rom_backup_folder = proj["rom_backup_folder"].get<std::string>();
2383 if (proj.contains("code_folder"))
2384 code_folder = proj["code_folder"].get<std::string>();
2385 if (proj.contains("assets_folder"))
2386 assets_folder = proj["assets_folder"].get<std::string>();
2387 if (proj.contains("patches_folder"))
2388 patches_folder = proj["patches_folder"].get<std::string>();
2389 if (proj.contains("labels_filename"))
2390 labels_filename = proj["labels_filename"].get<std::string>();
2391 if (proj.contains("symbols_filename"))
2392 symbols_filename = proj["symbols_filename"].get<std::string>();
2393 if (proj.contains("hack_manifest_file"))
2394 hack_manifest_file = proj["hack_manifest_file"].get<std::string>();
2395
2396 if (proj.contains("rom") && proj["rom"].is_object()) {
2397 auto& rom = proj["rom"];
2398 if (rom.contains("role"))
2399 rom_metadata.role = ParseRomRole(rom["role"].get<std::string>());
2400 if (rom.contains("expected_hash"))
2401 rom_metadata.expected_hash = rom["expected_hash"].get<std::string>();
2402 if (rom.contains("write_policy"))
2404 ParseRomWritePolicy(rom["write_policy"].get<std::string>());
2405 }
2406
2407 // Embedded labels flag
2408 if (proj.contains("use_embedded_labels")) {
2409 use_embedded_labels = proj["use_embedded_labels"].get<bool>();
2410 }
2411
2412 // Feature flags
2413 if (proj.contains("feature_flags")) {
2414 auto& flags = proj["feature_flags"];
2415 // REMOVED: kLogInstructions (deprecated - DisassemblyViewer always
2416 // active)
2417 if (flags.contains("kSaveDungeonMaps"))
2419 flags["kSaveDungeonMaps"].get<bool>();
2420 if (flags.contains("kSaveOverworldMaps"))
2422 flags["kSaveOverworldMaps"].get<bool>();
2423 if (flags.contains("kSaveOverworldEntrances"))
2425 flags["kSaveOverworldEntrances"].get<bool>();
2426 if (flags.contains("kSaveOverworldExits"))
2428 flags["kSaveOverworldExits"].get<bool>();
2429 if (flags.contains("kSaveOverworldItems"))
2431 flags["kSaveOverworldItems"].get<bool>();
2432 if (flags.contains("kSaveOverworldProperties"))
2434 flags["kSaveOverworldProperties"].get<bool>();
2435 if (flags.contains("kSaveDungeonObjects"))
2437 flags["kSaveDungeonObjects"].get<bool>();
2438 if (flags.contains("kSaveDungeonSprites"))
2440 flags["kSaveDungeonSprites"].get<bool>();
2441 if (flags.contains("kSaveDungeonRoomHeaders"))
2443 flags["kSaveDungeonRoomHeaders"].get<bool>();
2444 if (flags.contains("kSaveDungeonTorches"))
2446 flags["kSaveDungeonTorches"].get<bool>();
2447 if (flags.contains("kSaveDungeonPits"))
2449 flags["kSaveDungeonPits"].get<bool>();
2450 if (flags.contains("kSaveDungeonBlocks"))
2452 flags["kSaveDungeonBlocks"].get<bool>();
2453 if (flags.contains("kSaveDungeonCollision"))
2455 flags["kSaveDungeonCollision"].get<bool>();
2456 if (flags.contains("kSaveDungeonWaterFillZones"))
2458 flags["kSaveDungeonWaterFillZones"].get<bool>();
2459 if (flags.contains("kSaveDungeonChests"))
2461 flags["kSaveDungeonChests"].get<bool>();
2462 if (flags.contains("kSaveDungeonPotItems"))
2464 flags["kSaveDungeonPotItems"].get<bool>();
2465 if (flags.contains("kSaveDungeonPalettes"))
2467 flags["kSaveDungeonPalettes"].get<bool>();
2468 if (flags.contains("kSaveGraphicsSheet"))
2470 flags["kSaveGraphicsSheet"].get<bool>();
2471 if (flags.contains("kSaveAllPalettes"))
2473 flags["kSaveAllPalettes"].get<bool>();
2474 if (flags.contains("kSaveGfxGroups"))
2475 feature_flags.kSaveGfxGroups = flags["kSaveGfxGroups"].get<bool>();
2476 if (flags.contains("kSaveMessages"))
2477 feature_flags.kSaveMessages = flags["kSaveMessages"].get<bool>();
2478 }
2479
2480 // Workspace settings
2481 if (proj.contains("workspace_settings")) {
2482 auto& ws = proj["workspace_settings"];
2483 if (ws.contains("auto_save_enabled"))
2485 ws["auto_save_enabled"].get<bool>();
2486 if (ws.contains("auto_save_interval"))
2488 ws["auto_save_interval"].get<float>();
2489 if (ws.contains("backup_on_save"))
2490 workspace_settings.backup_on_save = ws["backup_on_save"].get<bool>();
2491 if (ws.contains("backup_retention_count"))
2493 ws["backup_retention_count"].get<int>();
2494 if (ws.contains("backup_keep_daily"))
2496 ws["backup_keep_daily"].get<bool>();
2497 if (ws.contains("backup_keep_daily_days"))
2499 ws["backup_keep_daily_days"].get<int>();
2500 }
2501
2502 if (proj.contains("rom_addresses") && proj["rom_addresses"].is_object()) {
2504 for (auto it = proj["rom_addresses"].begin();
2505 it != proj["rom_addresses"].end(); ++it) {
2506 if (it.value().is_number_unsigned()) {
2508 it.value().get<uint32_t>();
2509 } else if (it.value().is_string()) {
2510 auto parsed = ParseHexUint32(it.value().get<std::string>());
2511 if (parsed.has_value()) {
2512 rom_address_overrides.addresses[it.key()] = *parsed;
2513 }
2514 }
2515 }
2516 }
2517
2518 if (proj.contains("custom_objects") &&
2519 proj["custom_objects"].is_object()) {
2520 custom_object_files.clear();
2521 for (auto it = proj["custom_objects"].begin();
2522 it != proj["custom_objects"].end(); ++it) {
2523 if (!it.value().is_array())
2524 continue;
2525 auto parsed = ParseHexUint32(it.key());
2526 if (!parsed.has_value()) {
2527 continue;
2528 }
2529 std::vector<std::string> files;
2530 for (const auto& entry : it.value()) {
2531 if (entry.is_string()) {
2532 files.push_back(entry.get<std::string>());
2533 }
2534 }
2535 if (!files.empty()) {
2536 custom_object_files[static_cast<int>(*parsed)] = std::move(files);
2537 }
2538 }
2539 }
2540
2541 if (proj.contains("agent_settings") &&
2542 proj["agent_settings"].is_object()) {
2543 auto& agent = proj["agent_settings"];
2545 agent.value("ai_provider", agent_settings.ai_provider);
2547 agent.value("ai_model", agent_settings.ai_model);
2549 agent.value("ollama_host", agent_settings.ollama_host);
2551 agent.value("gemini_api_key", agent_settings.gemini_api_key);
2553 agent.value("use_custom_prompt", agent_settings.use_custom_prompt);
2555 "custom_system_prompt", agent_settings.custom_system_prompt);
2557 agent.value("show_reasoning", agent_settings.show_reasoning);
2558 agent_settings.verbose = agent.value("verbose", agent_settings.verbose);
2559 agent_settings.max_tool_iterations = agent.value(
2560 "max_tool_iterations", agent_settings.max_tool_iterations);
2561 agent_settings.max_retry_attempts = agent.value(
2562 "max_retry_attempts", agent_settings.max_retry_attempts);
2564 agent.value("temperature", agent_settings.temperature);
2565 agent_settings.top_p = agent.value("top_p", agent_settings.top_p);
2567 agent.value("max_output_tokens", agent_settings.max_output_tokens);
2569 agent.value("stream_responses", agent_settings.stream_responses);
2570 if (agent.contains("favorite_models") &&
2571 agent["favorite_models"].is_array()) {
2573 for (const auto& model : agent["favorite_models"]) {
2574 if (model.is_string())
2576 model.get<std::string>());
2577 }
2578 }
2579 if (agent.contains("model_chain") && agent["model_chain"].is_array()) {
2581 for (const auto& model : agent["model_chain"]) {
2582 if (model.is_string())
2583 agent_settings.model_chain.push_back(model.get<std::string>());
2584 }
2585 }
2587 agent.value("chain_mode", agent_settings.chain_mode);
2589 "enable_tool_resources", agent_settings.enable_tool_resources);
2590 agent_settings.enable_tool_dungeon = agent.value(
2591 "enable_tool_dungeon", agent_settings.enable_tool_dungeon);
2593 "enable_tool_overworld", agent_settings.enable_tool_overworld);
2595 "enable_tool_messages", agent_settings.enable_tool_messages);
2597 "enable_tool_dialogue", agent_settings.enable_tool_dialogue);
2599 agent.value("enable_tool_gui", agent_settings.enable_tool_gui);
2601 agent.value("enable_tool_music", agent_settings.enable_tool_music);
2602 agent_settings.enable_tool_sprite = agent.value(
2603 "enable_tool_sprite", agent_settings.enable_tool_sprite);
2605 "enable_tool_emulator", agent_settings.enable_tool_emulator);
2607 agent.value("enable_tool_memory_inspector",
2610 "builder_blueprint_path", agent_settings.builder_blueprint_path);
2611 }
2612
2613 // Build settings
2614 if (proj.contains("build_script"))
2615 build_script = proj["build_script"].get<std::string>();
2616 if (proj.contains("output_folder"))
2617 output_folder = proj["output_folder"].get<std::string>();
2618 if (proj.contains("git_repository"))
2619 git_repository = proj["git_repository"].get<std::string>();
2620 if (proj.contains("track_changes"))
2621 track_changes = proj["track_changes"].get<bool>();
2622 }
2623
2624 return absl::OkStatus();
2625 } catch (const json::exception& e) {
2626 return absl::InvalidArgumentError(
2627 absl::StrFormat("JSON parse error: %s", e.what()));
2628 }
2629}
2630
2631absl::Status YazeProject::SaveToJsonFormat() {
2632#ifdef __EMSCRIPTEN__
2633 return absl::UnimplementedError(
2634 "JSON project format saving is not supported in the web build");
2635#endif
2636 json j;
2637 auto& proj = j["yaze_project"];
2638
2639 // Metadata
2640 proj["version"] = metadata.version;
2641 proj["name"] = name;
2642 proj["author"] = metadata.author;
2643 proj["created_by"] = metadata.created_by;
2644 proj["description"] = metadata.description;
2645 proj["created"] = metadata.created_date;
2646 proj["modified"] = metadata.last_modified;
2647
2648 // Files
2649 proj["rom_filename"] = rom_filename;
2650 proj["rom_backup_folder"] = rom_backup_folder;
2651 proj["code_folder"] = code_folder;
2652 proj["assets_folder"] = assets_folder;
2653 proj["patches_folder"] = patches_folder;
2654 proj["labels_filename"] = labels_filename;
2655 proj["symbols_filename"] = symbols_filename;
2656 proj["hack_manifest_file"] = hack_manifest_file;
2657 proj["output_folder"] = output_folder;
2658
2659 proj["rom"]["role"] = RomRoleToString(rom_metadata.role);
2660 proj["rom"]["expected_hash"] = rom_metadata.expected_hash;
2661 proj["rom"]["write_policy"] =
2663
2664 // Embedded labels
2665 proj["use_embedded_labels"] = use_embedded_labels;
2666
2667 // Feature flags
2668 // REMOVED: kLogInstructions (deprecated)
2669 proj["feature_flags"]["kSaveDungeonMaps"] = feature_flags.kSaveDungeonMaps;
2670 proj["feature_flags"]["kSaveOverworldMaps"] =
2672 proj["feature_flags"]["kSaveOverworldEntrances"] =
2674 proj["feature_flags"]["kSaveOverworldExits"] =
2676 proj["feature_flags"]["kSaveOverworldItems"] =
2678 proj["feature_flags"]["kSaveOverworldProperties"] =
2680 proj["feature_flags"]["kSaveDungeonObjects"] =
2682 proj["feature_flags"]["kSaveDungeonSprites"] =
2684 proj["feature_flags"]["kSaveDungeonRoomHeaders"] =
2686 proj["feature_flags"]["kSaveDungeonTorches"] =
2688 proj["feature_flags"]["kSaveDungeonPits"] = feature_flags.dungeon.kSavePits;
2689 proj["feature_flags"]["kSaveDungeonBlocks"] =
2691 proj["feature_flags"]["kSaveDungeonCollision"] =
2693 proj["feature_flags"]["kSaveDungeonWaterFillZones"] =
2695 proj["feature_flags"]["kSaveDungeonChests"] =
2697 proj["feature_flags"]["kSaveDungeonPotItems"] =
2699 proj["feature_flags"]["kSaveDungeonPalettes"] =
2701 proj["feature_flags"]["kSaveGraphicsSheet"] =
2703 proj["feature_flags"]["kSaveAllPalettes"] = feature_flags.kSaveAllPalettes;
2704 proj["feature_flags"]["kSaveGfxGroups"] = feature_flags.kSaveGfxGroups;
2705 proj["feature_flags"]["kSaveMessages"] = feature_flags.kSaveMessages;
2706
2707 // Workspace settings
2708 proj["workspace_settings"]["auto_save_enabled"] =
2710 proj["workspace_settings"]["auto_save_interval"] =
2712 proj["workspace_settings"]["backup_on_save"] =
2714 proj["workspace_settings"]["backup_retention_count"] =
2716 proj["workspace_settings"]["backup_keep_daily"] =
2718 proj["workspace_settings"]["backup_keep_daily_days"] =
2720
2721 auto& agent = proj["agent_settings"];
2722 agent["ai_provider"] = agent_settings.ai_provider;
2723 agent["ai_model"] = agent_settings.ai_model;
2724 agent["ollama_host"] = agent_settings.ollama_host;
2725 agent["gemini_api_key"] = agent_settings.gemini_api_key;
2726 agent["use_custom_prompt"] = agent_settings.use_custom_prompt;
2727 agent["custom_system_prompt"] = agent_settings.custom_system_prompt;
2728 agent["show_reasoning"] = agent_settings.show_reasoning;
2729 agent["verbose"] = agent_settings.verbose;
2730 agent["max_tool_iterations"] = agent_settings.max_tool_iterations;
2731 agent["max_retry_attempts"] = agent_settings.max_retry_attempts;
2732 agent["temperature"] = agent_settings.temperature;
2733 agent["top_p"] = agent_settings.top_p;
2734 agent["max_output_tokens"] = agent_settings.max_output_tokens;
2735 agent["stream_responses"] = agent_settings.stream_responses;
2736 agent["favorite_models"] = agent_settings.favorite_models;
2737 agent["model_chain"] = agent_settings.model_chain;
2738 agent["chain_mode"] = agent_settings.chain_mode;
2739 agent["enable_tool_resources"] = agent_settings.enable_tool_resources;
2740 agent["enable_tool_dungeon"] = agent_settings.enable_tool_dungeon;
2741 agent["enable_tool_overworld"] = agent_settings.enable_tool_overworld;
2742 agent["enable_tool_messages"] = agent_settings.enable_tool_messages;
2743 agent["enable_tool_dialogue"] = agent_settings.enable_tool_dialogue;
2744 agent["enable_tool_gui"] = agent_settings.enable_tool_gui;
2745 agent["enable_tool_music"] = agent_settings.enable_tool_music;
2746 agent["enable_tool_sprite"] = agent_settings.enable_tool_sprite;
2747 agent["enable_tool_emulator"] = agent_settings.enable_tool_emulator;
2748 agent["enable_tool_memory_inspector"] =
2750 agent["builder_blueprint_path"] = agent_settings.builder_blueprint_path;
2751
2752 if (!rom_address_overrides.addresses.empty()) {
2753 auto& addrs = proj["rom_addresses"];
2754 for (const auto& [key, value] : rom_address_overrides.addresses) {
2755 addrs[key] = value;
2756 }
2757 }
2758
2759 if (!custom_object_files.empty()) {
2760 auto& objs = proj["custom_objects"];
2761 for (const auto& [object_id, files] : custom_object_files) {
2762 objs[absl::StrFormat("0x%X", object_id)] = files;
2763 }
2764 }
2765
2766 // Build settings
2767 proj["build_script"] = build_script;
2768 proj["git_repository"] = git_repository;
2769 proj["track_changes"] = track_changes;
2770
2771 // Write to file
2772 std::ofstream file(filepath);
2773 if (!file.is_open()) {
2774 return absl::InvalidArgumentError(
2775 absl::StrFormat("Cannot write JSON project file: %s", filepath));
2776 }
2777
2778 file << j.dump(2); // Pretty print with 2-space indent
2779 return absl::OkStatus();
2780}
2781
2782#endif // YAZE_ENABLE_JSON_PROJECT_FORMAT
2783
2784// RecentFilesManager implementation
2786 auto config_dir = util::PlatformPaths::GetConfigDirectory();
2787 if (!config_dir.ok()) {
2788 return ""; // Or handle error appropriately
2789 }
2790 return (*config_dir / kRecentFilesFilename).string();
2791}
2792
2794#ifdef __EMSCRIPTEN__
2795 auto status = platform::WasmStorage::SaveProject(
2796 kRecentFilesFilename, absl::StrJoin(recent_files_, "\n"));
2797 if (!status.ok()) {
2798 LOG_WARN("RecentFilesManager", "Could not persist recent files: %s",
2799 status.ToString().c_str());
2800 }
2801 return;
2802#endif
2803 // Ensure config directory exists
2804 auto config_dir_status = util::PlatformPaths::GetConfigDirectory();
2805 if (!config_dir_status.ok()) {
2806 LOG_ERROR("Project", "Failed to get or create config directory: %s",
2807 config_dir_status.status().ToString().c_str());
2808 return;
2809 }
2810
2811 std::string filepath = GetFilePath();
2812 std::ofstream file(filepath);
2813 if (!file.is_open()) {
2814 LOG_WARN("RecentFilesManager", "Could not save recent files to %s",
2815 filepath.c_str());
2816 return;
2817 }
2818
2819 for (const auto& file_path : recent_files_) {
2820 file << file_path << std::endl;
2821 }
2822}
2823
2825#ifdef __EMSCRIPTEN__
2826 auto storage_or = platform::WasmStorage::LoadProject(kRecentFilesFilename);
2827 if (!storage_or.ok()) {
2828 return;
2829 }
2830 recent_files_.clear();
2831 std::istringstream stream(storage_or.value());
2832 std::string line;
2833 while (std::getline(stream, line)) {
2834 if (!line.empty()) {
2835 recent_files_.push_back(line);
2836 }
2837 }
2839 return;
2840#else
2841 std::string filepath = GetFilePath();
2842 std::ifstream file(filepath);
2843 if (!file.is_open()) {
2844 // File doesn't exist yet, which is fine
2845 return;
2846 }
2847
2848 recent_files_.clear();
2849 std::string line;
2850 while (std::getline(file, line)) {
2851 if (!line.empty()) {
2852 recent_files_.push_back(line);
2853 }
2854 }
2856#endif
2857}
2858
2859} // namespace project
2860} // namespace yaze
void Clear()
Clear any loaded manifest state.
const ProjectRegistry & project_registry() const
bool HasProjectRegistry() const
absl::Status LoadProjectRegistry(const std::string &code_folder)
Load project registry data from the code folder.
absl::Status LoadFromFile(const std::string &filepath)
Load manifest from a JSON file path.
bool loaded() const
Check if the manifest has been loaded.
static std::vector< std::string > FindProjectsInDirectory(const std::string &directory)
Definition project.cc:1975
static absl::Status ValidateProjectStructure(const YazeProject &project)
Definition project.cc:2038
static absl::StatusOr< YazeProject > CreateFromTemplate(const std::string &template_name, const std::string &project_name, const std::string &base_path)
Definition project.cc:1933
static std::vector< std::string > GetRecommendedFixesForProject(const YazeProject &project)
Definition project.cc:2043
static std::vector< ProjectTemplate > GetProjectTemplates()
Definition project.cc:1786
static absl::Status BackupProject(const YazeProject &project)
Definition project.cc:2005
std::string GetFilePath() const
Definition project.cc:2785
std::vector< std::string > recent_files_
Definition project.h:483
static absl::StatusOr< std::filesystem::path > GetConfigDirectory()
Get the user-specific configuration directory for YAZE.
void SetHackManifest(const core::HackManifest *manifest)
Set the hack manifest reference for ASM-defined labels.
#define YAZE_VERSION_STRING
#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 LOG_INFO(category, format,...)
Definition log.h:105
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
float ParseFloat(const std::string &value)
Definition project.cc:71
std::string ResolveOptionalPath(const std::filesystem::path &base_dir, const std::string &value)
Definition project.cc:175
std::vector< uint16_t > ParseHexUintList(const std::string &value)
Definition project.cc:96
std::string ToLowerCopy(std::string value)
Definition project.cc:42
bool ParseBool(const std::string &value)
Definition project.cc:67
std::pair< std::string, std::string > ParseDefineToken(const std::string &value)
Definition project.cc:167
std::optional< uint32_t > ParseHexUint32(const std::string &value)
Definition project.cc:124
std::string FormatHexUint32(uint32_t value)
Definition project.cc:150
std::string SanitizeStorageKey(absl::string_view input)
Definition project.cc:154
std::string FormatHexUintList(const std::vector< uint16_t > &values)
Definition project.cc:144
std::string BasenameLower(const std::string &path)
Definition project.cc:187
std::vector< std::string > ParseStringList(const std::string &value)
Definition project.cc:79
std::string RomRoleToString(RomRole role)
Definition project.cc:192
RomRole ParseRomRole(absl::string_view value)
Definition project.cc:206
const std::string kRecentFilesFilename
Definition project.h:420
RomWritePolicy ParseRomWritePolicy(absl::string_view value)
Definition project.cc:232
std::string RomWritePolicyToString(RomWritePolicy policy)
Definition project.cc:220
ResourceLabelProvider & GetResourceLabels()
Get the global ResourceLabelProvider instance.
struct yaze::core::FeatureFlags::Flags::Dungeon dungeon
struct yaze::core::FeatureFlags::Flags::Overworld overworld
std::unordered_map< std::string, std::unordered_map< std::string, std::string > > all_resource_labels
std::unordered_map< std::string, uint32_t > addresses
std::vector< uint16_t > track_object_ids
Definition project.h:100
std::vector< uint16_t > minecart_sprite_ids
Definition project.h:101
std::vector< uint16_t > track_stop_tiles
Definition project.h:96
std::vector< uint16_t > track_tiles
Definition project.h:95
std::vector< uint16_t > track_switch_tiles
Definition project.h:97
std::vector< std::string > tags
Definition project.h:43
std::string CreateOrGetLabel(const std::string &type, const std::string &key, const std::string &defaultValue)
Definition project.cc:2193
std::string GetLabel(const std::string &type, const std::string &key)
Definition project.cc:2180
void EditLabel(const std::string &type, const std::string &key, const std::string &newValue)
Definition project.cc:2162
bool LoadLabels(const std::string &filename)
Definition project.cc:2078
void SelectableLabelWithNameEdit(bool selected, const std::string &type, const std::string &key, const std::string &defaultValue)
Definition project.cc:2168
std::unordered_map< std::string, std::unordered_map< std::string, std::string > > labels_
Definition project.h:416
std::string expected_hash
Definition project.h:109
RomWritePolicy write_policy
Definition project.h:110
std::map< std::string, std::string > custom_keybindings
Definition project.h:84
std::vector< std::string > saved_layouts
Definition project.h:65
std::map< std::string, bool > editor_visibility
Definition project.h:86
std::vector< std::string > recent_files
Definition project.h:85
std::vector< std::string > favorite_models
Definition project.h:241
std::vector< std::string > model_chain
Definition project.h:242
Modern project structure with comprehensive settings consolidation.
Definition project.h:164
std::string rom_backup_folder
Definition project.h:173
std::unordered_map< int, std::vector< std::string > > custom_object_files
Definition project.h:189
absl::Status ResetToDefaults()
Definition project.cc:1197
std::string custom_objects_folder
Definition project.h:184
absl::Status RepairProject()
Definition project.cc:1258
std::string MakeStorageKey(absl::string_view suffix) const
Definition project.cc:484
static std::string ResolveBundleRoot(const std::string &path)
Definition project.cc:297
struct yaze::project::YazeProject::MusicPersistence music_persistence
absl::StatusOr< std::string > SerializeToString() const
Definition project.cc:500
std::string zscream_project_file
Definition project.h:258
absl::Status ExportForZScream(const std::string &target_path)
Definition project.cc:1164
ProjectMetadata metadata
Definition project.h:166
absl::Status ImportZScreamProject(const std::string &zscream_project_path)
Definition project.cc:1141
absl::Status SaveAllSettings()
Definition project.cc:1192
absl::Status ImportLabelsFromZScreamContent(const std::string &content)
Import labels from ZScream format content directly.
Definition project.cc:2301
std::string git_repository
Definition project.h:218
core::HackManifest hack_manifest
Definition project.h:204
void InitializeResourceLabelProvider()
Initialize the global ResourceLabelProvider with this project's labels.
Definition project.cc:2325
absl::Status ParseFromString(const std::string &content)
Definition project.cc:795
std::vector< std::string > additional_roms
Definition project.h:174
std::string patches_folder
Definition project.h:180
absl::Status LoadFromYazeFormat(const std::string &project_path)
Definition project.cc:1082
std::unordered_map< std::string, std::unordered_map< std::string, std::string > > resource_labels
Definition project.h:197
std::string GenerateProjectId() const
Definition project.cc:1777
absl::Status Create(const std::string &project_name, const std::string &base_path)
Definition project.cc:244
std::string assets_folder
Definition project.h:179
absl::Status SaveToYazeFormat()
Definition project.cc:1103
absl::Status LoadAllSettings()
Definition project.cc:1186
std::string labels_filename
Definition project.h:181
std::vector< std::string > asm_sources
Definition project.h:215
std::string hack_manifest_file
Definition project.h:186
std::string GetDisplayName() const
Definition project.cc:1292
std::vector< std::string > GetMissingFiles() const
Definition project.cc:1237
WorkspaceSettings workspace_settings
Definition project.h:193
std::string GetZ3dkArtifactPath(absl::string_view artifact_name) const
Definition project.cc:1398
std::string output_folder
Definition project.h:211
std::string asm_entry_point
Definition project.h:214
std::string GetRelativePath(const std::string &absolute_path) const
Definition project.cc:1299
absl::Status InitializeEmbeddedLabels(const std::unordered_map< std::string, std::unordered_map< std::string, std::string > > &labels)
Definition project.cc:2208
absl::Status SaveAs(const std::string &new_path)
Definition project.cc:472
struct yaze::project::YazeProject::AgentSettings agent_settings
DungeonOverlaySettings dungeon_overlay
Definition project.h:194
absl::Status ImportFromZScreamFormat(const std::string &project_path)
Definition project.cc:1376
std::string GetAbsolutePath(const std::string &relative_path) const
Definition project.cc:1319
std::string GetLabel(const std::string &resource_type, int id, const std::string &default_value="") const
Definition project.cc:2266
absl::Status Open(const std::string &project_path)
Definition project.cc:323
absl::Status ImportLabelsFromZScream(const std::string &filepath)
Import labels from a ZScream DefaultNames.txt file.
Definition project.cc:2281
std::string last_build_hash
Definition project.h:220
std::map< std::string, std::string > zscream_mappings
Definition project.h:259
absl::Status Validate() const
Definition project.cc:1202
core::FeatureFlags::Flags feature_flags
Definition project.h:192
std::vector< std::string > build_configurations
Definition project.h:212
core::RomAddressOverrides rom_address_overrides
Definition project.h:195
std::string symbols_filename
Definition project.h:182
Z3dkSettings z3dk_settings
Definition project.h:207
std::vector< std::string > include_paths
Definition project.h:131
std::string std_includes_path
Definition project.h:135
std::vector< std::string > main_files
Definition project.h:134
std::vector< std::string > emits
Definition project.h:133
std::vector< std::pair< std::string, std::string > > defines
Definition project.h:132
Z3dkArtifactPaths artifact_paths
Definition project.h:152
std::optional< bool > lsp_log_enabled
Definition project.h:142
std::vector< Z3dkMemoryRange > prohibited_memory_ranges
Definition project.h:141
std::string std_defines_path
Definition project.h:136