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