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