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_chests="
740 << (feature_flags.dungeon.kSaveChests ? "true" : "false") << "\n";
741 file << "save_dungeon_pot_items="
742 << (feature_flags.dungeon.kSavePotItems ? "true" : "false") << "\n";
743 file << "save_dungeon_entrances="
744 << (feature_flags.dungeon.kSaveEntrances ? "true" : "false") << "\n";
745 file << "save_dungeon_palettes="
746 << (feature_flags.dungeon.kSavePalettes ? "true" : "false") << "\n";
747 file << "save_graphics_sheet="
748 << (feature_flags.kSaveGraphicsSheet ? "true" : "false") << "\n";
749 file << "save_all_palettes="
750 << (feature_flags.kSaveAllPalettes ? "true" : "false") << "\n";
751 file << "save_gfx_groups="
752 << (feature_flags.kSaveGfxGroups ? "true" : "false") << "\n";
753 file << "save_messages=" << (feature_flags.kSaveMessages ? "true" : "false")
754 << "\n";
755 file << "enable_custom_objects="
756 << (feature_flags.kEnableCustomObjects ? "true" : "false") << "\n\n";
757
758 // Workspace settings section
759 file << "[workspace]\n";
760 file << "font_global_scale=" << workspace_settings.font_global_scale << "\n";
761 file << "dark_mode=" << (workspace_settings.dark_mode ? "true" : "false")
762 << "\n";
763 file << "ui_theme=" << workspace_settings.ui_theme << "\n";
764 file << "autosave_enabled="
765 << (workspace_settings.autosave_enabled ? "true" : "false") << "\n";
766 file << "autosave_interval_secs=" << workspace_settings.autosave_interval_secs
767 << "\n";
768 file << "backup_on_save="
769 << (workspace_settings.backup_on_save ? "true" : "false") << "\n";
770 file << "backup_retention_count=" << workspace_settings.backup_retention_count
771 << "\n";
772 file << "backup_keep_daily="
773 << (workspace_settings.backup_keep_daily ? "true" : "false") << "\n";
774 file << "backup_keep_daily_days=" << workspace_settings.backup_keep_daily_days
775 << "\n";
776 file << "show_grid=" << (workspace_settings.show_grid ? "true" : "false")
777 << "\n";
778 file << "show_collision="
779 << (workspace_settings.show_collision ? "true" : "false") << "\n";
780 file << "prefer_hmagic_names="
781 << (workspace_settings.prefer_hmagic_names ? "true" : "false") << "\n";
782 file << "last_layout_preset=" << workspace_settings.last_layout_preset
783 << "\n";
784 file << "saved_layouts="
785 << absl::StrJoin(workspace_settings.saved_layouts, ",") << "\n";
786 file << "recent_files=" << absl::StrJoin(workspace_settings.recent_files, ",")
787 << "\n\n";
788
789 // Dungeon overlay settings section
790 auto track_tiles = dungeon_overlay.track_tiles;
791 if (track_tiles.empty()) {
792 for (uint16_t tile = 0xB0; tile <= 0xBE; ++tile) {
793 track_tiles.push_back(tile);
794 }
795 }
796 auto track_stop_tiles = dungeon_overlay.track_stop_tiles;
797 if (track_stop_tiles.empty()) {
798 track_stop_tiles = {0xB7, 0xB8, 0xB9, 0xBA};
799 }
800 auto track_switch_tiles = dungeon_overlay.track_switch_tiles;
801 if (track_switch_tiles.empty()) {
802 track_switch_tiles = {0xD0, 0xD1, 0xD2, 0xD3};
803 }
804 auto track_object_ids = dungeon_overlay.track_object_ids;
805 if (track_object_ids.empty()) {
806 track_object_ids = {0x31};
807 }
808 auto minecart_sprite_ids = dungeon_overlay.minecart_sprite_ids;
809 if (minecart_sprite_ids.empty()) {
810 minecart_sprite_ids = {0xA3};
811 }
812 file << "[dungeon_overlay]\n";
813 file << "track_tiles=" << FormatHexUintList(track_tiles) << "\n";
814 file << "track_stop_tiles=" << FormatHexUintList(track_stop_tiles) << "\n";
815 file << "track_switch_tiles=" << FormatHexUintList(track_switch_tiles)
816 << "\n";
817 file << "track_object_ids=" << FormatHexUintList(track_object_ids) << "\n";
818 file << "minecart_sprite_ids=" << FormatHexUintList(minecart_sprite_ids)
819 << "\n\n";
820
821 if (!rom_address_overrides.addresses.empty()) {
822 file << "[rom_addresses]\n";
823 for (const auto& [key, value] : rom_address_overrides.addresses) {
824 file << key << "=" << FormatHexUint32(value) << "\n";
825 }
826 file << "\n";
827 }
828
829 if (!custom_object_files.empty()) {
830 file << "[custom_objects]\n";
831 for (const auto& [object_id, files] : custom_object_files) {
832 file << absl::StrFormat("object_0x%X", object_id) << "="
833 << absl::StrJoin(files, ",") << "\n";
834 }
835 file << "\n";
836 }
837
838 // AI Agent settings section
839 file << "[agent_settings]\n";
840 file << "ai_provider=" << agent_settings.ai_provider << "\n";
841 file << "ai_model=" << agent_settings.ai_model << "\n";
842 file << "ollama_host=" << agent_settings.ollama_host << "\n";
843 file << "gemini_api_key=" << agent_settings.gemini_api_key << "\n";
844 file << "custom_system_prompt="
846 file << "use_custom_prompt="
847 << (agent_settings.use_custom_prompt ? "true" : "false") << "\n";
848 file << "show_reasoning="
849 << (agent_settings.show_reasoning ? "true" : "false") << "\n";
850 file << "verbose=" << (agent_settings.verbose ? "true" : "false") << "\n";
851 file << "max_tool_iterations=" << agent_settings.max_tool_iterations << "\n";
852 file << "max_retry_attempts=" << agent_settings.max_retry_attempts << "\n";
853 file << "temperature=" << agent_settings.temperature << "\n";
854 file << "top_p=" << agent_settings.top_p << "\n";
855 file << "max_output_tokens=" << agent_settings.max_output_tokens << "\n";
856 file << "stream_responses="
857 << (agent_settings.stream_responses ? "true" : "false") << "\n";
858 file << "favorite_models="
859 << absl::StrJoin(agent_settings.favorite_models, ",") << "\n";
860 file << "model_chain=" << absl::StrJoin(agent_settings.model_chain, ",")
861 << "\n";
862 file << "chain_mode=" << agent_settings.chain_mode << "\n";
863 file << "enable_tool_resources="
864 << (agent_settings.enable_tool_resources ? "true" : "false") << "\n";
865 file << "enable_tool_dungeon="
866 << (agent_settings.enable_tool_dungeon ? "true" : "false") << "\n";
867 file << "enable_tool_overworld="
868 << (agent_settings.enable_tool_overworld ? "true" : "false") << "\n";
869 file << "enable_tool_messages="
870 << (agent_settings.enable_tool_messages ? "true" : "false") << "\n";
871 file << "enable_tool_dialogue="
872 << (agent_settings.enable_tool_dialogue ? "true" : "false") << "\n";
873 file << "enable_tool_gui="
874 << (agent_settings.enable_tool_gui ? "true" : "false") << "\n";
875 file << "enable_tool_music="
876 << (agent_settings.enable_tool_music ? "true" : "false") << "\n";
877 file << "enable_tool_sprite="
878 << (agent_settings.enable_tool_sprite ? "true" : "false") << "\n";
879 file << "enable_tool_emulator="
880 << (agent_settings.enable_tool_emulator ? "true" : "false") << "\n";
881 file << "enable_tool_memory_inspector="
882 << (agent_settings.enable_tool_memory_inspector ? "true" : "false")
883 << "\n";
884 file << "builder_blueprint_path=" << agent_settings.builder_blueprint_path
885 << "\n\n";
886
887 // Custom keybindings section
889 file << "[keybindings]\n";
890 for (const auto& [key, value] : workspace_settings.custom_keybindings) {
891 file << key << "=" << value << "\n";
892 }
893 file << "\n";
894 }
895
896 // Editor visibility section
898 file << "[editor_visibility]\n";
899 for (const auto& [key, value] : workspace_settings.editor_visibility) {
900 file << key << "=" << (value ? "true" : "false") << "\n";
901 }
902 file << "\n";
903 }
904
905 // Resource labels sections
906 for (const auto& [type, labels] : resource_labels) {
907 if (!labels.empty()) {
908 file << "[labels_" << type << "]\n";
909 for (const auto& [key, value] : labels) {
910 file << key << "=" << value << "\n";
911 }
912 file << "\n";
913 }
914 }
915
916 // Build settings section
917 file << "[build]\n";
918 file << "build_script=" << build_script << "\n";
919 file << "output_folder=" << GetRelativePath(output_folder) << "\n";
920 file << "git_repository=" << git_repository << "\n";
921 file << "track_changes=" << (track_changes ? "true" : "false") << "\n";
922 file << "build_configurations=" << absl::StrJoin(build_configurations, ",")
923 << "\n";
924 file << "build_target=" << build_target << "\n";
925 file << "asm_entry_point=" << asm_entry_point << "\n";
926 file << "asm_sources=" << absl::StrJoin(asm_sources, ",") << "\n";
927 file << "last_build_hash=" << last_build_hash << "\n";
928 file << "build_number=" << build_number << "\n\n";
929
930 // Music persistence section (for WASM/offline state)
931 file << "[music]\n";
932 file << "persist_custom_music="
933 << (music_persistence.persist_custom_music ? "true" : "false") << "\n";
934 file << "storage_key=" << music_persistence.storage_key << "\n";
935 file << "last_saved_at=" << music_persistence.last_saved_at << "\n\n";
936
937 // ZScream compatibility section
938 if (!zscream_project_file.empty()) {
939 file << "[zscream_compatibility]\n";
940 file << "original_project_file=" << zscream_project_file << "\n";
941 for (const auto& [key, value] : zscream_mappings) {
942 file << key << "=" << value << "\n";
943 }
944 file << "\n";
945 }
946
947 file << "# End of YAZE Project File\n";
948 return file.str();
949}
950
951absl::Status YazeProject::ParseFromString(const std::string& content) {
952 std::istringstream stream(content);
953 std::string line;
954 std::string current_section;
955
956 while (std::getline(stream, line)) {
957 if (line.empty() || line[0] == '#')
958 continue;
959
960 if (line.front() == '[' && line.back() == ']') {
961 current_section = line.substr(1, line.length() - 2);
962 continue;
963 }
964
965 auto [key, value] = ParseKeyValue(line);
966 if (key.empty())
967 continue;
968
969 if (current_section == "project") {
970 if (key == "name")
971 name = value;
972 else if (key == "description")
973 metadata.description = value;
974 else if (key == "author")
975 metadata.author = value;
976 else if (key == "license")
977 metadata.license = value;
978 else if (key == "version")
979 metadata.version = value;
980 else if (key == "created_date")
981 metadata.created_date = value;
982 else if (key == "last_modified")
983 metadata.last_modified = value;
984 else if (key == "yaze_version")
985 metadata.yaze_version = value;
986 else if (key == "created_by")
987 metadata.created_by = value;
988 else if (key == "tags")
989 metadata.tags = ParseStringList(value);
990 else if (key == "project_id")
991 metadata.project_id = value;
992 } else if (current_section == "files") {
993 if (key == "rom_filename")
994 rom_filename = value;
995 else if (key == "rom_backup_folder")
996 rom_backup_folder = value;
997 else if (key == "code_folder")
998 code_folder = value;
999 else if (key == "assets_folder")
1000 assets_folder = value;
1001 else if (key == "patches_folder")
1002 patches_folder = value;
1003 else if (key == "labels_filename")
1004 labels_filename = value;
1005 else if (key == "symbols_filename")
1006 symbols_filename = value;
1007 else if (key == "output_folder")
1008 output_folder = value;
1009 else if (key == "custom_objects_folder")
1010 custom_objects_folder = value;
1011 else if (key == "hack_manifest_file")
1012 hack_manifest_file = value;
1013 else if (key == "additional_roms")
1014 additional_roms = ParseStringList(value);
1015 } else if (current_section == "rom") {
1016 if (key == "role")
1018 else if (key == "expected_hash")
1020 else if (key == "write_policy")
1022 } else if (current_section == "feature_flags") {
1023 if (key == "load_custom_overworld")
1024 feature_flags.overworld.kLoadCustomOverworld = ParseBool(value);
1025 else if (key == "apply_zs_custom_overworld_asm")
1027 else if (key == "save_dungeon_maps")
1028 feature_flags.kSaveDungeonMaps = ParseBool(value);
1029 else if (key == "save_overworld_maps")
1030 feature_flags.overworld.kSaveOverworldMaps = ParseBool(value);
1031 else if (key == "save_overworld_entrances")
1033 else if (key == "save_overworld_exits")
1034 feature_flags.overworld.kSaveOverworldExits = ParseBool(value);
1035 else if (key == "save_overworld_items")
1036 feature_flags.overworld.kSaveOverworldItems = ParseBool(value);
1037 else if (key == "save_overworld_properties")
1039 else if (key == "save_dungeon_objects")
1040 feature_flags.dungeon.kSaveObjects = ParseBool(value);
1041 else if (key == "save_dungeon_sprites")
1042 feature_flags.dungeon.kSaveSprites = ParseBool(value);
1043 else if (key == "save_dungeon_room_headers")
1044 feature_flags.dungeon.kSaveRoomHeaders = ParseBool(value);
1045 else if (key == "save_dungeon_torches")
1046 feature_flags.dungeon.kSaveTorches = ParseBool(value);
1047 else if (key == "save_dungeon_pits")
1048 feature_flags.dungeon.kSavePits = ParseBool(value);
1049 else if (key == "save_dungeon_blocks")
1050 feature_flags.dungeon.kSaveBlocks = ParseBool(value);
1051 else if (key == "save_dungeon_collision")
1052 feature_flags.dungeon.kSaveCollision = ParseBool(value);
1053 else if (key == "save_dungeon_chests")
1054 feature_flags.dungeon.kSaveChests = ParseBool(value);
1055 else if (key == "save_dungeon_pot_items")
1056 feature_flags.dungeon.kSavePotItems = ParseBool(value);
1057 else if (key == "save_dungeon_entrances")
1058 feature_flags.dungeon.kSaveEntrances = ParseBool(value);
1059 else if (key == "save_dungeon_palettes")
1060 feature_flags.dungeon.kSavePalettes = ParseBool(value);
1061 else if (key == "save_graphics_sheet")
1062 feature_flags.kSaveGraphicsSheet = ParseBool(value);
1063 else if (key == "save_all_palettes")
1064 feature_flags.kSaveAllPalettes = ParseBool(value);
1065 else if (key == "save_gfx_groups")
1066 feature_flags.kSaveGfxGroups = ParseBool(value);
1067 else if (key == "save_messages")
1068 feature_flags.kSaveMessages = ParseBool(value);
1069 else if (key == "enable_custom_objects")
1070 feature_flags.kEnableCustomObjects = ParseBool(value);
1071 } else if (current_section == "workspace") {
1072 if (key == "font_global_scale")
1073 workspace_settings.font_global_scale = ParseFloat(value);
1074 else if (key == "dark_mode")
1075 workspace_settings.dark_mode = ParseBool(value);
1076 else if (key == "ui_theme")
1078 else if (key == "autosave_enabled")
1079 workspace_settings.autosave_enabled = ParseBool(value);
1080 else if (key == "autosave_interval_secs")
1081 workspace_settings.autosave_interval_secs = ParseFloat(value);
1082 else if (key == "backup_on_save")
1083 workspace_settings.backup_on_save = ParseBool(value);
1084 else if (key == "backup_retention_count")
1085 workspace_settings.backup_retention_count = std::stoi(value);
1086 else if (key == "backup_keep_daily")
1087 workspace_settings.backup_keep_daily = ParseBool(value);
1088 else if (key == "backup_keep_daily_days")
1089 workspace_settings.backup_keep_daily_days = std::stoi(value);
1090 else if (key == "show_grid")
1091 workspace_settings.show_grid = ParseBool(value);
1092 else if (key == "show_collision")
1093 workspace_settings.show_collision = ParseBool(value);
1094 else if (key == "prefer_hmagic_names")
1095 workspace_settings.prefer_hmagic_names = ParseBool(value);
1096 else if (key == "last_layout_preset")
1098 else if (key == "saved_layouts")
1099 workspace_settings.saved_layouts = ParseStringList(value);
1100 else if (key == "recent_files")
1101 workspace_settings.recent_files = ParseStringList(value);
1102 } else if (current_section == "dungeon_overlay") {
1103 if (key == "track_tiles")
1104 dungeon_overlay.track_tiles = ParseHexUintList(value);
1105 else if (key == "track_stop_tiles")
1106 dungeon_overlay.track_stop_tiles = ParseHexUintList(value);
1107 else if (key == "track_switch_tiles")
1108 dungeon_overlay.track_switch_tiles = ParseHexUintList(value);
1109 else if (key == "track_object_ids")
1110 dungeon_overlay.track_object_ids = ParseHexUintList(value);
1111 else if (key == "minecart_sprite_ids")
1112 dungeon_overlay.minecart_sprite_ids = ParseHexUintList(value);
1113 } else if (current_section == "rom_addresses") {
1114 auto parsed = ParseHexUint32(value);
1115 if (parsed.has_value()) {
1116 rom_address_overrides.addresses[key] = *parsed;
1117 }
1118 } else if (current_section == "custom_objects") {
1119 std::string id_token = key;
1120 if (absl::StartsWith(id_token, "object_")) {
1121 id_token = id_token.substr(7);
1122 }
1123 auto parsed = ParseHexUint32(id_token);
1124 if (parsed.has_value()) {
1125 custom_object_files[static_cast<int>(*parsed)] = ParseStringList(value);
1126 }
1127 } else if (current_section == "agent_settings") {
1128 if (key == "ai_provider")
1130 else if (key == "ai_model")
1131 agent_settings.ai_model = value;
1132 else if (key == "ollama_host")
1134 else if (key == "gemini_api_key")
1136 else if (key == "custom_system_prompt")
1138 else if (key == "use_custom_prompt")
1139 agent_settings.use_custom_prompt = ParseBool(value);
1140 else if (key == "show_reasoning")
1141 agent_settings.show_reasoning = ParseBool(value);
1142 else if (key == "verbose")
1143 agent_settings.verbose = ParseBool(value);
1144 else if (key == "max_tool_iterations")
1145 agent_settings.max_tool_iterations = std::stoi(value);
1146 else if (key == "max_retry_attempts")
1147 agent_settings.max_retry_attempts = std::stoi(value);
1148 else if (key == "temperature")
1149 agent_settings.temperature = ParseFloat(value);
1150 else if (key == "top_p")
1151 agent_settings.top_p = ParseFloat(value);
1152 else if (key == "max_output_tokens")
1153 agent_settings.max_output_tokens = std::stoi(value);
1154 else if (key == "stream_responses")
1155 agent_settings.stream_responses = ParseBool(value);
1156 else if (key == "favorite_models")
1157 agent_settings.favorite_models = ParseStringList(value);
1158 else if (key == "model_chain")
1159 agent_settings.model_chain = ParseStringList(value);
1160 else if (key == "chain_mode")
1161 agent_settings.chain_mode = std::stoi(value);
1162 else if (key == "enable_tool_resources")
1163 agent_settings.enable_tool_resources = ParseBool(value);
1164 else if (key == "enable_tool_dungeon")
1165 agent_settings.enable_tool_dungeon = ParseBool(value);
1166 else if (key == "enable_tool_overworld")
1167 agent_settings.enable_tool_overworld = ParseBool(value);
1168 else if (key == "enable_tool_messages")
1169 agent_settings.enable_tool_messages = ParseBool(value);
1170 else if (key == "enable_tool_dialogue")
1171 agent_settings.enable_tool_dialogue = ParseBool(value);
1172 else if (key == "enable_tool_gui")
1173 agent_settings.enable_tool_gui = ParseBool(value);
1174 else if (key == "enable_tool_music")
1175 agent_settings.enable_tool_music = ParseBool(value);
1176 else if (key == "enable_tool_sprite")
1177 agent_settings.enable_tool_sprite = ParseBool(value);
1178 else if (key == "enable_tool_emulator")
1179 agent_settings.enable_tool_emulator = ParseBool(value);
1180 else if (key == "enable_tool_memory_inspector")
1182 else if (key == "builder_blueprint_path")
1184 } else if (current_section == "build") {
1185 if (key == "build_script")
1186 build_script = value;
1187 else if (key == "output_folder")
1188 output_folder = value;
1189 else if (key == "git_repository")
1190 git_repository = value;
1191 else if (key == "track_changes")
1192 track_changes = ParseBool(value);
1193 else if (key == "build_configurations")
1194 build_configurations = ParseStringList(value);
1195 else if (key == "build_target")
1196 build_target = value;
1197 else if (key == "asm_entry_point")
1198 asm_entry_point = value;
1199 else if (key == "asm_sources")
1200 asm_sources = ParseStringList(value);
1201 else if (key == "last_build_hash")
1202 last_build_hash = value;
1203 else if (key == "build_number")
1204 build_number = std::stoi(value);
1205 } else if (current_section.rfind("labels_", 0) == 0) {
1206 std::string label_type = current_section.substr(7);
1207 resource_labels[label_type][key] = value;
1208 } else if (current_section == "keybindings") {
1210 } else if (current_section == "editor_visibility") {
1211 workspace_settings.editor_visibility[key] = ParseBool(value);
1212 } else if (current_section == "zscream_compatibility") {
1213 if (key == "original_project_file")
1214 zscream_project_file = value;
1215 else
1216 zscream_mappings[key] = value;
1217 } else if (current_section == "music") {
1218 if (key == "persist_custom_music")
1219 music_persistence.persist_custom_music = ParseBool(value);
1220 else if (key == "storage_key")
1222 else if (key == "last_saved_at")
1224 }
1225 }
1226
1227 if (metadata.project_id.empty()) {
1229 }
1230 if (metadata.created_by.empty()) {
1231 metadata.created_by = "YAZE";
1232 }
1233 if (music_persistence.storage_key.empty()) {
1235 }
1236
1237 return absl::OkStatus();
1238}
1239
1240absl::Status YazeProject::LoadFromYazeFormat(const std::string& project_path) {
1241#ifdef __EMSCRIPTEN__
1242 auto storage_key = MakeStorageKey("project");
1243 auto storage_or = platform::WasmStorage::LoadProject(storage_key);
1244 if (storage_or.ok()) {
1245 return ParseFromString(storage_or.value());
1246 }
1247#endif // __EMSCRIPTEN__
1248
1249 std::ifstream file(project_path);
1250 if (!file.is_open()) {
1251 return absl::InvalidArgumentError(
1252 absl::StrFormat("Cannot open project file: %s", project_path));
1253 }
1254
1255 std::stringstream buffer;
1256 buffer << file.rdbuf();
1257 file.close();
1258 return ParseFromString(buffer.str());
1259}
1260
1261absl::Status YazeProject::SaveToYazeFormat(bool replace_existing) {
1262 // Update last modified timestamp
1263 auto now = std::chrono::system_clock::now();
1264 auto time_t = std::chrono::system_clock::to_time_t(now);
1265 std::stringstream ss;
1266 ss << std::put_time(std::localtime(&time_t), "%Y-%m-%d %H:%M:%S");
1267 metadata.last_modified = ss.str();
1268 if (music_persistence.storage_key.empty()) {
1270 }
1271
1272 // Ensure we serialize clean relative paths even if the user edited fields
1273 // into relative form (and avoid relying on cwd when opening later).
1275
1276 ASSIGN_OR_RETURN(auto serialized, SerializeToString());
1277
1278#ifdef __EMSCRIPTEN__
1279 auto storage_status = platform::WasmStorage::SaveProject(
1280 MakeStorageKey("project"), serialized, replace_existing);
1281 if (!storage_status.ok()) {
1282 return storage_status;
1283 }
1284#else
1285 if (!filepath.empty()) {
1287 WriteProjectFileAtomically(filepath, serialized, replace_existing));
1288 }
1289#endif
1290
1291 return absl::OkStatus();
1292}
1293
1295 const std::string& zscream_project_path) {
1296 // Basic ZScream project import (to be expanded based on ZScream format)
1297 zscream_project_file = zscream_project_path;
1299
1300 // Extract project name from path
1301 std::filesystem::path zs_path(zscream_project_path);
1302 name = zs_path.stem().string() + "_imported";
1303
1304 // Set up basic mapping for common fields
1305 zscream_mappings["rom_file"] = "rom_filename";
1306 zscream_mappings["source_code"] = "code_folder";
1307 zscream_mappings["project_name"] = "name";
1308
1310
1311 // TODO: Implement actual ZScream format parsing when format is known
1312 // For now, just create a project structure that can be manually configured
1313
1314 return absl::OkStatus();
1315}
1316
1317absl::Status YazeProject::ExportForZScream(const std::string& target_path) {
1318 // Create a simplified project file that ZScream might understand
1319 std::ofstream file(target_path);
1320 if (!file.is_open()) {
1321 return absl::InvalidArgumentError(
1322 absl::StrFormat("Cannot create ZScream project file: %s", target_path));
1323 }
1324
1325 // Write in a simple format that ZScream might understand
1326 file << "# ZScream Compatible Project File\n";
1327 file << "# Exported from YAZE " << metadata.yaze_version << "\n\n";
1328 file << "name=" << name << "\n";
1329 file << "rom_file=" << rom_filename << "\n";
1330 file << "source_code=" << code_folder << "\n";
1331 file << "description=" << metadata.description << "\n";
1332 file << "author=" << metadata.author << "\n";
1333 file << "created_with=YAZE " << metadata.yaze_version << "\n";
1334
1335 file.close();
1336 return absl::OkStatus();
1337}
1338
1340 // Consolidated loading of all settings from project file
1341 // This replaces scattered config loading throughout the application
1343}
1344
1346 // Consolidated saving of all settings to project file
1347 return SaveToYazeFormat();
1348}
1349
1352 return Save();
1353}
1354
1355absl::Status YazeProject::Validate() const {
1356 std::vector<std::string> errors;
1357
1358 if (name.empty())
1359 errors.push_back("Project name is required");
1360 if (filepath.empty())
1361 errors.push_back("Project file path is required");
1362 if (rom_filename.empty())
1363 errors.push_back("ROM file is required");
1364
1365#ifndef __EMSCRIPTEN__
1366 // Check if files exist
1367 if (!rom_filename.empty() &&
1368 !std::filesystem::exists(GetAbsolutePath(rom_filename))) {
1369 errors.push_back("ROM file does not exist: " + rom_filename);
1370 }
1371
1372 if (!code_folder.empty() &&
1373 !std::filesystem::exists(GetAbsolutePath(code_folder))) {
1374 errors.push_back("Code folder does not exist: " + code_folder);
1375 }
1376
1377 if (!labels_filename.empty() &&
1378 !std::filesystem::exists(GetAbsolutePath(labels_filename))) {
1379 errors.push_back("Labels file does not exist: " + labels_filename);
1380 }
1381
1382 if (!hack_manifest_file.empty()) {
1383 if (!std::filesystem::exists(GetAbsolutePath(hack_manifest_file))) {
1384 errors.push_back("Hack manifest file does not exist: " +
1386 } else if (!hack_manifest.loaded()) {
1387 errors.push_back("Hack manifest file failed to load: " +
1389 }
1390 }
1391#endif // __EMSCRIPTEN__
1392
1393 if (!errors.empty()) {
1394 return absl::InvalidArgumentError(absl::StrJoin(errors, "; "));
1395 }
1396
1397 return absl::OkStatus();
1398}
1399
1400std::vector<std::string> YazeProject::GetMissingFiles() const {
1401 std::vector<std::string> missing;
1402
1403#ifndef __EMSCRIPTEN__
1404 if (!rom_filename.empty() &&
1405 !std::filesystem::exists(GetAbsolutePath(rom_filename))) {
1406 missing.push_back(rom_filename);
1407 }
1408 if (!labels_filename.empty() &&
1409 !std::filesystem::exists(GetAbsolutePath(labels_filename))) {
1410 missing.push_back(labels_filename);
1411 }
1412 if (!symbols_filename.empty() &&
1413 !std::filesystem::exists(GetAbsolutePath(symbols_filename))) {
1414 missing.push_back(symbols_filename);
1415 }
1416 if (!hack_manifest_file.empty() &&
1417 !std::filesystem::exists(GetAbsolutePath(hack_manifest_file))) {
1418 missing.push_back(hack_manifest_file);
1419 }
1420#endif // __EMSCRIPTEN__
1421
1422 return missing;
1423}
1424
1426#ifdef __EMSCRIPTEN__
1427 // In the web build, filesystem layout is virtual; nothing to repair eagerly.
1428 return absl::OkStatus();
1429#else
1430 // Create missing directories
1431 std::vector<std::string> folders = {code_folder, assets_folder,
1434
1435 for (const auto& folder : folders) {
1436 if (!folder.empty()) {
1437 std::filesystem::path abs_path = GetAbsolutePath(folder);
1438 if (!std::filesystem::exists(abs_path)) {
1439 std::filesystem::create_directories(abs_path);
1440 }
1441 }
1442 }
1443
1444 // Create missing files with defaults
1445 if (!labels_filename.empty()) {
1446 std::filesystem::path abs_labels = GetAbsolutePath(labels_filename);
1447 if (!std::filesystem::exists(abs_labels)) {
1448 std::ofstream labels_file(abs_labels);
1449 labels_file << "# yaze Resource Labels\n";
1450 labels_file << "# Format: [type] key=value\n\n";
1451 labels_file.close();
1452 }
1453 }
1454
1455 return absl::OkStatus();
1456#endif
1457}
1458
1459std::string YazeProject::GetDisplayName() const {
1460 if (!metadata.description.empty()) {
1461 return metadata.description;
1462 }
1463 return name.empty() ? "Untitled Project" : name;
1464}
1465
1467 const std::string& absolute_path) const {
1468 if (absolute_path.empty() || filepath.empty())
1469 return absolute_path;
1470
1471 std::filesystem::path project_dir =
1472 std::filesystem::path(filepath).parent_path();
1473 std::filesystem::path abs_path(absolute_path);
1474
1475 try {
1476 std::filesystem::path relative =
1477 std::filesystem::relative(abs_path.lexically_normal(), project_dir);
1478 // Persist relative paths in a platform-neutral format for project files.
1479 return relative.generic_string();
1480 } catch (...) {
1481 // Return normalized absolute path if relative conversion fails.
1482 return abs_path.lexically_normal().generic_string();
1483 }
1484}
1485
1487 const std::string& relative_path) const {
1488 if (relative_path.empty() || filepath.empty())
1489 return relative_path;
1490
1491 std::filesystem::path project_dir =
1492 std::filesystem::path(filepath).parent_path();
1493 std::filesystem::path abs_path(relative_path);
1494 if (abs_path.is_absolute()) {
1495 abs_path = abs_path.lexically_normal();
1496 abs_path.make_preferred();
1497 return abs_path.string();
1498 }
1499 abs_path = (project_dir / abs_path).lexically_normal();
1500 abs_path.make_preferred();
1501
1502 return abs_path.string();
1503}
1504
1506#ifdef __EMSCRIPTEN__
1507 // Web builds rely on a virtual filesystem and often use relative paths.
1508 return;
1509#endif
1510 if (filepath.empty()) {
1511 return;
1512 }
1513
1514 auto normalize = [this](std::string* path) {
1515 if (!path || path->empty()) {
1516 return;
1517 }
1518 *path = GetAbsolutePath(*path);
1519 };
1520
1521 normalize(&rom_filename);
1522 normalize(&rom_backup_folder);
1523 normalize(&code_folder);
1524 normalize(&assets_folder);
1525 normalize(&patches_folder);
1526 normalize(&labels_filename);
1527 normalize(&symbols_filename);
1528 normalize(&custom_objects_folder);
1529 normalize(&hack_manifest_file);
1530 normalize(&output_folder);
1531
1532 for (auto& rom_path : additional_roms) {
1533 if (!rom_path.empty()) {
1534 rom_path = GetAbsolutePath(rom_path);
1535 }
1536 }
1537}
1538
1540 return name.empty() && rom_filename.empty() && code_folder.empty();
1541}
1542
1544 const std::string& project_path) {
1545 // TODO: Implement ZScream format parsing when format specification is
1546 // available For now, create a basic project that can be manually configured
1547
1548 std::filesystem::path zs_path(project_path);
1549 name = zs_path.stem().string() + "_imported";
1550 zscream_project_file = project_path;
1551
1553
1554 return absl::OkStatus();
1555}
1556
1560
1564
1566 absl::string_view artifact_name) const {
1567 std::filesystem::path base_dir;
1568 if (!output_folder.empty()) {
1569 base_dir = output_folder;
1570 } else if (!z3dk_settings.config_path.empty()) {
1571 base_dir = std::filesystem::path(z3dk_settings.config_path).parent_path();
1572 } else if (!code_folder.empty()) {
1573 base_dir = code_folder;
1574 } else if (!filepath.empty()) {
1575 base_dir = std::filesystem::path(filepath).parent_path();
1576 }
1577
1578 if (base_dir.empty()) {
1579 return std::string(artifact_name);
1580 }
1581 return (base_dir / std::string(artifact_name)).lexically_normal().string();
1582}
1583
1586
1587#if defined(YAZE_WITH_Z3DK) && __has_include("z3dk_core/config.h")
1588 std::vector<std::filesystem::path> candidates;
1589 auto add_candidate = [&candidates](const std::filesystem::path& candidate) {
1590 if (candidate.empty()) {
1591 return;
1592 }
1593 auto normalized = candidate.lexically_normal();
1594 if (std::find(candidates.begin(), candidates.end(), normalized) ==
1595 candidates.end()) {
1596 candidates.push_back(normalized);
1597 }
1598 };
1599
1600 if (!code_folder.empty()) {
1601 std::filesystem::path code_path(code_folder);
1602 if (!std::filesystem::is_directory(code_path)) {
1603 code_path = code_path.parent_path();
1604 }
1605 add_candidate(code_path / "z3dk.toml");
1606 }
1607
1608 if (!hack_manifest_file.empty()) {
1609 add_candidate(std::filesystem::path(hack_manifest_file).parent_path() /
1610 "z3dk.toml");
1611 }
1612
1613 if (!filepath.empty()) {
1614 add_candidate(std::filesystem::path(filepath).parent_path() / "z3dk.toml");
1615 }
1616
1617 for (const auto& candidate : candidates) {
1618 if (!std::filesystem::exists(candidate)) {
1619 continue;
1620 }
1621
1622 std::string error;
1623 z3dk::Config config = z3dk::LoadConfigFile(candidate.string(), &error);
1624 if (!error.empty()) {
1625 LOG_WARN("Project", "Failed to parse z3dk config '%s': %s",
1626 candidate.string().c_str(), error.c_str());
1627 continue;
1628 }
1629
1630 const std::filesystem::path base_dir = candidate.parent_path();
1631 z3dk_settings.loaded = true;
1632 z3dk_settings.config_path = candidate.string();
1633 if (config.preset.has_value()) {
1634 z3dk_settings.preset = *config.preset;
1635 }
1636
1637 z3dk_settings.include_paths.reserve(config.include_paths.size());
1638 for (const auto& include_path : config.include_paths) {
1639 z3dk_settings.include_paths.push_back(
1640 ResolveOptionalPath(base_dir, include_path));
1641 }
1642
1643 z3dk_settings.defines.reserve(config.defines.size());
1644 for (const auto& define : config.defines) {
1645 z3dk_settings.defines.push_back(ParseDefineToken(define));
1646 }
1647
1648 z3dk_settings.main_files.reserve(config.main_files.size());
1649 for (const auto& main_file : config.main_files) {
1650 z3dk_settings.main_files.push_back(
1651 ResolveOptionalPath(base_dir, main_file));
1652 }
1653
1654 if (config.std_includes_path.has_value()) {
1656 ResolveOptionalPath(base_dir, *config.std_includes_path);
1657 }
1658 if (config.std_defines_path.has_value()) {
1660 ResolveOptionalPath(base_dir, *config.std_defines_path);
1661 }
1662 if (config.mapper.has_value()) {
1663 z3dk_settings.mapper = *config.mapper;
1664 }
1665 if (config.rom_size.has_value()) {
1666 z3dk_settings.rom_size = *config.rom_size;
1667 }
1668 if (config.symbols_format.has_value()) {
1669 z3dk_settings.symbols_format = *config.symbols_format;
1670 }
1671 z3dk_settings.lsp_log_enabled = config.lsp_log_enabled;
1672 if (config.lsp_log_path.has_value()) {
1674 ResolveOptionalPath(base_dir, *config.lsp_log_path);
1675 }
1676
1677 z3dk_settings.emits.reserve(config.emits.size());
1678 for (const auto& emit_path : config.emits) {
1679 z3dk_settings.emits.push_back(ResolveOptionalPath(base_dir, emit_path));
1680 }
1681
1682 for (const auto& range : config.prohibited_memory_ranges) {
1684 {.start = range.start, .end = range.end, .reason = range.reason});
1685 }
1686
1688 config.warn_unused_symbols.value_or(true);
1690 config.warn_branch_outside_bank.value_or(true);
1691 z3dk_settings.warn_unknown_width = config.warn_unknown_width.value_or(true);
1692 z3dk_settings.warn_org_collision = config.warn_org_collision.value_or(true);
1694 config.warn_unauthorized_hook.value_or(true);
1695 z3dk_settings.warn_stack_balance = config.warn_stack_balance.value_or(true);
1696 z3dk_settings.warn_hook_return = config.warn_hook_return.value_or(true);
1697
1698 if (config.rom_path.has_value()) {
1699 z3dk_settings.rom_path = ResolveOptionalPath(base_dir, *config.rom_path);
1700 }
1701 if (config.symbols_path.has_value()) {
1703 ResolveOptionalPath(base_dir, *config.symbols_path);
1704 }
1705
1706 for (const auto& emit_path : z3dk_settings.emits) {
1707 const std::string basename = BasenameLower(emit_path);
1708 if (basename.ends_with(".mlb") &&
1711 } else if (basename == "sourcemap.json") {
1713 } else if (basename == "annotations.json") {
1715 } else if (basename == "hooks.json") {
1717 } else if (basename == "lint.json") {
1719 }
1720 }
1721
1723 if (!z3dk_settings.symbols_path.empty() &&
1724 BasenameLower(z3dk_settings.symbols_path).ends_with(".mlb")) {
1726 } else {
1728 GetZ3dkArtifactPath("symbols.mlb");
1729 }
1730 }
1733 GetZ3dkArtifactPath("sourcemap.json");
1734 }
1737 GetZ3dkArtifactPath("annotations.json");
1738 }
1741 GetZ3dkArtifactPath("hooks.json");
1742 }
1745 }
1746
1747 LOG_INFO("Project",
1748 "Loaded z3dk config from %s (%zu include paths, %zu defines)",
1749 z3dk_settings.config_path.c_str(),
1751 return;
1752 }
1753#endif
1754}
1755
1757#ifdef __EMSCRIPTEN__
1760 return; // Hack manifests not supported in web builds
1761#endif
1762
1763 // Clear previous state so we never keep a stale manifest across project loads.
1766
1767 std::filesystem::path loaded_manifest_path;
1768 auto load_manifest = [&](const std::filesystem::path& candidate,
1769 bool update_project_setting) -> bool {
1770 if (candidate.empty() || !std::filesystem::exists(candidate)) {
1771 return false;
1772 }
1773 auto status = hack_manifest.LoadFromFile(candidate.string());
1774 if (!status.ok()) {
1775 LOG_WARN("Project", "Failed to load hack manifest %s: %s",
1776 candidate.string().c_str(),
1777 std::string(status.message()).c_str());
1778 return false;
1779 }
1780 loaded_manifest_path = candidate;
1781 if (update_project_setting) {
1782 hack_manifest_file = GetRelativePath(candidate.string());
1783 }
1784 LOG_DEBUG("Project", "Loaded hack manifest: %s",
1785 candidate.string().c_str());
1787 return true;
1788 };
1789
1790 // Priority 1: An explicit hack_manifest_file setting is authoritative. Do
1791 // not silently replace a missing or malformed configured manifest with an
1792 // auto-discovered file; project validation must surface the bad reference.
1793 const bool has_explicit_manifest = !hack_manifest_file.empty();
1794 if (has_explicit_manifest) {
1795 (void)load_manifest(GetAbsolutePath(hack_manifest_file), false);
1796 }
1797
1798 // Priority 2: Auto-discover hack_manifest.json in code_folder.
1799 if (!has_explicit_manifest && !hack_manifest.loaded() &&
1800 !code_folder.empty()) {
1801 auto code_path = GetAbsolutePath(code_folder);
1802 auto candidate = std::filesystem::path(code_path) / "hack_manifest.json";
1803 (void)load_manifest(candidate, true);
1804 }
1805
1806 // Priority 3: Fallback to the project file directory (or its parent).
1807 if (!has_explicit_manifest && !hack_manifest.loaded() && !filepath.empty()) {
1808 const std::filesystem::path project_dir =
1809 std::filesystem::path(filepath).parent_path();
1810 (void)load_manifest(project_dir / "hack_manifest.json", true);
1811 if (!hack_manifest.loaded() && project_dir.has_parent_path()) {
1812 (void)load_manifest(project_dir.parent_path() / "hack_manifest.json",
1813 true);
1814 }
1815 }
1816
1817 if (hack_manifest.loaded()) {
1819 }
1820
1821 auto try_load_registry = [&](const std::filesystem::path& base) -> bool {
1822 if (base.empty()) {
1823 return false;
1824 }
1825 const auto planning = base / "Docs" / "Dev" / "Planning";
1826 if (!std::filesystem::exists(planning)) {
1827 return false;
1828 }
1829 auto status = hack_manifest.LoadProjectRegistry(base.string());
1830 if (!status.ok()) {
1831 LOG_WARN("Project", "Failed to load project registry from %s: %s",
1832 base.string().c_str(), std::string(status.message()).c_str());
1833 return false;
1834 }
1836 };
1837
1838 bool registry_loaded = false;
1839
1840 // Prefer configured code_folder when valid.
1841 if (!code_folder.empty()) {
1842 registry_loaded =
1843 try_load_registry(std::filesystem::path(GetAbsolutePath(code_folder)));
1844 }
1845
1846 // Fallback to the manifest directory if code_folder is stale/misconfigured.
1847 if (!registry_loaded && !loaded_manifest_path.empty()) {
1848 registry_loaded = try_load_registry(loaded_manifest_path.parent_path());
1849 }
1850
1851 // Last fallback: project directory.
1852 if (!registry_loaded && !filepath.empty()) {
1853 registry_loaded =
1854 try_load_registry(std::filesystem::path(filepath).parent_path());
1855 }
1856
1857 if (!registry_loaded) {
1858 if (!hack_manifest.loaded()) {
1859 return;
1860 }
1861 LOG_WARN("Project",
1862 "Hack manifest loaded but project registry was not found "
1863 "(code_folder='%s', manifest='%s')",
1864 code_folder.c_str(), loaded_manifest_path.string().c_str());
1865 return;
1866 }
1867
1868 // Inject all Oracle resource labels into project resource_labels.
1869 size_t injected = 0;
1870 for (const auto& [type_key, labels] :
1872 for (const auto& [id_str, label] : labels) {
1873 resource_labels[type_key][id_str] = label;
1874 ++injected;
1875 }
1876 }
1877 LOG_DEBUG("Project", "Loaded project registry: %zu resource labels injected",
1878 injected);
1879}
1880
1882 if (metadata.project_id.empty()) {
1884 }
1885
1886 // Initialize default feature flags
1902 // REMOVED: kLogInstructions (deprecated)
1903
1904 // Initialize default workspace settings
1907 workspace_settings.ui_theme = "default";
1909 workspace_settings.autosave_interval_secs = 300.0f; // 5 minutes
1916
1917 // Initialize default dungeon overlay settings (minecart tracks)
1919 for (uint16_t tile = 0xB0; tile <= 0xBE; ++tile) {
1920 dungeon_overlay.track_tiles.push_back(tile);
1921 }
1922 dungeon_overlay.track_stop_tiles = {0xB7, 0xB8, 0xB9, 0xBA};
1923 dungeon_overlay.track_switch_tiles = {0xD0, 0xD1, 0xD2, 0xD3};
1926
1930
1931 // Initialize default build configurations
1932 build_configurations = {"Debug", "Release", "Distribution"};
1933 build_target.clear();
1934 asm_entry_point = "asm/main.asm";
1935 asm_sources = {"asm"};
1936 last_build_hash.clear();
1937 build_number = 0;
1938
1939 track_changes = true;
1940
1944
1945 if (metadata.created_by.empty()) {
1946 metadata.created_by = "YAZE";
1947 }
1948}
1949
1951 auto now = std::chrono::system_clock::now().time_since_epoch();
1952 auto timestamp =
1953 std::chrono::duration_cast<std::chrono::milliseconds>(now).count();
1954 return absl::StrFormat("yaze_project_%lld", timestamp);
1955}
1956
1957// ProjectManager Implementation
1958std::vector<ProjectManager::ProjectTemplate>
1960 std::vector<ProjectTemplate> templates;
1961
1962 // ==========================================================================
1963 // ZSCustomOverworld Templates (Recommended)
1964 // ==========================================================================
1965
1966 // Vanilla ROM Hack - no ZSO
1967 {
1969 t.name = "Vanilla ROM Hack";
1970 t.description =
1971 "Standard ROM editing without custom ASM. Limited to vanilla features.";
1975 false;
1980 templates.push_back(t);
1981 }
1982
1983 // ZSCustomOverworld v2 - Basic expansion
1984 {
1986 t.name = "ZSCustomOverworld v2";
1987 t.description =
1988 "Basic overworld expansion: custom BG colors, main palettes, parent "
1989 "system.";
1990 t.icon = ICON_MD_MAP;
1993 true;
2000 t.template_project.metadata.tags = {"zso_v2", "overworld", "expansion"};
2001 templates.push_back(t);
2002 }
2003
2004 // ZSCustomOverworld v3 - Full features (Recommended)
2005 {
2007 t.name = "ZSCustomOverworld v3 (Recommended)";
2008 t.description =
2009 "Full overworld expansion: wide/tall areas, animated GFX, overlays, "
2010 "all features.";
2014 true;
2024 t.template_project.metadata.tags = {"zso_v3", "overworld", "full",
2025 "recommended"};
2026 templates.push_back(t);
2027 }
2028
2029 // Randomizer Compatible
2030 {
2032 t.name = "Randomizer Compatible";
2033 t.description =
2034 "Compatible with ALttP Randomizer. Minimal custom features to avoid "
2035 "conflicts.";
2039 false;
2042 t.template_project.metadata.tags = {"randomizer", "compatible", "minimal"};
2043 templates.push_back(t);
2044 }
2045
2046 // ==========================================================================
2047 // Editor-Focused Templates
2048 // ==========================================================================
2049
2050 // Dungeon Designer
2051 {
2053 t.name = "Dungeon Designer";
2054 t.description = "Focused on dungeon creation and modification.";
2055 t.icon = ICON_MD_DOMAIN;
2059 "dungeon_default";
2060 t.template_project.metadata.tags = {"dungeons", "rooms", "design"};
2061 templates.push_back(t);
2062 }
2063
2064 // Graphics Pack
2065 {
2067 t.name = "Graphics Pack";
2068 t.description =
2069 "Project focused on graphics, sprites, and visual modifications.";
2076 "graphics_default";
2077 t.template_project.metadata.tags = {"graphics", "sprites", "palettes"};
2078 templates.push_back(t);
2079 }
2080
2081 // Complete Overhaul
2082 {
2084 t.name = "Complete Overhaul";
2085 t.description = "Full-scale ROM hack with all features enabled.";
2086 t.icon = ICON_MD_BUILD;
2089 true;
2099 t.template_project.metadata.tags = {"complete", "overhaul", "full-mod"};
2100 templates.push_back(t);
2101 }
2102
2103 return templates;
2104}
2105
2106absl::StatusOr<YazeProject> ProjectManager::CreateFromTemplate(
2107 const std::string& template_name, const std::string& project_name,
2108 const std::string& base_path) {
2109 YazeProject project;
2110 auto status = project.Create(project_name, base_path);
2111 if (!status.ok()) {
2112 return status;
2113 }
2114
2115 // Customize based on template
2116 if (template_name == "Full Overworld Mod") {
2119 project.metadata.description = "Overworld modification project";
2120 project.metadata.tags = {"overworld", "maps", "graphics"};
2121 } else if (template_name == "Dungeon Designer") {
2122 project.feature_flags.kSaveDungeonMaps = true;
2123 project.workspace_settings.show_grid = true;
2124 project.metadata.description = "Dungeon design and modification project";
2125 project.metadata.tags = {"dungeons", "rooms", "design"};
2126 } else if (template_name == "Graphics Pack") {
2127 project.feature_flags.kSaveGraphicsSheet = true;
2128 project.workspace_settings.show_grid = true;
2129 project.metadata.description = "Graphics and sprite modification project";
2130 project.metadata.tags = {"graphics", "sprites", "palettes"};
2131 } else if (template_name == "Complete Overhaul") {
2134 project.feature_flags.kSaveDungeonMaps = true;
2135 project.feature_flags.kSaveGraphicsSheet = true;
2136 project.metadata.description = "Complete ROM overhaul project";
2137 project.metadata.tags = {"complete", "overhaul", "full-mod"};
2138 }
2139
2140 status = project.Save();
2141 if (!status.ok()) {
2142 return status;
2143 }
2144
2145 return project;
2146}
2147
2149 const std::string& directory) {
2150#ifdef __EMSCRIPTEN__
2151 (void)directory;
2152 return {};
2153#else
2154 std::vector<std::string> projects;
2155
2156 try {
2157 for (const auto& entry : std::filesystem::directory_iterator(directory)) {
2158 if (entry.is_regular_file()) {
2159 std::string filename = entry.path().filename().string();
2160 if (filename.ends_with(".yaze") || filename.ends_with(".zsproj")) {
2161 projects.push_back(entry.path().string());
2162 }
2163 } else if (entry.is_directory()) {
2164 std::string filename = entry.path().filename().string();
2165 if (filename.ends_with(".yazeproj")) {
2166 projects.push_back(entry.path().string());
2167 }
2168 }
2169 }
2170 } catch (const std::filesystem::filesystem_error& e) {
2171 // Directory doesn't exist or can't be accessed
2172 }
2173
2174 return projects;
2175#endif // __EMSCRIPTEN__
2176}
2177
2178absl::Status ProjectManager::BackupProject(const YazeProject& project) {
2179#ifdef __EMSCRIPTEN__
2180 (void)project;
2181 return absl::UnimplementedError(
2182 "Project backups are not supported in the web build");
2183#else
2184 if (project.filepath.empty()) {
2185 return absl::InvalidArgumentError("Project has no file path");
2186 }
2187
2188 std::filesystem::path project_path(project.filepath);
2189 std::filesystem::path backup_dir = project_path.parent_path() / "backups";
2190 std::filesystem::create_directories(backup_dir);
2191
2192 auto now = std::chrono::system_clock::now();
2193 auto time_t = std::chrono::system_clock::to_time_t(now);
2194 std::stringstream ss;
2195 ss << std::put_time(std::localtime(&time_t), "%Y%m%d_%H%M%S");
2196
2197 std::string backup_filename = project.name + "_backup_" + ss.str() + ".yaze";
2198 std::filesystem::path backup_path = backup_dir / backup_filename;
2199
2200 try {
2201 std::filesystem::copy_file(project.filepath, backup_path);
2202 } catch (const std::filesystem::filesystem_error& e) {
2203 return absl::InternalError(
2204 absl::StrFormat("Failed to backup project: %s", e.what()));
2205 }
2206
2207 return absl::OkStatus();
2208#endif
2209}
2210
2212 const YazeProject& project) {
2213 return project.Validate();
2214}
2215
2217 const YazeProject& project) {
2218 std::vector<std::string> recommendations;
2219
2220 if (project.rom_filename.empty()) {
2221 recommendations.push_back("Add a ROM file to begin editing");
2222 }
2223
2224 if (project.code_folder.empty()) {
2225 recommendations.push_back("Set up a code folder for assembly patches");
2226 }
2227
2228 if (project.labels_filename.empty()) {
2229 recommendations.push_back("Create a labels file for better organization");
2230 }
2231
2232 if (project.metadata.description.empty()) {
2233 recommendations.push_back("Add a project description for documentation");
2234 }
2235
2236 if (project.git_repository.empty() && project.track_changes) {
2237 recommendations.push_back(
2238 "Consider setting up version control for your project");
2239 }
2240
2241 auto missing_files = project.GetMissingFiles();
2242 if (!missing_files.empty()) {
2243 recommendations.push_back(
2244 "Some project files are missing - use Project > Repair to fix");
2245 }
2246
2247 return recommendations;
2248}
2249
2250// Compatibility implementations for ResourceLabelManager and related classes
2251bool ResourceLabelManager::LoadLabels(const std::string& filename) {
2252 filename_ = filename;
2253 std::ifstream file(filename);
2254 if (!file.is_open()) {
2255 labels_loaded_ = false;
2256 return false;
2257 }
2258
2259 labels_.clear();
2260 std::string line;
2261 std::string current_type = "";
2262
2263 while (std::getline(file, line)) {
2264 if (line.empty() || line[0] == '#')
2265 continue;
2266
2267 // Check for type headers [type_name]
2268 if (line[0] == '[' && line.back() == ']') {
2269 current_type = line.substr(1, line.length() - 2);
2270 continue;
2271 }
2272
2273 // Parse key=value pairs
2274 size_t eq_pos = line.find('=');
2275 if (eq_pos != std::string::npos && !current_type.empty()) {
2276 std::string key = line.substr(0, eq_pos);
2277 std::string value = line.substr(eq_pos + 1);
2278 labels_[current_type][key] = value;
2279 }
2280 }
2281
2282 file.close();
2283 labels_loaded_ = true;
2284 return true;
2285}
2286
2288 if (filename_.empty())
2289 return false;
2290
2291 std::ofstream file(filename_);
2292 if (!file.is_open())
2293 return false;
2294
2295 file << "# yaze Resource Labels\n";
2296 file << "# Format: [type] followed by key=value pairs\n\n";
2297
2298 for (const auto& [type, type_labels] : labels_) {
2299 if (!type_labels.empty()) {
2300 file << "[" << type << "]\n";
2301 for (const auto& [key, value] : type_labels) {
2302 file << key << "=" << value << "\n";
2303 }
2304 file << "\n";
2305 }
2306 }
2307
2308 file.close();
2309 return true;
2310}
2311
2313 if (!p_open || !*p_open)
2314 return;
2315
2316 // Basic implementation - can be enhanced later
2317 if (ImGui::Begin("Resource Labels", p_open)) {
2318 ImGui::Text("Resource Labels Manager");
2319 ImGui::Text("Labels loaded: %s", labels_loaded_ ? "Yes" : "No");
2320 ImGui::Text("Total types: %zu", labels_.size());
2321
2322 for (const auto& [type, type_labels] : labels_) {
2323 if (ImGui::TreeNode(type.c_str())) {
2324 ImGui::Text("Labels: %zu", type_labels.size());
2325 for (const auto& [key, value] : type_labels) {
2326 ImGui::Text("%s = %s", key.c_str(), value.c_str());
2327 }
2328 ImGui::TreePop();
2329 }
2330 }
2331 }
2332 ImGui::End();
2333}
2334
2335void ResourceLabelManager::EditLabel(const std::string& type,
2336 const std::string& key,
2337 const std::string& newValue) {
2338 labels_[type][key] = newValue;
2339}
2340
2342 bool selected, const std::string& type, const std::string& key,
2343 const std::string& defaultValue) {
2344 // Basic implementation
2345 if (ImGui::Selectable(
2346 absl::StrFormat("%s: %s", key.c_str(), GetLabel(type, key).c_str())
2347 .c_str(),
2348 selected)) {
2349 // Handle selection
2350 }
2351}
2352
2353std::string ResourceLabelManager::GetLabel(const std::string& type,
2354 const std::string& key) {
2355 auto type_it = labels_.find(type);
2356 if (type_it == labels_.end())
2357 return "";
2358
2359 auto label_it = type_it->second.find(key);
2360 if (label_it == type_it->second.end())
2361 return "";
2362
2363 return label_it->second;
2364}
2365
2367 const std::string& type, const std::string& key,
2368 const std::string& defaultValue) {
2369 auto existing = GetLabel(type, key);
2370 if (!existing.empty())
2371 return existing;
2372
2373 labels_[type][key] = defaultValue;
2374 return defaultValue;
2375}
2376
2377// ============================================================================
2378// Embedded Labels Support
2379// ============================================================================
2380
2382 const std::unordered_map<
2383 std::string, std::unordered_map<std::string, std::string>>& labels) {
2384 try {
2385 // Load all default Zelda3 resource names into resource_labels
2386 // We merge them with existing labels, prioritizing existing overrides?
2387 // Or just overwrite? The previous code was:
2388 // resource_labels = zelda3::Zelda3Labels::ToResourceLabels();
2389 // which implies overwriting. But we want to keep overrides if possible.
2390 // However, this is usually called on load.
2391
2392 // Let's overwrite for now to match previous behavior, assuming overrides
2393 // are loaded afterwards or this is initial setup.
2394 // Actually, if we load project then init embedded labels, we might lose overrides.
2395 // But typically overrides are loaded from the project file *into* resource_labels.
2396 // If we call this, we might clobber them.
2397 // The previous implementation clobbered resource_labels.
2398
2399 // However, if we want to support overrides + embedded, we should merge.
2400 // But `resource_labels` was treated as "overrides" in the old code?
2401 // No, `resource_labels` was the container for loaded labels.
2402
2403 // If I look at `LoadFromYazeFormat`:
2404 // It parses `[labels_type]` into `resource_labels`.
2405
2406 // If `use_embedded_labels` is true, `InitializeEmbeddedLabels` is called?
2407 // I need to check when `InitializeEmbeddedLabels` is called.
2408
2409 resource_labels = labels;
2410 use_embedded_labels = true;
2411
2412 LOG_DEBUG("Project", "Initialized embedded labels:");
2413 LOG_DEBUG("Project", " - %d room names", resource_labels["room"].size());
2414 LOG_DEBUG("Project", " - %d entrance names",
2415 resource_labels["entrance"].size());
2416 LOG_DEBUG("Project", " - %d sprite names",
2417 resource_labels["sprite"].size());
2418 LOG_DEBUG("Project", " - %d overlord names",
2419 resource_labels["overlord"].size());
2420 LOG_DEBUG("Project", " - %d item names", resource_labels["item"].size());
2421 LOG_DEBUG("Project", " - %d music names",
2422 resource_labels["music"].size());
2423 LOG_DEBUG("Project", " - %d graphics names",
2424 resource_labels["graphics"].size());
2425 LOG_DEBUG("Project", " - %d room effect names",
2426 resource_labels["room_effect"].size());
2427 LOG_DEBUG("Project", " - %d room tag names",
2428 resource_labels["room_tag"].size());
2429 LOG_DEBUG("Project", " - %d tile type names",
2430 resource_labels["tile_type"].size());
2431
2432 return absl::OkStatus();
2433 } catch (const std::exception& e) {
2434 return absl::InternalError(
2435 absl::StrCat("Failed to initialize embedded labels: ", e.what()));
2436 }
2437}
2438
2439std::string YazeProject::GetLabel(const std::string& resource_type, int id,
2440 const std::string& default_value) const {
2441 // First check if we have a custom label override
2442 auto type_it = resource_labels.find(resource_type);
2443 if (type_it != resource_labels.end()) {
2444 auto label_it = type_it->second.find(std::to_string(id));
2445 if (label_it != type_it->second.end()) {
2446 return label_it->second;
2447 }
2448 }
2449
2450 return default_value.empty() ? resource_type + "_" + std::to_string(id)
2451 : default_value;
2452}
2453
2454absl::Status YazeProject::ImportLabelsFromZScream(const std::string& filepath) {
2455#ifdef __EMSCRIPTEN__
2456 (void)filepath;
2457 return absl::UnimplementedError(
2458 "File-based label import is not supported in the web build");
2459#else
2460 std::ifstream file(filepath);
2461 if (!file.is_open()) {
2462 return absl::InvalidArgumentError(
2463 absl::StrFormat("Cannot open labels file: %s", filepath));
2464 }
2465
2466 std::stringstream buffer;
2467 buffer << file.rdbuf();
2468 file.close();
2469
2470 return ImportLabelsFromZScreamContent(buffer.str());
2471#endif
2472}
2473
2475 const std::string& content) {
2476 // Initialize the global provider with our labels
2477 auto& provider = zelda3::GetResourceLabels();
2478 provider.SetProjectLabels(&resource_labels);
2479 provider.SetPreferHMagicNames(workspace_settings.prefer_hmagic_names);
2480
2481 // Use the provider to parse ZScream format
2482 auto status = provider.ImportFromZScreamFormat(content);
2483 if (!status.ok()) {
2484 return status;
2485 }
2486
2487 LOG_DEBUG("Project", "Imported ZScream labels:");
2488 LOG_DEBUG("Project", " - %d sprite labels",
2489 resource_labels["sprite"].size());
2490 LOG_DEBUG("Project", " - %d room labels", resource_labels["room"].size());
2491 LOG_DEBUG("Project", " - %d item labels", resource_labels["item"].size());
2492 LOG_DEBUG("Project", " - %d room tag labels",
2493 resource_labels["room_tag"].size());
2494
2495 return absl::OkStatus();
2496}
2497
2499 auto& provider = zelda3::GetResourceLabels();
2500 provider.SetProjectLabels(&resource_labels);
2501 provider.SetPreferHMagicNames(workspace_settings.prefer_hmagic_names);
2502 provider.SetHackManifest(hack_manifest.loaded() ? &hack_manifest : nullptr);
2503
2504 LOG_DEBUG("Project", "Initialized ResourceLabelProvider with project labels");
2505 LOG_DEBUG("Project", " - prefer_hmagic_names: %s",
2506 workspace_settings.prefer_hmagic_names ? "true" : "false");
2507 LOG_DEBUG("Project", " - hack_manifest: %s",
2508 hack_manifest.loaded() ? "loaded" : "not loaded");
2509}
2510
2511// ============================================================================
2512// JSON Format Support (Optional)
2513// ============================================================================
2514
2515#ifdef YAZE_ENABLE_JSON_PROJECT_FORMAT
2516
2517absl::Status YazeProject::LoadFromJsonFormat(const std::string& project_path) {
2518#ifdef __EMSCRIPTEN__
2519 return absl::UnimplementedError(
2520 "JSON project format loading is not supported in the web build");
2521#endif
2522 std::ifstream file(project_path);
2523 if (!file.is_open()) {
2524 return absl::InvalidArgumentError(
2525 absl::StrFormat("Cannot open JSON project file: %s", project_path));
2526 }
2527
2528 try {
2529 json j;
2530 file >> j;
2531
2532 // Parse project metadata
2533 if (j.contains("yaze_project")) {
2534 auto& proj = j["yaze_project"];
2535
2536 if (proj.contains("name"))
2537 name = proj["name"].get<std::string>();
2538 if (proj.contains("description"))
2539 metadata.description = proj["description"].get<std::string>();
2540 if (proj.contains("author"))
2541 metadata.author = proj["author"].get<std::string>();
2542 if (proj.contains("version"))
2543 metadata.version = proj["version"].get<std::string>();
2544 if (proj.contains("created"))
2545 metadata.created_date = proj["created"].get<std::string>();
2546 if (proj.contains("modified"))
2547 metadata.last_modified = proj["modified"].get<std::string>();
2548 if (proj.contains("created_by"))
2549 metadata.created_by = proj["created_by"].get<std::string>();
2550
2551 // Files
2552 if (proj.contains("rom_filename"))
2553 rom_filename = proj["rom_filename"].get<std::string>();
2554 if (proj.contains("rom_backup_folder"))
2555 rom_backup_folder = proj["rom_backup_folder"].get<std::string>();
2556 if (proj.contains("code_folder"))
2557 code_folder = proj["code_folder"].get<std::string>();
2558 if (proj.contains("assets_folder"))
2559 assets_folder = proj["assets_folder"].get<std::string>();
2560 if (proj.contains("patches_folder"))
2561 patches_folder = proj["patches_folder"].get<std::string>();
2562 if (proj.contains("labels_filename"))
2563 labels_filename = proj["labels_filename"].get<std::string>();
2564 if (proj.contains("symbols_filename"))
2565 symbols_filename = proj["symbols_filename"].get<std::string>();
2566 if (proj.contains("hack_manifest_file"))
2567 hack_manifest_file = proj["hack_manifest_file"].get<std::string>();
2568
2569 if (proj.contains("rom") && proj["rom"].is_object()) {
2570 auto& rom = proj["rom"];
2571 if (rom.contains("role"))
2572 rom_metadata.role = ParseRomRole(rom["role"].get<std::string>());
2573 if (rom.contains("expected_hash"))
2574 rom_metadata.expected_hash = rom["expected_hash"].get<std::string>();
2575 if (rom.contains("write_policy"))
2577 ParseRomWritePolicy(rom["write_policy"].get<std::string>());
2578 }
2579
2580 // Embedded labels flag
2581 if (proj.contains("use_embedded_labels")) {
2582 use_embedded_labels = proj["use_embedded_labels"].get<bool>();
2583 }
2584
2585 // Feature flags
2586 if (proj.contains("feature_flags")) {
2587 auto& flags = proj["feature_flags"];
2588 // REMOVED: kLogInstructions (deprecated - DisassemblyViewer always
2589 // active)
2590 if (flags.contains("kSaveDungeonMaps"))
2592 flags["kSaveDungeonMaps"].get<bool>();
2593 if (flags.contains("kSaveOverworldMaps"))
2595 flags["kSaveOverworldMaps"].get<bool>();
2596 if (flags.contains("kSaveOverworldEntrances"))
2598 flags["kSaveOverworldEntrances"].get<bool>();
2599 if (flags.contains("kSaveOverworldExits"))
2601 flags["kSaveOverworldExits"].get<bool>();
2602 if (flags.contains("kSaveOverworldItems"))
2604 flags["kSaveOverworldItems"].get<bool>();
2605 if (flags.contains("kSaveOverworldProperties"))
2607 flags["kSaveOverworldProperties"].get<bool>();
2608 if (flags.contains("kSaveDungeonObjects"))
2610 flags["kSaveDungeonObjects"].get<bool>();
2611 if (flags.contains("kSaveDungeonSprites"))
2613 flags["kSaveDungeonSprites"].get<bool>();
2614 if (flags.contains("kSaveDungeonRoomHeaders"))
2616 flags["kSaveDungeonRoomHeaders"].get<bool>();
2617 if (flags.contains("kSaveDungeonTorches"))
2619 flags["kSaveDungeonTorches"].get<bool>();
2620 if (flags.contains("kSaveDungeonPits"))
2622 flags["kSaveDungeonPits"].get<bool>();
2623 if (flags.contains("kSaveDungeonBlocks"))
2625 flags["kSaveDungeonBlocks"].get<bool>();
2626 if (flags.contains("kSaveDungeonCollision"))
2628 flags["kSaveDungeonCollision"].get<bool>();
2629 if (flags.contains("kSaveDungeonWaterFillZones"))
2631 flags["kSaveDungeonWaterFillZones"].get<bool>();
2632 if (flags.contains("kSaveDungeonChests"))
2634 flags["kSaveDungeonChests"].get<bool>();
2635 if (flags.contains("kSaveDungeonPotItems"))
2637 flags["kSaveDungeonPotItems"].get<bool>();
2638 if (flags.contains("kSaveDungeonEntrances"))
2640 flags["kSaveDungeonEntrances"].get<bool>();
2641 if (flags.contains("kSaveDungeonPalettes"))
2643 flags["kSaveDungeonPalettes"].get<bool>();
2644 if (flags.contains("kSaveGraphicsSheet"))
2646 flags["kSaveGraphicsSheet"].get<bool>();
2647 if (flags.contains("kSaveAllPalettes"))
2649 flags["kSaveAllPalettes"].get<bool>();
2650 if (flags.contains("kSaveGfxGroups"))
2651 feature_flags.kSaveGfxGroups = flags["kSaveGfxGroups"].get<bool>();
2652 if (flags.contains("kSaveMessages"))
2653 feature_flags.kSaveMessages = flags["kSaveMessages"].get<bool>();
2654 }
2655
2656 // Workspace settings
2657 if (proj.contains("workspace_settings")) {
2658 auto& ws = proj["workspace_settings"];
2659 if (ws.contains("auto_save_enabled"))
2661 ws["auto_save_enabled"].get<bool>();
2662 if (ws.contains("auto_save_interval"))
2664 ws["auto_save_interval"].get<float>();
2665 if (ws.contains("backup_on_save"))
2666 workspace_settings.backup_on_save = ws["backup_on_save"].get<bool>();
2667 if (ws.contains("backup_retention_count"))
2669 ws["backup_retention_count"].get<int>();
2670 if (ws.contains("backup_keep_daily"))
2672 ws["backup_keep_daily"].get<bool>();
2673 if (ws.contains("backup_keep_daily_days"))
2675 ws["backup_keep_daily_days"].get<int>();
2676 }
2677
2678 if (proj.contains("rom_addresses") && proj["rom_addresses"].is_object()) {
2680 for (auto it = proj["rom_addresses"].begin();
2681 it != proj["rom_addresses"].end(); ++it) {
2682 if (it.value().is_number_unsigned()) {
2684 it.value().get<uint32_t>();
2685 } else if (it.value().is_string()) {
2686 auto parsed = ParseHexUint32(it.value().get<std::string>());
2687 if (parsed.has_value()) {
2688 rom_address_overrides.addresses[it.key()] = *parsed;
2689 }
2690 }
2691 }
2692 }
2693
2694 if (proj.contains("custom_objects") &&
2695 proj["custom_objects"].is_object()) {
2696 custom_object_files.clear();
2697 for (auto it = proj["custom_objects"].begin();
2698 it != proj["custom_objects"].end(); ++it) {
2699 if (!it.value().is_array())
2700 continue;
2701 auto parsed = ParseHexUint32(it.key());
2702 if (!parsed.has_value()) {
2703 continue;
2704 }
2705 std::vector<std::string> files;
2706 for (const auto& entry : it.value()) {
2707 if (entry.is_string()) {
2708 files.push_back(entry.get<std::string>());
2709 }
2710 }
2711 if (!files.empty()) {
2712 custom_object_files[static_cast<int>(*parsed)] = std::move(files);
2713 }
2714 }
2715 }
2716
2717 if (proj.contains("agent_settings") &&
2718 proj["agent_settings"].is_object()) {
2719 auto& agent = proj["agent_settings"];
2721 agent.value("ai_provider", agent_settings.ai_provider);
2723 agent.value("ai_model", agent_settings.ai_model);
2725 agent.value("ollama_host", agent_settings.ollama_host);
2727 agent.value("gemini_api_key", agent_settings.gemini_api_key);
2729 agent.value("use_custom_prompt", agent_settings.use_custom_prompt);
2731 "custom_system_prompt", agent_settings.custom_system_prompt);
2733 agent.value("show_reasoning", agent_settings.show_reasoning);
2734 agent_settings.verbose = agent.value("verbose", agent_settings.verbose);
2735 agent_settings.max_tool_iterations = agent.value(
2736 "max_tool_iterations", agent_settings.max_tool_iterations);
2737 agent_settings.max_retry_attempts = agent.value(
2738 "max_retry_attempts", agent_settings.max_retry_attempts);
2740 agent.value("temperature", agent_settings.temperature);
2741 agent_settings.top_p = agent.value("top_p", agent_settings.top_p);
2743 agent.value("max_output_tokens", agent_settings.max_output_tokens);
2745 agent.value("stream_responses", agent_settings.stream_responses);
2746 if (agent.contains("favorite_models") &&
2747 agent["favorite_models"].is_array()) {
2749 for (const auto& model : agent["favorite_models"]) {
2750 if (model.is_string())
2752 model.get<std::string>());
2753 }
2754 }
2755 if (agent.contains("model_chain") && agent["model_chain"].is_array()) {
2757 for (const auto& model : agent["model_chain"]) {
2758 if (model.is_string())
2759 agent_settings.model_chain.push_back(model.get<std::string>());
2760 }
2761 }
2763 agent.value("chain_mode", agent_settings.chain_mode);
2765 "enable_tool_resources", agent_settings.enable_tool_resources);
2766 agent_settings.enable_tool_dungeon = agent.value(
2767 "enable_tool_dungeon", agent_settings.enable_tool_dungeon);
2769 "enable_tool_overworld", agent_settings.enable_tool_overworld);
2771 "enable_tool_messages", agent_settings.enable_tool_messages);
2773 "enable_tool_dialogue", agent_settings.enable_tool_dialogue);
2775 agent.value("enable_tool_gui", agent_settings.enable_tool_gui);
2777 agent.value("enable_tool_music", agent_settings.enable_tool_music);
2778 agent_settings.enable_tool_sprite = agent.value(
2779 "enable_tool_sprite", agent_settings.enable_tool_sprite);
2781 "enable_tool_emulator", agent_settings.enable_tool_emulator);
2783 agent.value("enable_tool_memory_inspector",
2786 "builder_blueprint_path", agent_settings.builder_blueprint_path);
2787 }
2788
2789 // Build settings
2790 if (proj.contains("build_script"))
2791 build_script = proj["build_script"].get<std::string>();
2792 if (proj.contains("output_folder"))
2793 output_folder = proj["output_folder"].get<std::string>();
2794 if (proj.contains("git_repository"))
2795 git_repository = proj["git_repository"].get<std::string>();
2796 if (proj.contains("track_changes"))
2797 track_changes = proj["track_changes"].get<bool>();
2798 }
2799
2800 return absl::OkStatus();
2801 } catch (const json::exception& e) {
2802 return absl::InvalidArgumentError(
2803 absl::StrFormat("JSON parse error: %s", e.what()));
2804 }
2805}
2806
2807absl::Status YazeProject::SaveToJsonFormat() {
2808#ifdef __EMSCRIPTEN__
2809 return absl::UnimplementedError(
2810 "JSON project format saving is not supported in the web build");
2811#endif
2812 json j;
2813 auto& proj = j["yaze_project"];
2814
2815 // Metadata
2816 proj["version"] = metadata.version;
2817 proj["name"] = name;
2818 proj["author"] = metadata.author;
2819 proj["created_by"] = metadata.created_by;
2820 proj["description"] = metadata.description;
2821 proj["created"] = metadata.created_date;
2822 proj["modified"] = metadata.last_modified;
2823
2824 // Files
2825 proj["rom_filename"] = rom_filename;
2826 proj["rom_backup_folder"] = rom_backup_folder;
2827 proj["code_folder"] = code_folder;
2828 proj["assets_folder"] = assets_folder;
2829 proj["patches_folder"] = patches_folder;
2830 proj["labels_filename"] = labels_filename;
2831 proj["symbols_filename"] = symbols_filename;
2832 proj["hack_manifest_file"] = hack_manifest_file;
2833 proj["output_folder"] = output_folder;
2834
2835 proj["rom"]["role"] = RomRoleToString(rom_metadata.role);
2836 proj["rom"]["expected_hash"] = rom_metadata.expected_hash;
2837 proj["rom"]["write_policy"] =
2839
2840 // Embedded labels
2841 proj["use_embedded_labels"] = use_embedded_labels;
2842
2843 // Feature flags
2844 // REMOVED: kLogInstructions (deprecated)
2845 proj["feature_flags"]["kSaveDungeonMaps"] = feature_flags.kSaveDungeonMaps;
2846 proj["feature_flags"]["kSaveOverworldMaps"] =
2848 proj["feature_flags"]["kSaveOverworldEntrances"] =
2850 proj["feature_flags"]["kSaveOverworldExits"] =
2852 proj["feature_flags"]["kSaveOverworldItems"] =
2854 proj["feature_flags"]["kSaveOverworldProperties"] =
2856 proj["feature_flags"]["kSaveDungeonObjects"] =
2858 proj["feature_flags"]["kSaveDungeonSprites"] =
2860 proj["feature_flags"]["kSaveDungeonRoomHeaders"] =
2862 proj["feature_flags"]["kSaveDungeonTorches"] =
2864 proj["feature_flags"]["kSaveDungeonPits"] = feature_flags.dungeon.kSavePits;
2865 proj["feature_flags"]["kSaveDungeonBlocks"] =
2867 proj["feature_flags"]["kSaveDungeonCollision"] =
2869 proj["feature_flags"]["kSaveDungeonWaterFillZones"] =
2871 proj["feature_flags"]["kSaveDungeonChests"] =
2873 proj["feature_flags"]["kSaveDungeonPotItems"] =
2875 proj["feature_flags"]["kSaveDungeonEntrances"] =
2877 proj["feature_flags"]["kSaveDungeonPalettes"] =
2879 proj["feature_flags"]["kSaveGraphicsSheet"] =
2881 proj["feature_flags"]["kSaveAllPalettes"] = feature_flags.kSaveAllPalettes;
2882 proj["feature_flags"]["kSaveGfxGroups"] = feature_flags.kSaveGfxGroups;
2883 proj["feature_flags"]["kSaveMessages"] = feature_flags.kSaveMessages;
2884
2885 // Workspace settings
2886 proj["workspace_settings"]["auto_save_enabled"] =
2888 proj["workspace_settings"]["auto_save_interval"] =
2890 proj["workspace_settings"]["backup_on_save"] =
2892 proj["workspace_settings"]["backup_retention_count"] =
2894 proj["workspace_settings"]["backup_keep_daily"] =
2896 proj["workspace_settings"]["backup_keep_daily_days"] =
2898
2899 auto& agent = proj["agent_settings"];
2900 agent["ai_provider"] = agent_settings.ai_provider;
2901 agent["ai_model"] = agent_settings.ai_model;
2902 agent["ollama_host"] = agent_settings.ollama_host;
2903 agent["gemini_api_key"] = agent_settings.gemini_api_key;
2904 agent["use_custom_prompt"] = agent_settings.use_custom_prompt;
2905 agent["custom_system_prompt"] = agent_settings.custom_system_prompt;
2906 agent["show_reasoning"] = agent_settings.show_reasoning;
2907 agent["verbose"] = agent_settings.verbose;
2908 agent["max_tool_iterations"] = agent_settings.max_tool_iterations;
2909 agent["max_retry_attempts"] = agent_settings.max_retry_attempts;
2910 agent["temperature"] = agent_settings.temperature;
2911 agent["top_p"] = agent_settings.top_p;
2912 agent["max_output_tokens"] = agent_settings.max_output_tokens;
2913 agent["stream_responses"] = agent_settings.stream_responses;
2914 agent["favorite_models"] = agent_settings.favorite_models;
2915 agent["model_chain"] = agent_settings.model_chain;
2916 agent["chain_mode"] = agent_settings.chain_mode;
2917 agent["enable_tool_resources"] = agent_settings.enable_tool_resources;
2918 agent["enable_tool_dungeon"] = agent_settings.enable_tool_dungeon;
2919 agent["enable_tool_overworld"] = agent_settings.enable_tool_overworld;
2920 agent["enable_tool_messages"] = agent_settings.enable_tool_messages;
2921 agent["enable_tool_dialogue"] = agent_settings.enable_tool_dialogue;
2922 agent["enable_tool_gui"] = agent_settings.enable_tool_gui;
2923 agent["enable_tool_music"] = agent_settings.enable_tool_music;
2924 agent["enable_tool_sprite"] = agent_settings.enable_tool_sprite;
2925 agent["enable_tool_emulator"] = agent_settings.enable_tool_emulator;
2926 agent["enable_tool_memory_inspector"] =
2928 agent["builder_blueprint_path"] = agent_settings.builder_blueprint_path;
2929
2930 if (!rom_address_overrides.addresses.empty()) {
2931 auto& addrs = proj["rom_addresses"];
2932 for (const auto& [key, value] : rom_address_overrides.addresses) {
2933 addrs[key] = value;
2934 }
2935 }
2936
2937 if (!custom_object_files.empty()) {
2938 auto& objs = proj["custom_objects"];
2939 for (const auto& [object_id, files] : custom_object_files) {
2940 objs[absl::StrFormat("0x%X", object_id)] = files;
2941 }
2942 }
2943
2944 // Build settings
2945 proj["build_script"] = build_script;
2946 proj["git_repository"] = git_repository;
2947 proj["track_changes"] = track_changes;
2948
2949 // Write to file
2950 std::ofstream file(filepath);
2951 if (!file.is_open()) {
2952 return absl::InvalidArgumentError(
2953 absl::StrFormat("Cannot write JSON project file: %s", filepath));
2954 }
2955
2956 file << j.dump(2); // Pretty print with 2-space indent
2957 return absl::OkStatus();
2958}
2959
2960#endif // YAZE_ENABLE_JSON_PROJECT_FORMAT
2961
2962// RecentFilesManager implementation
2964 auto config_dir = util::PlatformPaths::GetConfigDirectory();
2965 if (!config_dir.ok()) {
2966 return ""; // Or handle error appropriately
2967 }
2968 return (*config_dir / kRecentFilesFilename).string();
2969}
2970
2972#ifdef __EMSCRIPTEN__
2973 auto status = platform::WasmStorage::SaveProject(
2974 kRecentFilesFilename, absl::StrJoin(recent_files_, "\n"));
2975 if (!status.ok()) {
2976 LOG_WARN("RecentFilesManager", "Could not persist recent files: %s",
2977 status.ToString().c_str());
2978 }
2979 return;
2980#endif
2981 // Ensure config directory exists
2982 auto config_dir_status = util::PlatformPaths::GetConfigDirectory();
2983 if (!config_dir_status.ok()) {
2984 LOG_ERROR("Project", "Failed to get or create config directory: %s",
2985 config_dir_status.status().ToString().c_str());
2986 return;
2987 }
2988
2989 std::string filepath = GetFilePath();
2990 std::ofstream file(filepath);
2991 if (!file.is_open()) {
2992 LOG_WARN("RecentFilesManager", "Could not save recent files to %s",
2993 filepath.c_str());
2994 return;
2995 }
2996
2997 for (const auto& file_path : recent_files_) {
2998 file << file_path << std::endl;
2999 }
3000}
3001
3003#ifdef __EMSCRIPTEN__
3004 auto storage_or = platform::WasmStorage::LoadProject(kRecentFilesFilename);
3005 if (!storage_or.ok()) {
3006 return;
3007 }
3008 recent_files_.clear();
3009 std::istringstream stream(storage_or.value());
3010 std::string line;
3011 while (std::getline(stream, line)) {
3012 if (!line.empty()) {
3013 recent_files_.push_back(line);
3014 }
3015 }
3017 return;
3018#else
3019 std::string filepath = GetFilePath();
3020 std::ifstream file(filepath);
3021 if (!file.is_open()) {
3022 // File doesn't exist yet, which is fine
3023 return;
3024 }
3025
3026 recent_files_.clear();
3027 std::string line;
3028 while (std::getline(file, line)) {
3029 if (!line.empty()) {
3030 recent_files_.push_back(line);
3031 }
3032 }
3034#endif
3035}
3036
3037} // namespace project
3038} // 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:2148
static absl::Status ValidateProjectStructure(const YazeProject &project)
Definition project.cc:2211
static absl::StatusOr< YazeProject > CreateFromTemplate(const std::string &template_name, const std::string &project_name, const std::string &base_path)
Definition project.cc:2106
static std::vector< std::string > GetRecommendedFixesForProject(const YazeProject &project)
Definition project.cc:2216
static std::vector< ProjectTemplate > GetProjectTemplates()
Definition project.cc:1959
static absl::Status BackupProject(const YazeProject &project)
Definition project.cc:2178
std::string GetFilePath() const
Definition project.cc:2963
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:2366
std::string GetLabel(const std::string &type, const std::string &key)
Definition project.cc:2353
void EditLabel(const std::string &type, const std::string &key, const std::string &newValue)
Definition project.cc:2335
bool LoadLabels(const std::string &filename)
Definition project.cc:2251
void SelectableLabelWithNameEdit(bool selected, const std::string &type, const std::string &key, const std::string &defaultValue)
Definition project.cc:2341
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:1350
std::string custom_objects_folder
Definition project.h:192
absl::Status RepairProject()
Definition project.cc:1425
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:1317
ProjectMetadata metadata
Definition project.h:174
absl::Status SaveToYazeFormat(bool replace_existing=true)
Definition project.cc:1261
absl::Status ImportZScreamProject(const std::string &zscream_project_path)
Definition project.cc:1294
absl::Status SaveAllSettings()
Definition project.cc:1345
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:2474
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:2498
absl::Status ParseFromString(const std::string &content)
Definition project.cc:951
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:1240
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:1950
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:1339
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:1459
std::vector< std::string > GetMissingFiles() const
Definition project.cc:1400
WorkspaceSettings workspace_settings
Definition project.h:201
std::string GetZ3dkArtifactPath(absl::string_view artifact_name) const
Definition project.cc:1565
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:1466
absl::Status InitializeEmbeddedLabels(const std::unordered_map< std::string, std::unordered_map< std::string, std::string > > &labels)
Definition project.cc:2381
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:1543
std::string GetAbsolutePath(const std::string &relative_path) const
Definition project.cc:1486
std::string GetLabel(const std::string &resource_type, int id, const std::string &default_value="") const
Definition project.cc:2439
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:2454
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:1355
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