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"
4
5#include <algorithm>
6#include <atomic>
7#include <cctype>
8#include <cerrno>
9#include <chrono>
10#include <cstdint>
11#include <filesystem>
12#include <fstream>
13#include <iomanip>
14#include <sstream>
15
16#include "absl/strings/match.h"
17#include "absl/strings/str_format.h"
18#include "absl/strings/str_join.h"
19#include "absl/strings/str_split.h"
20#include "absl/strings/strip.h"
21#include "app/gui/core/icons.h"
22#include "imgui/imgui.h"
23#include "util/file_util.h"
24#include "util/json.h"
25#include "util/log.h"
26#include "util/macro.h"
27#include "util/platform_paths.h"
28#include "yaze_config.h"
30
31#if defined(YAZE_WITH_Z3DK) && __has_include("z3dk_core/config.h")
32#include "z3dk_core/config.h"
33#endif
34
35#ifdef __EMSCRIPTEN__
37#elif defined(_WIN32)
38#include <windows.h>
39#else
40#include <unistd.h>
41#endif
42
43// #ifdef YAZE_ENABLE_JSON_PROJECT_FORMAT
44// #include "nlohmann/json.hpp"
45// using json = nlohmann::json;
46// #endif
47
48namespace yaze {
49namespace project {
50
51namespace {
52std::string ToLowerCopy(std::string value) {
53 std::transform(
54 value.begin(), value.end(), value.begin(),
55 [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
56 return value;
57}
58
59// Helper functions for parsing key-value pairs
60std::pair<std::string, std::string> ParseKeyValue(const std::string& line) {
61 size_t eq_pos = line.find('=');
62 if (eq_pos == std::string::npos)
63 return {"", ""};
64
65 std::string key = line.substr(0, eq_pos);
66 std::string value = line.substr(eq_pos + 1);
67
68 // Trim whitespace
69 key.erase(0, key.find_first_not_of(" \t"));
70 key.erase(key.find_last_not_of(" \t") + 1);
71 value.erase(0, value.find_first_not_of(" \t"));
72 value.erase(value.find_last_not_of(" \t") + 1);
73
74 return {key, value};
75}
76
77bool ParseBool(const std::string& value) {
78 return value == "true" || value == "1" || value == "yes";
79}
80
81float ParseFloat(const std::string& value) {
82 try {
83 return std::stof(value);
84 } catch (...) {
85 return 0.0f;
86 }
87}
88
89std::vector<std::string> ParseStringList(const std::string& value) {
90 std::vector<std::string> result;
91 if (value.empty())
92 return result;
93
94 std::vector<std::string> parts = absl::StrSplit(value, ',');
95 for (const auto& part : parts) {
96 std::string trimmed = std::string(part);
97 trimmed.erase(0, trimmed.find_first_not_of(" \t"));
98 trimmed.erase(trimmed.find_last_not_of(" \t") + 1);
99 if (!trimmed.empty()) {
100 result.push_back(trimmed);
101 }
102 }
103 return result;
104}
105
106std::vector<std::string> ParsePositionalStringList(const std::string& value) {
107 std::vector<std::string> result;
108 if (value.empty())
109 return result;
110
111 std::vector<std::string> parts = absl::StrSplit(value, ',');
112 result.reserve(parts.size());
113 for (const auto& part : parts) {
114 std::string trimmed = part;
115 trimmed.erase(0, trimmed.find_first_not_of(" \t"));
116 trimmed.erase(trimmed.find_last_not_of(" \t") + 1);
117 result.push_back(std::move(trimmed));
118 }
119 return result;
120}
121
122std::vector<uint16_t> ParseHexUintList(const std::string& value) {
123 std::vector<uint16_t> result;
124 if (value.empty()) {
125 return result;
126 }
127
128 auto parts = ParseStringList(value);
129 result.reserve(parts.size());
130 for (const auto& part : parts) {
131 std::string token = part;
132 if (token.rfind("0x", 0) == 0 || token.rfind("0X", 0) == 0) {
133 token = token.substr(2);
134 try {
135 result.push_back(static_cast<uint16_t>(std::stoul(token, nullptr, 16)));
136 } catch (...) {
137 // Ignore malformed entries
138 }
139 } else {
140 try {
141 result.push_back(static_cast<uint16_t>(std::stoul(token, nullptr, 10)));
142 } catch (...) {
143 // Ignore malformed entries
144 }
145 }
146 }
147 return result;
148}
149
150std::optional<uint32_t> ParseHexUint32(const std::string& value) {
151 if (value.empty()) {
152 return std::nullopt;
153 }
154 std::string token = value;
155 if (token.rfind("0x", 0) == 0 || token.rfind("0X", 0) == 0) {
156 token = token.substr(2);
157 try {
158 return static_cast<uint32_t>(std::stoul(token, nullptr, 16));
159 } catch (...) {
160 return std::nullopt;
161 }
162 }
163 try {
164 return static_cast<uint32_t>(std::stoul(token, nullptr, 10));
165 } catch (...) {
166 return std::nullopt;
167 }
168}
169
170std::string FormatHexUintList(const std::vector<uint16_t>& values) {
171 return absl::StrJoin(values, ",", [](std::string* out, uint16_t value) {
172 out->append(absl::StrFormat("0x%02X", value));
173 });
174}
175
176// Before 2026-09-25 the gfx group editor keyed blockset, roomset and
177// spriteset labels as "0x" + decimal digits (spriteset 12 -> "0x12", which the
178// label resolver reads as hex 18); the paletteset editor did the same until
179// 2026-09-26. Canonical keys are decimal. On load:
180// existing decimal keys win; "0x" + only decimal digits is that old format and
181// is read as decimal; other hex keys ("0x0C") are read as hex.
183 std::unordered_map<std::string,
184 std::unordered_map<std::string, std::string>>& labels) {
185 auto all_of = [](absl::string_view text, auto predicate) {
186 return !text.empty() && std::all_of(text.begin(), text.end(), predicate);
187 };
188 auto is_decimal = [](char c) {
189 return c >= '0' && c <= '9';
190 };
191 auto is_hex = [](char c) {
192 return std::isxdigit(static_cast<unsigned char>(c)) != 0;
193 };
194 for (const char* type : {"blockset", "roomset", "spriteset", "paletteset"}) {
195 auto found = labels.find(type);
196 if (found == labels.end()) {
197 continue;
198 }
199 std::unordered_map<std::string, std::string> migrated;
200 // Pass 0: canonical decimal keys. Pass 1: old "0x"+decimal. Pass 2: hex.
201 for (int pass = 0; pass < 3; ++pass) {
202 for (const auto& [key, value] : found->second) {
203 absl::string_view digits = key;
204 const bool prefixed = absl::ConsumePrefix(&digits, "0x") ||
205 absl::ConsumePrefix(&digits, "0X");
206 std::string canonical;
207 if (pass == 0 && !prefixed && all_of(digits, is_decimal)) {
208 canonical = std::string(digits);
209 } else if (pass == 1 && prefixed && all_of(digits, is_decimal)) {
210 canonical =
211 std::to_string(std::stoul(std::string(digits), nullptr, 10));
212 } else if (pass == 2 && !all_of(digits, is_decimal)) {
213 // Real hex keys become decimal; anything else is kept as written.
214 canonical =
215 prefixed && all_of(digits, is_hex)
216 ? std::to_string(std::stoul(std::string(digits), nullptr, 16))
217 : key;
218 }
219 if (!canonical.empty()) {
220 migrated.emplace(canonical, value);
221 }
222 }
223 }
224 found->second = std::move(migrated);
225 }
226}
227
228// "0x55:0,1,2;0x54:3" -> {0x55: {0, 1, 2}, 0x54: {3}}
229std::map<uint16_t, std::vector<uint16_t>> ParseHexUintListMap(
230 const std::string& value) {
231 std::map<uint16_t, std::vector<uint16_t>> result;
232 for (absl::string_view entry : absl::StrSplit(value, ';')) {
233 const size_t colon = entry.find(':');
234 if (colon == absl::string_view::npos) {
235 continue;
236 }
237 const auto keys = ParseHexUintList(std::string(entry.substr(0, colon)));
238 if (keys.size() != 1) {
239 continue;
240 }
241 result[keys.front()] =
242 ParseHexUintList(std::string(entry.substr(colon + 1)));
243 }
244 return result;
245}
246
248 const std::map<uint16_t, std::vector<uint16_t>>& values) {
249 return absl::StrJoin(values, ";", [](std::string* out, const auto& entry) {
250 out->append(absl::StrFormat("0x%02X:", entry.first));
251 out->append(absl::StrJoin(entry.second, ","));
252 });
253}
254
255std::string FormatHexUint32(uint32_t value) {
256 return absl::StrFormat("0x%06X", value);
257}
258
259std::string SanitizeStorageKey(absl::string_view input) {
260 std::string key(input);
261 for (char& c : key) {
262 if (!std::isalnum(static_cast<unsigned char>(c))) {
263 c = '_';
264 }
265 }
266 if (key.empty()) {
267 key = "project";
268 }
269 return key;
270}
271
272std::pair<std::string, std::string> ParseDefineToken(const std::string& value) {
273 auto [key, parsed_value] = ParseKeyValue(value);
274 if (key.empty()) {
275 return {value, "1"};
276 }
277 return {key, parsed_value.empty() ? "1" : parsed_value};
278}
279
280std::string ResolveOptionalPath(const std::filesystem::path& base_dir,
281 const std::string& value) {
282 if (value.empty()) {
283 return {};
284 }
285 std::filesystem::path path(value);
286 if (path.is_absolute()) {
287 return path.lexically_normal().string();
288 }
289 return (base_dir / path).lexically_normal().string();
290}
291
292std::string BasenameLower(const std::string& path) {
293 return ToLowerCopy(std::filesystem::path(path).filename().string());
294}
295
296#ifndef __EMSCRIPTEN__
298#if defined(_WIN32)
299 return static_cast<uint64_t>(::GetCurrentProcessId());
300#else
301 return static_cast<uint64_t>(::getpid());
302#endif
303}
304
305std::filesystem::path MakeProjectSaveTempPath(
306 const std::filesystem::path& target_path) {
307 static std::atomic<uint64_t> next_temp_id{0};
308 std::filesystem::path temp_path = target_path;
309 temp_path +=
310 ".tmp." + std::to_string(CurrentProcessIdForProjectSave()) + "." +
311 std::to_string(next_temp_id.fetch_add(1, std::memory_order_relaxed));
312 return temp_path;
313}
314
315void RemoveProjectSaveTempFile(const std::filesystem::path& temp_path) {
316 std::error_code remove_error;
317 std::filesystem::remove(temp_path, remove_error);
318}
319
321 const std::filesystem::path& target_path, absl::string_view contents,
322 bool replace_existing) {
323 const std::filesystem::path temp_path = MakeProjectSaveTempPath(target_path);
324 std::ofstream file(temp_path, std::ios::binary | std::ios::trunc);
325 if (!file.is_open()) {
326 return absl::InvalidArgumentError(absl::StrFormat(
327 "Cannot create temporary project file: %s", temp_path.string()));
328 }
329
330 file.write(contents.data(), static_cast<std::streamsize>(contents.size()));
331 file.flush();
332 if (!file.good()) {
333 file.close();
334 RemoveProjectSaveTempFile(temp_path);
335 return absl::InternalError(absl::StrFormat(
336 "Failed to write temporary project file: %s", temp_path.string()));
337 }
338
339 file.close();
340 if (file.fail()) {
341 RemoveProjectSaveTempFile(temp_path);
342 return absl::InternalError(absl::StrFormat(
343 "Failed to close temporary project file: %s", temp_path.string()));
344 }
345
346 std::error_code rename_error;
347#if defined(_WIN32)
348 const DWORD move_flags =
349 MOVEFILE_WRITE_THROUGH |
350 (replace_existing ? MOVEFILE_REPLACE_EXISTING : static_cast<DWORD>(0));
351 if (!::MoveFileExW(temp_path.c_str(), target_path.c_str(), move_flags)) {
352 rename_error = std::error_code(static_cast<int>(::GetLastError()),
353 std::system_category());
354 }
355#else
356 if (replace_existing) {
357 std::filesystem::rename(temp_path, target_path, rename_error);
358 } else if (::link(temp_path.c_str(), target_path.c_str()) != 0) {
359 rename_error = std::error_code(errno, std::generic_category());
360 } else {
361 RemoveProjectSaveTempFile(temp_path);
362 }
363#endif
364 if (rename_error) {
365 RemoveProjectSaveTempFile(temp_path);
366 bool target_already_exists = rename_error == std::errc::file_exists;
367#if defined(_WIN32)
368 target_already_exists = target_already_exists ||
369 rename_error.value() == ERROR_FILE_EXISTS ||
370 rename_error.value() == ERROR_ALREADY_EXISTS;
371#endif
372 if (!replace_existing && target_already_exists) {
373 return absl::AlreadyExistsError(absl::StrFormat(
374 "Project file already exists: %s", target_path.string()));
375 }
376 return absl::InternalError(
377 absl::StrFormat("Failed to replace project file %s: %s",
378 target_path.string(), rename_error.message()));
379 }
380
381 return absl::OkStatus();
382}
383#endif
384} // namespace
385
386#ifndef __EMSCRIPTEN__
387absl::Status WriteProjectFileAtomically(absl::string_view target_path,
388 absl::string_view contents,
389 bool replace_existing) {
390 return WriteProjectFileAtomicallyImpl(
391 std::filesystem::path(std::string(target_path)), contents,
392 replace_existing);
393}
394#endif
395
396std::string RomRoleToString(RomRole role) {
397 switch (role) {
398 case RomRole::kBase:
399 return "base";
401 return "patched";
403 return "release";
404 case RomRole::kDev:
405 default:
406 return "dev";
407 }
408}
409
410RomRole ParseRomRole(absl::string_view value) {
411 std::string lower = ToLowerCopy(std::string(value));
412 if (lower == "base") {
413 return RomRole::kBase;
414 }
415 if (lower == "patched") {
416 return RomRole::kPatched;
417 }
418 if (lower == "release") {
419 return RomRole::kRelease;
420 }
421 return RomRole::kDev;
422}
423
425 switch (policy) {
427 return "allow";
429 return "block";
431 default:
432 return "warn";
433 }
434}
435
436RomWritePolicy ParseRomWritePolicy(absl::string_view value) {
437 std::string lower = ToLowerCopy(std::string(value));
438 if (lower == "allow") {
440 }
441 if (lower == "block") {
443 }
445}
446
447// YazeProject Implementation
449 auto& provider = zelda3::GetResourceLabels();
450 if (provider.hack_manifest() == &hack_manifest) {
451 provider.SetHackManifest(nullptr);
452 }
453 if (provider.GetAllProjectLabels() == &resource_labels) {
454 provider.SetProjectLabels(nullptr);
455 }
456}
457
458absl::Status YazeProject::Create(const std::string& project_name,
459 const std::string& base_path) {
460 name = project_name;
461 filepath = base_path + "/" + project_name + ".yaze";
462
463 // Initialize metadata
464 auto now = std::chrono::system_clock::now();
465 auto time_t = std::chrono::system_clock::to_time_t(now);
466 std::stringstream ss;
467 ss << std::put_time(std::localtime(&time_t), "%Y-%m-%d %H:%M:%S");
468
469 metadata.created_date = ss.str();
470 metadata.last_modified = ss.str();
472 metadata.version = "2.0";
473 metadata.created_by = "YAZE";
475
477
478#ifndef __EMSCRIPTEN__
479 // Create project directory structure
480 std::filesystem::path project_dir(base_path + "/" + project_name);
481 std::filesystem::create_directories(project_dir);
482 std::filesystem::create_directories(project_dir / "code");
483 std::filesystem::create_directories(project_dir / "assets");
484 std::filesystem::create_directories(project_dir / "patches");
485 std::filesystem::create_directories(project_dir / "backups");
486 std::filesystem::create_directories(project_dir / "output");
487
488 // Set folder paths
489 code_folder = (project_dir / "code").string();
490 assets_folder = (project_dir / "assets").string();
491 patches_folder = (project_dir / "patches").string();
492 rom_backup_folder = (project_dir / "backups").string();
493 output_folder = (project_dir / "output").string();
494 labels_filename = (project_dir / "labels.txt").string();
495 symbols_filename = (project_dir / "symbols.txt").string();
496#else
497 // WASM: keep paths relative; persistence handled by WasmStorage/IDBFS
498 code_folder = "code";
499 assets_folder = "assets";
500 patches_folder = "patches";
501 rom_backup_folder = "backups";
502 output_folder = "output";
503 labels_filename = "labels.txt";
504 symbols_filename = "symbols.txt";
505#endif
506
507 return Save();
508}
509
510// static
511std::string YazeProject::ResolveBundleRoot(const std::string& path) {
512 if (path.empty()) {
513 return {};
514 }
515
516 // Walk up the path hierarchy looking for a directory whose filename
517 // (extension) is ".yazeproj". The first match from the leaf upward wins.
518 std::error_code ec;
519 auto current = std::filesystem::path(path).lexically_normal();
520
521 for (; !current.empty(); current = current.parent_path()) {
522 if (current.extension() == ".yazeproj") {
523 // Must be an existing directory to count as a valid bundle root.
524 if (std::filesystem::is_directory(current, ec) && !ec) {
525 return current.string();
526 }
527 }
528 // Guard against infinite loop at the filesystem root.
529 if (current == current.parent_path()) {
530 break;
531 }
532 }
533
534 return {};
535}
536
537absl::Status YazeProject::Open(const std::string& project_path) {
538 // Resolve bundle root: if the user opened a file *inside* a .yazeproj
539 // bundle, normalize to the bundle root directory so the existing
540 // .yazeproj handling takes over.
541 std::string resolved_path = project_path;
542 const std::string bundle_root = ResolveBundleRoot(project_path);
543 if (!bundle_root.empty() && bundle_root != project_path) {
544 // The user pointed at a file inside a bundle; redirect to the root.
545 resolved_path = bundle_root;
546 }
547
548#ifndef __EMSCRIPTEN__
549 // Keep the project root stable even when a caller supplies a relative path.
550 // All project-relative files and registry fallbacks must resolve beside the
551 // project, not against whichever working directory happens to be active
552 // later in the session.
553 std::error_code absolute_ec;
554 const auto absolute_path =
555 std::filesystem::absolute(resolved_path, absolute_ec).lexically_normal();
556 if (!absolute_ec) {
557 resolved_path = absolute_path.string();
558 }
559#endif
560
561 filepath = resolved_path;
562
563#ifdef __EMSCRIPTEN__
564 // Prefer persistent storage in WASM builds
565 auto storage_key = MakeStorageKey("project");
566 auto storage_or = platform::WasmStorage::LoadProject(storage_key);
567 if (storage_or.ok()) {
568 return ParseFromString(storage_or.value());
569 }
570#endif
571
572 // Determine format and load accordingly
573 absl::Status load_status;
574 if (resolved_path.ends_with(".yazeproj")) {
576
577 const std::filesystem::path bundle_path(resolved_path);
578 std::error_code ec;
579 if (!std::filesystem::exists(bundle_path, ec) || ec ||
580 !std::filesystem::is_directory(bundle_path, ec) || ec) {
581 return absl::InvalidArgumentError(
582 absl::StrFormat("Project bundle does not exist: %s", resolved_path));
583 }
584
585 // Bundle convention: store the actual project config at the root so both
586 // desktop and iOS can open the same `.yazeproj` directory.
587 const std::filesystem::path project_file = bundle_path / "project.yaze";
588 filepath = project_file.string();
589
590 if (!std::filesystem::exists(project_file, ec) || ec) {
591 // Create a minimal portable project file for the bundle if missing.
593 name = bundle_path.stem().string();
594
595 // Initialize metadata timestamps (Create() normally does this).
596 auto now = std::chrono::system_clock::now();
597 auto time_t = std::chrono::system_clock::to_time_t(now);
598 std::stringstream ss;
599 ss << std::put_time(std::localtime(&time_t), "%Y-%m-%d %H:%M:%S");
600 if (metadata.created_date.empty()) {
601 metadata.created_date = ss.str();
602 }
603 metadata.last_modified = ss.str();
604 if (metadata.yaze_version.empty()) {
606 }
607 if (metadata.version.empty()) {
608 metadata.version = "2.0";
609 }
610 if (metadata.created_by.empty()) {
611 metadata.created_by = "YAZE";
612 }
613 if (metadata.project_id.empty()) {
615 }
616
617 // Bundle layout defaults (paths stored as absolute; serializer writes
618 // relative values for portability).
619 const std::filesystem::path rom_candidate = bundle_path / "rom";
620 // Always set the expected bundle ROM path even if the file is not yet
621 // present on disk (e.g. still downloading from iCloud). The load
622 // attempt in LoadProjectWithRom() handles the missing-file case without
623 // corrupting the project by saving a temporary path.
624 rom_filename = rom_candidate.string();
625
626 const std::filesystem::path project_dir = bundle_path / "project";
627 const std::filesystem::path code_dir = bundle_path / "code";
628 if (std::filesystem::exists(project_dir, ec) &&
629 std::filesystem::is_directory(project_dir, ec) && !ec) {
630 code_folder = project_dir.string();
631 } else if (std::filesystem::exists(code_dir, ec) &&
632 std::filesystem::is_directory(code_dir, ec) && !ec) {
633 code_folder = code_dir.string();
634 }
635
636 assets_folder = (bundle_path / "assets").string();
637 patches_folder = (bundle_path / "patches").string();
638 rom_backup_folder = (bundle_path / "backups").string();
639 output_folder = (bundle_path / "output").string();
640 labels_filename = (bundle_path / "labels.txt").string();
641 symbols_filename = (bundle_path / "symbols.txt").string();
642
643 load_status = SaveToYazeFormat();
644 } else {
645 load_status = LoadFromYazeFormat(project_file.string());
646 }
647 } else if (resolved_path.ends_with(".yaze")) {
649
650 // Try to detect if it's JSON format by peeking at first character
651 std::ifstream file(resolved_path);
652 if (file.is_open()) {
653 std::stringstream buffer;
654 buffer << file.rdbuf();
655 std::string content = buffer.str();
656
657#ifdef YAZE_ENABLE_JSON_PROJECT_FORMAT
658 if (!content.empty() && content.front() == '{') {
659 LOG_DEBUG("Project", "Detected JSON format project file");
660 load_status = LoadFromJsonFormat(resolved_path);
661 } else {
662 load_status = ParseFromString(content);
663 }
664#else
665 load_status = ParseFromString(content);
666#endif
667 } else {
668 return absl::InvalidArgumentError(
669 absl::StrFormat("Cannot open project file: %s", resolved_path));
670 }
671 } else if (resolved_path.ends_with(".zsproj")) {
673 load_status = ImportFromZScreamFormat(resolved_path);
674 } else {
675 return absl::InvalidArgumentError("Unsupported project file format");
676 }
677
678 if (!load_status.ok()) {
679 return load_status;
680 }
681
682 // Normalize project-relative paths so downstream code never depends on the
683 // process working directory (important for iOS and portable bundles).
685
686 // Auto-load z3dk project config if discoverable.
688
689 // Auto-load hack manifest if configured or discoverable
691
692 return absl::OkStatus();
693}
694
695absl::Status YazeProject::Save() {
696 return SaveToYazeFormat();
697}
698
699absl::Status YazeProject::SaveNew() {
700 return SaveToYazeFormat(false);
701}
702
703absl::Status YazeProject::LoadFromString(const std::string& content,
704 const std::string& project_path) {
705 if (project_path.empty()) {
706 return absl::InvalidArgumentError("Project file path cannot be empty");
707 }
708
709 *this = YazeProject();
710
711#ifndef __EMSCRIPTEN__
712 std::error_code ec;
713 const auto absolute_path =
714 std::filesystem::absolute(project_path, ec).lexically_normal();
715 filepath =
716 ec ? std::filesystem::path(project_path).lexically_normal().string()
717 : absolute_path.string();
718#else
719 filepath = project_path;
720#endif
722 try {
724 } catch (const std::exception& error) {
725 return absl::InvalidArgumentError(
726 absl::StrFormat("Invalid project file value: %s", error.what()));
727 }
731 return absl::OkStatus();
732}
733
734absl::Status YazeProject::SaveAs(const std::string& new_path) {
735 std::string old_filepath = filepath;
736 filepath = new_path;
737
738 auto status = Save();
739 if (!status.ok()) {
740 filepath = old_filepath; // Restore on failure
741 }
742
743 return status;
744}
745
746std::string YazeProject::MakeStorageKey(absl::string_view suffix) const {
747 std::string base;
748 if (!metadata.project_id.empty()) {
749 base = metadata.project_id;
750 } else if (!name.empty()) {
751 base = name;
752 } else if (!filepath.empty()) {
753 base = std::filesystem::path(filepath).stem().string();
754 }
755 base = SanitizeStorageKey(base);
756 if (suffix.empty()) {
757 return base;
758 }
759 return absl::StrFormat("%s_%s", base, suffix);
760}
761
762namespace {
763// A '\r' not followed by '\n' is a classic-Mac line ending. The parser can
764// normalize CRLF, but a lone CR would split a line somewhere it does not
765// expect, so both directions refuse it.
766bool ContainsLoneCarriageReturn(const std::string& content) {
767 for (size_t i = 0; i < content.size(); ++i) {
768 if (content[i] == '\r' &&
769 (i + 1 >= content.size() || content[i + 1] != '\n')) {
770 return true;
771 }
772 }
773 return false;
774}
775
776constexpr absl::string_view kProjectFileFooter = "# End of YAZE Project File";
777
778// Sections ParseFromString models. Everything else is passed through.
779bool IsKnownProjectSection(absl::string_view name) {
780 static constexpr absl::string_view kKnown[] = {"project",
781 "files",
782 "sprite_assets",
783 "rom",
784 "feature_flags",
785 "workspace",
786 "dungeon_overlay",
787 "graphics_sheets",
788 "atlas",
789 "planning",
790 "rom_addresses",
791 "custom_objects",
792 "agent_settings",
793 "build",
794 "keybindings",
795 "editor_visibility",
796 "zscream_compatibility",
797 "music"};
798 if (absl::StartsWith(name, "labels_")) {
799 return true;
800 }
801 return std::find(std::begin(kKnown), std::end(kKnown), name) !=
802 std::end(kKnown);
803}
804
805// Comments SerializeToString writes itself; keeping them would duplicate
806// them on every save.
807bool IsGeneratedProjectComment(absl::string_view line, bool before_sections) {
808 if (line == kProjectFileFooter) {
809 return true;
810 }
811 if (!before_sections) {
812 return false;
813 }
814 return absl::StartsWith(line, "# yaze Project File") ||
815 absl::StartsWith(line, "# Format Version:") ||
816 absl::StartsWith(line, "# Generated by YAZE") ||
817 absl::StartsWith(line, "# Last Modified:");
818}
819
821 ProjectFilePassthrough& passthrough, const std::string& name) {
822 for (auto& section : passthrough.sections) {
823 if (section.name == name) {
824 return section;
825 }
826 }
827 passthrough.sections.push_back({name, false, {}});
828 return passthrough.sections.back();
829}
830
831bool IsKeyLine(absl::string_view line) {
832 return !line.empty() && line.front() != '#' &&
833 line.find('=') != absl::string_view::npos;
834}
835
836// Writes `canonical` (SerializeToString's own output) with the passthrough
837// lines put back. See ProjectFilePassthrough for the placement rules.
839 const std::string& canonical, const ProjectFilePassthrough& passthrough) {
840 if (passthrough.empty()) {
841 return canonical;
842 }
844
845 struct CanonicalSection {
846 std::string header;
847 std::string name;
848 std::vector<std::string> lines;
849 };
850 std::vector<std::string> preamble;
851 std::vector<CanonicalSection> canonical_sections;
852 bool has_footer = false;
853 for (absl::string_view line : absl::StrSplit(canonical, '\n')) {
854 if (line == kProjectFileFooter) {
855 has_footer = true;
856 continue;
857 }
858 if (line.size() >= 2 && line.front() == '[' && line.back() == ']') {
859 canonical_sections.push_back(
860 {std::string(line),
861 std::string(line.substr(1, line.size() - 2)),
862 {}});
863 continue;
864 }
865 if (line.empty()) {
866 continue;
867 }
868 (canonical_sections.empty() ? preamble : canonical_sections.back().lines)
869 .push_back(std::string(line));
870 }
871
872 auto find_section =
873 [&](absl::string_view name) -> const ProjectFilePassthrough::Section* {
874 for (const auto& section : passthrough.sections) {
875 if (section.name == name) {
876 return &section;
877 }
878 }
879 return nullptr;
880 };
881
882 std::ostringstream out;
883 // Every comment and unknown line of `section`, in file order.
884 auto write_all = [&](const ProjectFilePassthrough::Section& section) {
885 for (const auto& line : section.lines) {
886 if (line.kind != Kind::kKnownKey) {
887 out << line.text << "\n";
888 }
889 }
890 };
891
892 for (const auto& line : preamble) {
893 out << line << "\n";
894 }
895 out << "\n";
896 if (const auto* before = find_section(""); before != nullptr) {
897 write_all(*before);
898 if (!before->lines.empty()) {
899 out << "\n";
900 }
901 }
902
903 std::vector<const ProjectFilePassthrough::Section*> merged;
904 for (const auto& section : canonical_sections) {
905 out << section.header << "\n";
906 const auto* kept = find_section(section.name);
907 if (kept == nullptr) {
908 for (const auto& line : section.lines) {
909 out << line << "\n";
910 }
911 out << "\n";
912 continue;
913 }
914 merged.push_back(kept);
915
916 // Comments belong to the next key line after them in the file.
917 std::vector<std::pair<std::string, std::vector<std::string>>> anchored;
918 std::vector<std::string> pending;
919 std::vector<std::pair<std::string, std::vector<std::string>>> unknown;
920 for (const auto& line : kept->lines) {
921 if (line.kind == Kind::kComment) {
922 pending.push_back(line.text);
923 } else if (line.kind == Kind::kUnknownLine) {
924 unknown.push_back({line.text, std::move(pending)});
925 pending.clear();
926 } else if (!pending.empty()) {
927 anchored.push_back({line.key, std::move(pending)});
928 pending.clear();
929 }
930 }
931 std::vector<bool> anchor_written(anchored.size(), false);
932 for (const auto& line : section.lines) {
933 if (IsKeyLine(line)) {
934 const std::string key = ParseKeyValue(line).first;
935 for (size_t i = 0; i < anchored.size(); ++i) {
936 if (!anchor_written[i] && anchored[i].first == key) {
937 for (const auto& comment : anchored[i].second) {
938 out << comment << "\n";
939 }
940 anchor_written[i] = true;
941 }
942 }
943 }
944 out << line << "\n";
945 }
946 for (const auto& [text, comments] : unknown) {
947 for (const auto& comment : comments) {
948 out << comment << "\n";
949 }
950 out << text << "\n";
951 }
952 // Comments whose key this build no longer writes, then trailing ones.
953 for (size_t i = 0; i < anchored.size(); ++i) {
954 if (!anchor_written[i]) {
955 for (const auto& comment : anchored[i].second) {
956 out << comment << "\n";
957 }
958 }
959 }
960 for (const auto& comment : pending) {
961 out << comment << "\n";
962 }
963 out << "\n";
964 }
965
966 // Unknown sections, and known sections this build left out (for example
967 // an emptied [graphics_sheets]) that still carry comments or unknown keys.
968 for (const auto& section : passthrough.sections) {
969 if (section.name.empty() ||
970 std::find(merged.begin(), merged.end(), &section) != merged.end()) {
971 continue;
972 }
973 const bool has_content = std::any_of(
974 section.lines.begin(), section.lines.end(),
975 [](const auto& line) { return line.kind != Kind::kKnownKey; });
976 if (!has_content) {
977 continue;
978 }
979 out << "[" << section.name << "]\n";
980 write_all(section);
981 out << "\n";
982 }
983
984 if (has_footer) {
985 out << kProjectFileFooter << "\n";
986 }
987 return out.str();
988}
989} // namespace
990
991bool IsKnownProjectFileSection(absl::string_view name) {
992 return IsKnownProjectSection(name);
993}
994
995absl::StatusOr<std::string> YazeProject::SerializeToString() const {
996 std::ostringstream file;
997
998 // Write header comment
999 file << "# yaze Project File\n";
1000 file << "# Format Version: 2.0\n";
1001 file << "# Generated by YAZE " << metadata.yaze_version << "\n";
1002 file << "# Last Modified: " << metadata.last_modified << "\n\n";
1003
1004 // Project section
1005 file << "[project]\n";
1006 file << "name=" << name << "\n";
1007 file << "description=" << metadata.description << "\n";
1008 file << "author=" << metadata.author << "\n";
1009 file << "license=" << metadata.license << "\n";
1010 file << "version=" << metadata.version << "\n";
1011 file << "created_date=" << metadata.created_date << "\n";
1012 file << "last_modified=" << metadata.last_modified << "\n";
1013 file << "yaze_version=" << metadata.yaze_version << "\n";
1014 file << "created_by=" << metadata.created_by << "\n";
1015 file << "project_id=" << metadata.project_id << "\n";
1016 file << "tags=" << absl::StrJoin(metadata.tags, ",") << "\n\n";
1017
1018 // Files section
1019 file << "[files]\n";
1020 file << "rom_filename=" << GetRelativePath(rom_filename) << "\n";
1021 file << "rom_backup_folder=" << GetRelativePath(rom_backup_folder) << "\n";
1022 file << "code_folder=" << GetRelativePath(code_folder) << "\n";
1023 file << "assets_folder=" << GetRelativePath(assets_folder) << "\n";
1024 file << "patches_folder=" << GetRelativePath(patches_folder) << "\n";
1025 file << "labels_filename=" << GetRelativePath(labels_filename) << "\n";
1026 file << "symbols_filename=" << GetRelativePath(symbols_filename) << "\n";
1027 if (!overworld_graphics_preview.empty()) {
1028 file << "overworld_graphics_preview="
1030 }
1031 if (!dungeon_tile_patterns.empty()) {
1032 file << "dungeon_tile_patterns="
1034 }
1035 if (!cutscene_shots.empty()) {
1036 file << "cutscene_shots="
1038 }
1039 if (!custom_collision_json.empty()) {
1040 file << "custom_collision_json="
1042 }
1043 file << "output_folder=" << GetRelativePath(output_folder) << "\n";
1044 file << "custom_objects_folder=" << GetRelativePath(custom_objects_folder)
1045 << "\n";
1046 file << "palette_catalog_file="
1048 file << "terrain_catalog_file="
1050 file << "hack_manifest_file=" << GetRelativePath(hack_manifest_file) << "\n";
1051 file << "sprite_catalog_file="
1053 file << "sprite_source_root="
1055 file << "additional_roms=" << absl::StrJoin(additional_roms, ",") << "\n\n";
1056
1057 file << "[sprite_assets]\n";
1058 for (size_t i = 0; i < sprite_assets.size(); ++i) {
1059 auto asset = sprite_assets[i];
1060 asset.zsm_path = GetRelativePath(GetAbsolutePath(asset.zsm_path));
1061 file << "asset_" << i << "=" << SpriteAssetToJson(asset).dump() << "\n";
1062 }
1063 file << "\n";
1064
1065 // ROM metadata section
1066 file << "[rom]\n";
1067 file << "role=" << RomRoleToString(rom_metadata.role) << "\n";
1068 file << "expected_hash=" << rom_metadata.expected_hash << "\n";
1069 file << "write_policy=" << RomWritePolicyToString(rom_metadata.write_policy)
1070 << "\n\n";
1071
1072 // Feature flags section
1073 file << "[feature_flags]\n";
1074 file << "load_custom_overworld="
1075 << (feature_flags.overworld.kLoadCustomOverworld ? "true" : "false")
1076 << "\n";
1077 file << "apply_zs_custom_overworld_asm="
1079 : "false")
1080 << "\n";
1081 file << "save_dungeon_maps="
1082 << (feature_flags.kSaveDungeonMaps ? "true" : "false") << "\n";
1083 file << "save_overworld_maps="
1084 << (feature_flags.overworld.kSaveOverworldMaps ? "true" : "false")
1085 << "\n";
1086 file << "save_overworld_entrances="
1087 << (feature_flags.overworld.kSaveOverworldEntrances ? "true" : "false")
1088 << "\n";
1089 file << "save_overworld_exits="
1090 << (feature_flags.overworld.kSaveOverworldExits ? "true" : "false")
1091 << "\n";
1092 file << "save_overworld_items="
1093 << (feature_flags.overworld.kSaveOverworldItems ? "true" : "false")
1094 << "\n";
1095 file << "save_overworld_properties="
1096 << (feature_flags.overworld.kSaveOverworldProperties ? "true" : "false")
1097 << "\n";
1098 file << "save_dungeon_objects="
1099 << (feature_flags.dungeon.kSaveObjects ? "true" : "false") << "\n";
1100 file << "save_dungeon_sprites="
1101 << (feature_flags.dungeon.kSaveSprites ? "true" : "false") << "\n";
1102 file << "save_dungeon_room_headers="
1103 << (feature_flags.dungeon.kSaveRoomHeaders ? "true" : "false") << "\n";
1104 file << "save_dungeon_torches="
1105 << (feature_flags.dungeon.kSaveTorches ? "true" : "false") << "\n";
1106 file << "save_dungeon_pits="
1107 << (feature_flags.dungeon.kSavePits ? "true" : "false") << "\n";
1108 file << "save_dungeon_blocks="
1109 << (feature_flags.dungeon.kSaveBlocks ? "true" : "false") << "\n";
1110 file << "save_dungeon_collision="
1111 << (feature_flags.dungeon.kSaveCollision ? "true" : "false") << "\n";
1112 file << "save_dungeon_water_fill_zones="
1113 << (feature_flags.dungeon.kSaveWaterFillZones ? "true" : "false")
1114 << "\n";
1115 file << "save_dungeon_chests="
1116 << (feature_flags.dungeon.kSaveChests ? "true" : "false") << "\n";
1117 file << "save_dungeon_pot_items="
1118 << (feature_flags.dungeon.kSavePotItems ? "true" : "false") << "\n";
1119 file << "save_dungeon_entrances="
1120 << (feature_flags.dungeon.kSaveEntrances ? "true" : "false") << "\n";
1121 file << "save_dungeon_palettes="
1122 << (feature_flags.dungeon.kSavePalettes ? "true" : "false") << "\n";
1123 file << "save_graphics_sheet="
1124 << (feature_flags.kSaveGraphicsSheet ? "true" : "false") << "\n";
1125 file << "save_all_palettes="
1126 << (feature_flags.kSaveAllPalettes ? "true" : "false") << "\n";
1127 file << "save_gfx_groups="
1128 << (feature_flags.kSaveGfxGroups ? "true" : "false") << "\n";
1129 file << "save_messages=" << (feature_flags.kSaveMessages ? "true" : "false")
1130 << "\n";
1131 file << "enable_custom_objects="
1132 << (feature_flags.kEnableCustomObjects ? "true" : "false") << "\n\n";
1133
1134 // Workspace settings section
1135 file << "[workspace]\n";
1136 file << "font_global_scale=" << workspace_settings.font_global_scale << "\n";
1137 file << "dark_mode=" << (workspace_settings.dark_mode ? "true" : "false")
1138 << "\n";
1139 file << "ui_theme=" << workspace_settings.ui_theme << "\n";
1140 file << "autosave_enabled="
1141 << (workspace_settings.autosave_enabled ? "true" : "false") << "\n";
1142 file << "autosave_interval_secs=" << workspace_settings.autosave_interval_secs
1143 << "\n";
1144 file << "backup_on_save="
1145 << (workspace_settings.backup_on_save ? "true" : "false") << "\n";
1146 file << "backup_retention_count=" << workspace_settings.backup_retention_count
1147 << "\n";
1148 file << "backup_keep_daily="
1149 << (workspace_settings.backup_keep_daily ? "true" : "false") << "\n";
1150 file << "backup_keep_daily_days=" << workspace_settings.backup_keep_daily_days
1151 << "\n";
1152 file << "show_grid=" << (workspace_settings.show_grid ? "true" : "false")
1153 << "\n";
1154 file << "show_collision="
1155 << (workspace_settings.show_collision ? "true" : "false") << "\n";
1156 file << "prefer_hmagic_names="
1157 << (workspace_settings.prefer_hmagic_names ? "true" : "false") << "\n";
1158 file << "last_layout_preset=" << workspace_settings.last_layout_preset
1159 << "\n";
1160 file << "saved_layouts="
1161 << absl::StrJoin(workspace_settings.saved_layouts, ",") << "\n";
1162 file << "recent_files=" << absl::StrJoin(workspace_settings.recent_files, ",")
1163 << "\n\n";
1164
1165 // Dungeon overlay settings section
1166 auto track_tiles = dungeon_overlay.track_tiles;
1167 if (track_tiles.empty()) {
1168 for (uint16_t tile = 0xB0; tile <= 0xBE; ++tile) {
1169 track_tiles.push_back(tile);
1170 }
1171 }
1172 auto track_stop_tiles = dungeon_overlay.track_stop_tiles;
1173 if (track_stop_tiles.empty()) {
1174 track_stop_tiles = {0xB7, 0xB8, 0xB9, 0xBA};
1175 }
1176 auto track_switch_tiles = dungeon_overlay.track_switch_tiles;
1177 if (track_switch_tiles.empty()) {
1178 track_switch_tiles = {0xD0, 0xD1, 0xD2, 0xD3};
1179 }
1180 auto track_object_ids = dungeon_overlay.track_object_ids;
1181 if (track_object_ids.empty()) {
1182 track_object_ids = {0x31};
1183 }
1184 auto minecart_sprite_ids = dungeon_overlay.minecart_sprite_ids;
1185 if (minecart_sprite_ids.empty()) {
1186 minecart_sprite_ids = {0xA3};
1187 }
1188 file << "[dungeon_overlay]\n";
1189 file << "track_tiles=" << FormatHexUintList(track_tiles) << "\n";
1190 file << "track_stop_tiles=" << FormatHexUintList(track_stop_tiles) << "\n";
1191 file << "track_switch_tiles=" << FormatHexUintList(track_switch_tiles)
1192 << "\n";
1193 file << "track_object_ids=" << FormatHexUintList(track_object_ids) << "\n";
1194 file << "minecart_sprite_ids=" << FormatHexUintList(minecart_sprite_ids)
1195 << "\n\n";
1196
1197 if (!graphics_sheets.empty()) {
1198 file << "[graphics_sheets]\n";
1199 file << "reserved_sheets="
1200 << FormatHexUintList(graphics_sheets.reserved_sheets) << "\n";
1201 file << "flagged_sheets="
1202 << FormatHexUintList(graphics_sheets.flagged_sheets) << "\n";
1203 file << "reserved_blocks="
1204 << FormatHexUintListMap(graphics_sheets.reserved_blocks) << "\n\n";
1205 }
1206
1208 file << "[atlas]\n";
1209 file << "annotations=" << absl::StrJoin(atlas.annotations, ",") << "\n";
1210 file << "defaults=" << atlas.defaults << "\n";
1211 file << "symbols=" << atlas.symbols << "\n";
1212 file << "export_folder=" << atlas.export_folder << "\n\n";
1213 }
1214
1216 file << "[planning]\n";
1217 file << "file=" << planning.file << "\n\n";
1218 }
1219
1220 if (!rom_address_overrides.addresses.empty()) {
1221 file << "[rom_addresses]\n";
1222 for (const auto& [key, value] : rom_address_overrides.addresses) {
1223 file << key << "=" << FormatHexUint32(value) << "\n";
1224 }
1225 file << "\n";
1226 }
1227
1228 if (!custom_object_files.empty()) {
1229 file << "[custom_objects]\n";
1230 for (const auto& [object_id, files] : custom_object_files) {
1231 file << absl::StrFormat("object_0x%X", object_id) << "="
1232 << absl::StrJoin(files, ",") << "\n";
1233 }
1234 file << "\n";
1235 }
1236
1237 // AI Agent settings section
1238 file << "[agent_settings]\n";
1239 file << "ai_provider=" << agent_settings.ai_provider << "\n";
1240 file << "ai_model=" << agent_settings.ai_model << "\n";
1241 file << "ollama_host=" << agent_settings.ollama_host << "\n";
1242 file << "gemini_api_key=" << agent_settings.gemini_api_key << "\n";
1243 file << "custom_system_prompt="
1245 << "\n";
1246 file << "use_custom_prompt="
1247 << (agent_settings.use_custom_prompt ? "true" : "false") << "\n";
1248 file << "show_reasoning="
1249 << (agent_settings.show_reasoning ? "true" : "false") << "\n";
1250 file << "verbose=" << (agent_settings.verbose ? "true" : "false") << "\n";
1251 file << "max_tool_iterations=" << agent_settings.max_tool_iterations << "\n";
1252 file << "max_retry_attempts=" << agent_settings.max_retry_attempts << "\n";
1253 file << "temperature=" << agent_settings.temperature << "\n";
1254 file << "top_p=" << agent_settings.top_p << "\n";
1255 file << "max_output_tokens=" << agent_settings.max_output_tokens << "\n";
1256 file << "stream_responses="
1257 << (agent_settings.stream_responses ? "true" : "false") << "\n";
1258 file << "favorite_models="
1259 << absl::StrJoin(agent_settings.favorite_models, ",") << "\n";
1260 file << "model_chain=" << absl::StrJoin(agent_settings.model_chain, ",")
1261 << "\n";
1262 file << "chain_mode=" << agent_settings.chain_mode << "\n";
1263 file << "enable_tool_resources="
1264 << (agent_settings.enable_tool_resources ? "true" : "false") << "\n";
1265 file << "enable_tool_dungeon="
1266 << (agent_settings.enable_tool_dungeon ? "true" : "false") << "\n";
1267 file << "enable_tool_overworld="
1268 << (agent_settings.enable_tool_overworld ? "true" : "false") << "\n";
1269 file << "enable_tool_messages="
1270 << (agent_settings.enable_tool_messages ? "true" : "false") << "\n";
1271 file << "enable_tool_dialogue="
1272 << (agent_settings.enable_tool_dialogue ? "true" : "false") << "\n";
1273 file << "enable_tool_gui="
1274 << (agent_settings.enable_tool_gui ? "true" : "false") << "\n";
1275 file << "enable_tool_music="
1276 << (agent_settings.enable_tool_music ? "true" : "false") << "\n";
1277 file << "enable_tool_sprite="
1278 << (agent_settings.enable_tool_sprite ? "true" : "false") << "\n";
1279 file << "enable_tool_emulator="
1280 << (agent_settings.enable_tool_emulator ? "true" : "false") << "\n";
1281 file << "enable_tool_memory_inspector="
1282 << (agent_settings.enable_tool_memory_inspector ? "true" : "false")
1283 << "\n";
1284 file << "builder_blueprint_path=" << agent_settings.builder_blueprint_path
1285 << "\n\n";
1286
1287 // Custom keybindings section
1289 file << "[keybindings]\n";
1290 for (const auto& [key, value] : workspace_settings.custom_keybindings) {
1291 file << key << "=" << value << "\n";
1292 }
1293 file << "\n";
1294 }
1295
1296 // Editor visibility section
1298 file << "[editor_visibility]\n";
1299 for (const auto& [key, value] : workspace_settings.editor_visibility) {
1300 file << key << "=" << (value ? "true" : "false") << "\n";
1301 }
1302 file << "\n";
1303 }
1304
1305 // Resource labels sections
1306 for (const auto& [type, labels] : resource_labels) {
1307 if (!labels.empty()) {
1308 file << "[labels_" << type << "]\n";
1309 for (const auto& [key, value] : labels) {
1310 file << key << "=" << value << "\n";
1311 }
1312 file << "\n";
1313 }
1314 }
1315
1316 // Build settings section
1317 file << "[build]\n";
1318 file << "build_script=" << build_script << "\n";
1319 file << "output_folder=" << GetRelativePath(output_folder) << "\n";
1320 file << "git_repository=" << git_repository << "\n";
1321 file << "track_changes=" << (track_changes ? "true" : "false") << "\n";
1322 file << "build_configurations=" << absl::StrJoin(build_configurations, ",")
1323 << "\n";
1324 file << "build_target=" << build_target << "\n";
1325 file << "asm_entry_point=" << asm_entry_point << "\n";
1326 file << "asm_sources=" << absl::StrJoin(asm_sources, ",") << "\n";
1327 file << "last_build_hash=" << last_build_hash << "\n";
1328 file << "build_number=" << build_number << "\n\n";
1329
1330 // Music persistence section (for WASM/offline state)
1331 file << "[music]\n";
1332 file << "persist_custom_music="
1333 << (music_persistence.persist_custom_music ? "true" : "false") << "\n";
1334 file << "storage_key=" << music_persistence.storage_key << "\n";
1335 file << "last_saved_at=" << music_persistence.last_saved_at << "\n\n";
1336
1337 // ZScream compatibility section
1338 if (!zscream_project_file.empty()) {
1339 file << "[zscream_compatibility]\n";
1340 file << "original_project_file=" << zscream_project_file << "\n";
1341 for (const auto& [key, value] : zscream_mappings) {
1342 file << key << "=" << value << "\n";
1343 }
1344 file << "\n";
1345 }
1346
1347 file << "# End of YAZE Project File\n";
1348
1349 // Refuse to WRITE what ParseFromString refuses to READ. Values are streamed
1350 // verbatim — metadata.description and resource labels injected from
1351 // hack_manifest.json among them — so a lone CR pasted from an old-Mac
1352 // source would round-trip through Save() and leave the project permanently
1353 // unopenable, with the previous good file already overwritten. Failing here
1354 // keeps that file intact.
1355 std::string serialized =
1356 MergeProjectFilePassthrough(file.str(), file_passthrough);
1357 if (ContainsLoneCarriageReturn(serialized)) {
1358 return absl::InvalidArgumentError(
1359 "Project contains a lone carriage return in a value; refusing to "
1360 "write a descriptor that could not be read back");
1361 }
1362 return serialized;
1363}
1364
1365absl::Status YazeProject::ParseFromString(const std::string& content) {
1366 if (ContainsLoneCarriageReturn(content)) {
1367 return absl::InvalidArgumentError(
1368 "Project file contains unsupported lone carriage returns");
1369 }
1370
1371 sprite_catalog_file.clear();
1372 sprite_source_root.clear();
1373 sprite_assets.clear();
1374 terrain_catalog_file.clear();
1375 palette_catalog_file.clear();
1376 dungeon_tile_patterns.clear();
1378 atlas = AtlasSettings{};
1380 std::istringstream stream(content);
1381 std::string line;
1382 std::string current_section;
1383 ProjectFilePassthrough::Section* passthrough_section =
1384 &PassthroughSection(file_passthrough, "");
1385
1386 while (std::getline(stream, line)) {
1387 // std::getline() consumes '\n' but preserves the '\r' in CRLF input.
1388 // Normalize that line ending before comment, section, or value parsing so
1389 // project safety flags have identical behavior across platforms.
1390 if (!line.empty() && line.back() == '\r') {
1391 line.pop_back();
1392 }
1393
1394 if (line.find_first_not_of(" \t") == std::string::npos)
1395 continue;
1396
1397 if (line[0] == '#') {
1398 if (!IsGeneratedProjectComment(line, current_section.empty())) {
1399 passthrough_section->lines.push_back(
1401 }
1402 continue;
1403 }
1404
1405 if (line.front() == '[' && line.back() == ']') {
1406 current_section = line.substr(1, line.length() - 2);
1407 passthrough_section =
1408 &PassthroughSection(file_passthrough, current_section);
1409 passthrough_section->known = IsKnownProjectSection(current_section);
1410 if (current_section == "atlas") {
1411 atlas.section_present = true;
1412 } else if (current_section == "planning") {
1414 }
1415 continue;
1416 }
1417
1418 auto [key, value] = ParseKeyValue(line);
1419 // False when this build does not model the line; it is then kept verbatim
1420 // and written back on save.
1421 bool understood = !key.empty();
1422 if (key.empty()) {
1423 // No "key=value": nothing to parse.
1424 } else if (current_section == "project") {
1425 if (key == "name")
1426 name = value;
1427 else if (key == "description")
1428 metadata.description = value;
1429 else if (key == "author")
1430 metadata.author = value;
1431 else if (key == "license")
1432 metadata.license = value;
1433 else if (key == "version")
1434 metadata.version = value;
1435 else if (key == "created_date")
1436 metadata.created_date = value;
1437 else if (key == "last_modified")
1438 metadata.last_modified = value;
1439 else if (key == "yaze_version")
1440 metadata.yaze_version = value;
1441 else if (key == "created_by")
1442 metadata.created_by = value;
1443 else if (key == "tags")
1444 metadata.tags = ParseStringList(value);
1445 else if (key == "project_id")
1446 metadata.project_id = value;
1447 else
1448 understood = false;
1449 } else if (current_section == "files") {
1450 if (key == "rom_filename")
1451 rom_filename = value;
1452 else if (key == "rom_backup_folder")
1453 rom_backup_folder = value;
1454 else if (key == "code_folder")
1455 code_folder = value;
1456 else if (key == "assets_folder")
1457 assets_folder = value;
1458 else if (key == "patches_folder")
1459 patches_folder = value;
1460 else if (key == "labels_filename")
1461 labels_filename = value;
1462 else if (key == "symbols_filename")
1463 symbols_filename = value;
1464 else if (key == "overworld_graphics_preview")
1466 else if (key == "dungeon_tile_patterns")
1467 dungeon_tile_patterns = value;
1468 else if (key == "cutscene_shots")
1469 cutscene_shots = value;
1470 else if (key == "custom_collision_json")
1471 custom_collision_json = value;
1472 else if (key == "output_folder")
1473 output_folder = value;
1474 else if (key == "custom_objects_folder")
1475 custom_objects_folder = value;
1476 else if (key == "palette_catalog_file")
1477 palette_catalog_file = value;
1478 else if (key == "terrain_catalog_file")
1479 terrain_catalog_file = value;
1480 else if (key == "hack_manifest_file")
1481 hack_manifest_file = value;
1482 else if (key == "sprite_catalog_file")
1483 sprite_catalog_file = value;
1484 else if (key == "sprite_source_root")
1485 sprite_source_root = value;
1486 else if (key == "additional_roms")
1487 additional_roms = ParseStringList(value);
1488 else
1489 understood = false;
1490 } else if (current_section == "sprite_assets") {
1491 if (key.rfind("asset_", 0) != 0 || value.size() > 65536 ||
1492 sprite_assets.size() >= 4096)
1493 return absl::InvalidArgumentError("Invalid sprite_assets section");
1494 auto asset = ParseSpriteAssetBinding(value);
1495 if (!asset.ok())
1496 return asset.status();
1497 for (const auto& prior : sprite_assets)
1498 if (prior.zsm_path == asset->zsm_path)
1499 return absl::InvalidArgumentError("Duplicate sprite asset path");
1500 sprite_assets.push_back(std::move(*asset));
1501 } else if (current_section == "rom") {
1502 if (key == "role")
1504 else if (key == "expected_hash")
1506 else if (key == "write_policy")
1508 else
1509 understood = false;
1510 } else if (current_section == "feature_flags") {
1511 if (key == "load_custom_overworld")
1512 feature_flags.overworld.kLoadCustomOverworld = ParseBool(value);
1513 else if (key == "apply_zs_custom_overworld_asm")
1515 else if (key == "save_dungeon_maps")
1516 feature_flags.kSaveDungeonMaps = ParseBool(value);
1517 else if (key == "save_overworld_maps")
1518 feature_flags.overworld.kSaveOverworldMaps = ParseBool(value);
1519 else if (key == "save_overworld_entrances")
1521 else if (key == "save_overworld_exits")
1522 feature_flags.overworld.kSaveOverworldExits = ParseBool(value);
1523 else if (key == "save_overworld_items")
1524 feature_flags.overworld.kSaveOverworldItems = ParseBool(value);
1525 else if (key == "save_overworld_properties")
1527 else if (key == "save_dungeon_objects")
1528 feature_flags.dungeon.kSaveObjects = ParseBool(value);
1529 else if (key == "save_dungeon_sprites")
1530 feature_flags.dungeon.kSaveSprites = ParseBool(value);
1531 else if (key == "save_dungeon_room_headers")
1532 feature_flags.dungeon.kSaveRoomHeaders = ParseBool(value);
1533 else if (key == "save_dungeon_torches")
1534 feature_flags.dungeon.kSaveTorches = ParseBool(value);
1535 else if (key == "save_dungeon_pits")
1536 feature_flags.dungeon.kSavePits = ParseBool(value);
1537 else if (key == "save_dungeon_blocks")
1538 feature_flags.dungeon.kSaveBlocks = ParseBool(value);
1539 else if (key == "save_dungeon_collision")
1540 feature_flags.dungeon.kSaveCollision = ParseBool(value);
1541 else if (key == "save_dungeon_water_fill_zones")
1542 feature_flags.dungeon.kSaveWaterFillZones = ParseBool(value);
1543 else if (key == "save_dungeon_chests")
1544 feature_flags.dungeon.kSaveChests = ParseBool(value);
1545 else if (key == "save_dungeon_pot_items")
1546 feature_flags.dungeon.kSavePotItems = ParseBool(value);
1547 else if (key == "save_dungeon_entrances")
1548 feature_flags.dungeon.kSaveEntrances = ParseBool(value);
1549 else if (key == "save_dungeon_palettes")
1550 feature_flags.dungeon.kSavePalettes = ParseBool(value);
1551 else if (key == "save_graphics_sheet")
1552 feature_flags.kSaveGraphicsSheet = ParseBool(value);
1553 else if (key == "save_all_palettes")
1554 feature_flags.kSaveAllPalettes = ParseBool(value);
1555 else if (key == "save_gfx_groups")
1556 feature_flags.kSaveGfxGroups = ParseBool(value);
1557 else if (key == "save_messages")
1558 feature_flags.kSaveMessages = ParseBool(value);
1559 else if (key == "enable_custom_objects")
1560 feature_flags.kEnableCustomObjects = ParseBool(value);
1561 else
1562 understood = false;
1563 } else if (current_section == "workspace") {
1564 if (key == "font_global_scale")
1565 workspace_settings.font_global_scale = ParseFloat(value);
1566 else if (key == "dark_mode")
1567 workspace_settings.dark_mode = ParseBool(value);
1568 else if (key == "ui_theme")
1570 else if (key == "autosave_enabled")
1571 workspace_settings.autosave_enabled = ParseBool(value);
1572 else if (key == "autosave_interval_secs")
1573 workspace_settings.autosave_interval_secs = ParseFloat(value);
1574 else if (key == "backup_on_save")
1575 workspace_settings.backup_on_save = ParseBool(value);
1576 else if (key == "backup_retention_count")
1577 workspace_settings.backup_retention_count = std::stoi(value);
1578 else if (key == "backup_keep_daily")
1579 workspace_settings.backup_keep_daily = ParseBool(value);
1580 else if (key == "backup_keep_daily_days")
1581 workspace_settings.backup_keep_daily_days = std::stoi(value);
1582 else if (key == "show_grid")
1583 workspace_settings.show_grid = ParseBool(value);
1584 else if (key == "show_collision")
1585 workspace_settings.show_collision = ParseBool(value);
1586 else if (key == "prefer_hmagic_names")
1587 workspace_settings.prefer_hmagic_names = ParseBool(value);
1588 else if (key == "last_layout_preset")
1590 else if (key == "saved_layouts")
1591 workspace_settings.saved_layouts = ParseStringList(value);
1592 else if (key == "recent_files")
1593 workspace_settings.recent_files = ParseStringList(value);
1594 else
1595 understood = false;
1596 } else if (current_section == "dungeon_overlay") {
1597 if (key == "track_tiles")
1598 dungeon_overlay.track_tiles = ParseHexUintList(value);
1599 else if (key == "track_stop_tiles")
1600 dungeon_overlay.track_stop_tiles = ParseHexUintList(value);
1601 else if (key == "track_switch_tiles")
1602 dungeon_overlay.track_switch_tiles = ParseHexUintList(value);
1603 else if (key == "track_object_ids")
1604 dungeon_overlay.track_object_ids = ParseHexUintList(value);
1605 else if (key == "minecart_sprite_ids")
1606 dungeon_overlay.minecart_sprite_ids = ParseHexUintList(value);
1607 else
1608 understood = false;
1609 } else if (current_section == "graphics_sheets") {
1610 if (key == "reserved_sheets")
1611 graphics_sheets.reserved_sheets = ParseHexUintList(value);
1612 else if (key == "flagged_sheets")
1613 graphics_sheets.flagged_sheets = ParseHexUintList(value);
1614 else if (key == "reserved_blocks")
1615 graphics_sheets.reserved_blocks = ParseHexUintListMap(value);
1616 else
1617 understood = false;
1618 } else if (current_section == "atlas") {
1619 if (key == "annotations")
1620 atlas.annotations = ParseStringList(value);
1621 else if (key == "defaults")
1622 atlas.defaults = value.empty() ? "vanilla" : value;
1623 else if (key == "symbols")
1624 atlas.symbols = value;
1625 else if (key == "export_folder")
1626 atlas.export_folder = value;
1627 else
1628 understood = false;
1629 } else if (current_section == "planning") {
1630 if (key == "file")
1631 planning.file = value;
1632 else
1633 understood = false;
1634 } else if (current_section == "rom_addresses") {
1635 auto parsed = ParseHexUint32(value);
1636 if (parsed.has_value()) {
1637 rom_address_overrides.addresses[key] = *parsed;
1638 } else {
1639 understood = false;
1640 }
1641 } else if (current_section == "custom_objects") {
1642 std::string id_token = key;
1643 if (absl::StartsWith(id_token, "object_")) {
1644 id_token = id_token.substr(7);
1645 }
1646 auto parsed = ParseHexUint32(id_token);
1647 if (parsed.has_value()) {
1648 custom_object_files[static_cast<int>(*parsed)] =
1649 ParsePositionalStringList(value);
1650 } else {
1651 understood = false;
1652 }
1653 } else if (current_section == "agent_settings") {
1654 if (key == "ai_provider")
1656 else if (key == "ai_model")
1657 agent_settings.ai_model = value;
1658 else if (key == "ollama_host")
1660 else if (key == "gemini_api_key")
1662 else if (key == "custom_system_prompt")
1664 else if (key == "use_custom_prompt")
1665 agent_settings.use_custom_prompt = ParseBool(value);
1666 else if (key == "show_reasoning")
1667 agent_settings.show_reasoning = ParseBool(value);
1668 else if (key == "verbose")
1669 agent_settings.verbose = ParseBool(value);
1670 else if (key == "max_tool_iterations")
1671 agent_settings.max_tool_iterations = std::stoi(value);
1672 else if (key == "max_retry_attempts")
1673 agent_settings.max_retry_attempts = std::stoi(value);
1674 else if (key == "temperature")
1675 agent_settings.temperature = ParseFloat(value);
1676 else if (key == "top_p")
1677 agent_settings.top_p = ParseFloat(value);
1678 else if (key == "max_output_tokens")
1679 agent_settings.max_output_tokens = std::stoi(value);
1680 else if (key == "stream_responses")
1681 agent_settings.stream_responses = ParseBool(value);
1682 else if (key == "favorite_models")
1683 agent_settings.favorite_models = ParseStringList(value);
1684 else if (key == "model_chain")
1685 agent_settings.model_chain = ParseStringList(value);
1686 else if (key == "chain_mode")
1687 agent_settings.chain_mode = std::stoi(value);
1688 else if (key == "enable_tool_resources")
1689 agent_settings.enable_tool_resources = ParseBool(value);
1690 else if (key == "enable_tool_dungeon")
1691 agent_settings.enable_tool_dungeon = ParseBool(value);
1692 else if (key == "enable_tool_overworld")
1693 agent_settings.enable_tool_overworld = ParseBool(value);
1694 else if (key == "enable_tool_messages")
1695 agent_settings.enable_tool_messages = ParseBool(value);
1696 else if (key == "enable_tool_dialogue")
1697 agent_settings.enable_tool_dialogue = ParseBool(value);
1698 else if (key == "enable_tool_gui")
1699 agent_settings.enable_tool_gui = ParseBool(value);
1700 else if (key == "enable_tool_music")
1701 agent_settings.enable_tool_music = ParseBool(value);
1702 else if (key == "enable_tool_sprite")
1703 agent_settings.enable_tool_sprite = ParseBool(value);
1704 else if (key == "enable_tool_emulator")
1705 agent_settings.enable_tool_emulator = ParseBool(value);
1706 else if (key == "enable_tool_memory_inspector")
1708 else if (key == "builder_blueprint_path")
1710 else
1711 understood = false;
1712 } else if (current_section == "build") {
1713 if (key == "build_script")
1714 build_script = value;
1715 else if (key == "output_folder")
1716 output_folder = value;
1717 else if (key == "git_repository")
1718 git_repository = value;
1719 else if (key == "track_changes")
1720 track_changes = ParseBool(value);
1721 else if (key == "build_configurations")
1722 build_configurations = ParseStringList(value);
1723 else if (key == "build_target")
1724 build_target = value;
1725 else if (key == "asm_entry_point")
1726 asm_entry_point = value;
1727 else if (key == "asm_sources")
1728 asm_sources = ParseStringList(value);
1729 else if (key == "last_build_hash")
1730 last_build_hash = value;
1731 else if (key == "build_number")
1732 build_number = std::stoi(value);
1733 else
1734 understood = false;
1735 } else if (current_section.rfind("labels_", 0) == 0) {
1736 std::string label_type = current_section.substr(7);
1737 resource_labels[label_type][key] = value;
1738 } else if (current_section == "keybindings") {
1740 } else if (current_section == "editor_visibility") {
1741 workspace_settings.editor_visibility[key] = ParseBool(value);
1742 } else if (current_section == "zscream_compatibility") {
1743 if (key == "original_project_file")
1744 zscream_project_file = value;
1745 else
1746 zscream_mappings[key] = value;
1747 } else if (current_section == "music") {
1748 if (key == "persist_custom_music")
1749 music_persistence.persist_custom_music = ParseBool(value);
1750 else if (key == "storage_key")
1752 else if (key == "last_saved_at")
1754 else
1755 understood = false;
1756 } else {
1757 understood = false; // A section this build does not know.
1758 }
1759 passthrough_section->lines.push_back(
1760 understood
1765 }
1766
1767 MigrateGfxGroupLabelKeys(resource_labels);
1768
1769 if (metadata.project_id.empty()) {
1771 }
1772 if (metadata.created_by.empty()) {
1773 metadata.created_by = "YAZE";
1774 }
1775 if (music_persistence.storage_key.empty()) {
1777 }
1778
1779 return absl::OkStatus();
1780}
1781
1782absl::Status YazeProject::LoadFromYazeFormat(const std::string& project_path) {
1783#ifdef __EMSCRIPTEN__
1784 auto storage_key = MakeStorageKey("project");
1785 auto storage_or = platform::WasmStorage::LoadProject(storage_key);
1786 if (storage_or.ok()) {
1787 return ParseFromString(storage_or.value());
1788 }
1789#endif // __EMSCRIPTEN__
1790
1791 std::ifstream file(project_path);
1792 if (!file.is_open()) {
1793 return absl::InvalidArgumentError(
1794 absl::StrFormat("Cannot open project file: %s", project_path));
1795 }
1796
1797 std::stringstream buffer;
1798 buffer << file.rdbuf();
1799 file.close();
1800 return ParseFromString(buffer.str());
1801}
1802
1803absl::Status YazeProject::SaveToYazeFormat(bool replace_existing) {
1804 // Update last modified timestamp
1805 auto now = std::chrono::system_clock::now();
1806 auto time_t = std::chrono::system_clock::to_time_t(now);
1807 std::stringstream ss;
1808 ss << std::put_time(std::localtime(&time_t), "%Y-%m-%d %H:%M:%S");
1809 metadata.last_modified = ss.str();
1810 if (music_persistence.storage_key.empty()) {
1812 }
1813
1814 // Ensure we serialize clean relative paths even if the user edited fields
1815 // into relative form (and avoid relying on cwd when opening later).
1817
1818 ASSIGN_OR_RETURN(auto serialized, SerializeToString());
1819
1820#ifdef __EMSCRIPTEN__
1821 auto storage_status = platform::WasmStorage::SaveProject(
1822 MakeStorageKey("project"), serialized, replace_existing);
1823 if (!storage_status.ok()) {
1824 return storage_status;
1825 }
1826#else
1827 if (!filepath.empty()) {
1829 WriteProjectFileAtomically(filepath, serialized, replace_existing));
1830 }
1831#endif
1832
1833 return absl::OkStatus();
1834}
1835
1837 const std::string& zscream_project_path) {
1838 // Basic ZScream project import (to be expanded based on ZScream format)
1839 zscream_project_file = zscream_project_path;
1841
1842 // Extract project name from path
1843 std::filesystem::path zs_path(zscream_project_path);
1844 name = zs_path.stem().string() + "_imported";
1845
1846 // Set up basic mapping for common fields
1847 zscream_mappings["rom_file"] = "rom_filename";
1848 zscream_mappings["source_code"] = "code_folder";
1849 zscream_mappings["project_name"] = "name";
1850
1852
1853 // TODO: Implement actual ZScream format parsing when format is known
1854 // For now, just create a project structure that can be manually configured
1855
1856 return absl::OkStatus();
1857}
1858
1859absl::Status YazeProject::ExportForZScream(const std::string& target_path) {
1860 // Create a simplified project file that ZScream might understand
1861 std::ofstream file(target_path);
1862 if (!file.is_open()) {
1863 return absl::InvalidArgumentError(
1864 absl::StrFormat("Cannot create ZScream project file: %s", target_path));
1865 }
1866
1867 // Write in a simple format that ZScream might understand
1868 file << "# ZScream Compatible Project File\n";
1869 file << "# Exported from YAZE " << metadata.yaze_version << "\n\n";
1870 file << "name=" << name << "\n";
1871 file << "rom_file=" << rom_filename << "\n";
1872 file << "source_code=" << code_folder << "\n";
1873 file << "description=" << metadata.description << "\n";
1874 file << "author=" << metadata.author << "\n";
1875 file << "created_with=YAZE " << metadata.yaze_version << "\n";
1876
1877 file.close();
1878 return absl::OkStatus();
1879}
1880
1882 // Consolidated loading of all settings from project file
1883 // This replaces scattered config loading throughout the application
1885}
1886
1888 // Consolidated saving of all settings to project file
1889 return SaveToYazeFormat();
1890}
1891
1894 return Save();
1895}
1896
1897absl::Status YazeProject::Validate() const {
1898 std::vector<std::string> errors;
1899
1900 if (name.empty())
1901 errors.push_back("Project name is required");
1902 if (filepath.empty())
1903 errors.push_back("Project file path is required");
1904 if (rom_filename.empty())
1905 errors.push_back("ROM file is required");
1906
1907#ifndef __EMSCRIPTEN__
1908 // Check if files exist
1909 if (!rom_filename.empty() &&
1910 !std::filesystem::exists(GetAbsolutePath(rom_filename))) {
1911 errors.push_back("ROM file does not exist: " + rom_filename);
1912 }
1913
1914 if (!code_folder.empty() &&
1915 !std::filesystem::exists(GetAbsolutePath(code_folder))) {
1916 errors.push_back("Code folder does not exist: " + code_folder);
1917 }
1918
1919 if (!labels_filename.empty() &&
1920 !std::filesystem::exists(GetAbsolutePath(labels_filename))) {
1921 errors.push_back("Labels file does not exist: " + labels_filename);
1922 }
1923
1924 if (!hack_manifest_file.empty()) {
1925 if (!std::filesystem::exists(GetAbsolutePath(hack_manifest_file))) {
1926 errors.push_back("Hack manifest file does not exist: " +
1928 } else if (!hack_manifest.loaded()) {
1929 errors.push_back("Hack manifest file failed to load: " +
1931 }
1932 }
1933#endif // __EMSCRIPTEN__
1934
1935 if (!errors.empty()) {
1936 return absl::InvalidArgumentError(absl::StrJoin(errors, "; "));
1937 }
1938
1939 return absl::OkStatus();
1940}
1941
1942std::vector<std::string> YazeProject::GetMissingFiles() const {
1943 std::vector<std::string> missing;
1944
1945#ifndef __EMSCRIPTEN__
1946 if (!rom_filename.empty() &&
1947 !std::filesystem::exists(GetAbsolutePath(rom_filename))) {
1948 missing.push_back(rom_filename);
1949 }
1950 if (!labels_filename.empty() &&
1951 !std::filesystem::exists(GetAbsolutePath(labels_filename))) {
1952 missing.push_back(labels_filename);
1953 }
1954 if (!symbols_filename.empty() &&
1955 !std::filesystem::exists(GetAbsolutePath(symbols_filename))) {
1956 missing.push_back(symbols_filename);
1957 }
1958 if (!hack_manifest_file.empty() &&
1959 !std::filesystem::exists(GetAbsolutePath(hack_manifest_file))) {
1960 missing.push_back(hack_manifest_file);
1961 }
1962#endif // __EMSCRIPTEN__
1963
1964 return missing;
1965}
1966
1968#ifdef __EMSCRIPTEN__
1969 // In the web build, filesystem layout is virtual; nothing to repair eagerly.
1970 return absl::OkStatus();
1971#else
1972 // Create missing directories
1973 std::vector<std::string> folders = {code_folder, assets_folder,
1976
1977 for (const auto& folder : folders) {
1978 if (!folder.empty()) {
1979 std::filesystem::path abs_path = GetAbsolutePath(folder);
1980 if (!std::filesystem::exists(abs_path)) {
1981 std::filesystem::create_directories(abs_path);
1982 }
1983 }
1984 }
1985
1986 // Create missing files with defaults
1987 if (!labels_filename.empty()) {
1988 std::filesystem::path abs_labels = GetAbsolutePath(labels_filename);
1989 if (!std::filesystem::exists(abs_labels)) {
1990 std::ofstream labels_file(abs_labels);
1991 labels_file << "# yaze Resource Labels\n";
1992 labels_file << "# Format: [type] key=value\n\n";
1993 labels_file.close();
1994 }
1995 }
1996
1997 return absl::OkStatus();
1998#endif
1999}
2000
2001std::string YazeProject::GetDisplayName() const {
2002 if (!metadata.description.empty()) {
2003 return metadata.description;
2004 }
2005 return name.empty() ? "Untitled Project" : name;
2006}
2007
2009 const std::string& absolute_path) const {
2010 if (absolute_path.empty() || filepath.empty())
2011 return absolute_path;
2012
2013 std::filesystem::path project_dir =
2014 std::filesystem::path(filepath).parent_path();
2015 std::filesystem::path abs_path(absolute_path);
2016
2017 try {
2018 std::filesystem::path relative =
2019 std::filesystem::relative(abs_path.lexically_normal(), project_dir);
2020 // Persist relative paths in a platform-neutral format for project files.
2021 return relative.generic_string();
2022 } catch (...) {
2023 // Return normalized absolute path if relative conversion fails.
2024 return abs_path.lexically_normal().generic_string();
2025 }
2026}
2027
2029 const std::string& relative_path) const {
2030 if (relative_path.empty() || filepath.empty())
2031 return relative_path;
2032
2033 std::filesystem::path project_dir =
2034 std::filesystem::path(filepath).parent_path();
2035 std::filesystem::path abs_path(relative_path);
2036 if (abs_path.is_absolute()) {
2037 abs_path = abs_path.lexically_normal();
2038 abs_path.make_preferred();
2039 return abs_path.string();
2040 }
2041 abs_path = (project_dir / abs_path).lexically_normal();
2042 abs_path.make_preferred();
2043
2044 return abs_path.string();
2045}
2046
2048#ifdef __EMSCRIPTEN__
2049 // Web builds rely on a virtual filesystem and often use relative paths.
2050 return;
2051#endif
2052 if (filepath.empty()) {
2053 return;
2054 }
2055
2056 auto normalize = [this](std::string* path) {
2057 if (!path || path->empty()) {
2058 return;
2059 }
2060 *path = GetAbsolutePath(*path);
2061 };
2062
2063 normalize(&rom_filename);
2064 normalize(&rom_backup_folder);
2065 normalize(&code_folder);
2066 normalize(&assets_folder);
2067 normalize(&patches_folder);
2068 normalize(&labels_filename);
2069 normalize(&symbols_filename);
2070 normalize(&custom_objects_folder);
2071 normalize(&hack_manifest_file);
2072 normalize(&sprite_catalog_file);
2073 normalize(&sprite_source_root);
2074 normalize(&overworld_graphics_preview);
2075 normalize(&dungeon_tile_patterns);
2076 normalize(&terrain_catalog_file);
2077 normalize(&palette_catalog_file);
2078 for (auto& asset : sprite_assets)
2079 normalize(&asset.zsm_path);
2080 normalize(&output_folder);
2081
2082 for (auto& rom_path : additional_roms) {
2083 if (!rom_path.empty()) {
2084 rom_path = GetAbsolutePath(rom_path);
2085 }
2086 }
2087}
2088
2089std::vector<std::string> YazeProject::AtlasAnnotationPaths() const {
2090 std::vector<std::string> paths;
2091 paths.reserve(atlas.annotations.size());
2092 for (const auto& path : atlas.annotations) {
2093 paths.push_back(GetAbsolutePath(path));
2094 }
2095 return paths;
2096}
2097
2100}
2101
2104}
2105
2107 return name.empty() && rom_filename.empty() && code_folder.empty();
2108}
2109
2111 const std::string& project_path) {
2112 // TODO: Implement ZScream format parsing when format specification is
2113 // available For now, create a basic project that can be manually configured
2114
2115 std::filesystem::path zs_path(project_path);
2116 name = zs_path.stem().string() + "_imported";
2117 zscream_project_file = project_path;
2118
2120
2121 return absl::OkStatus();
2122}
2123
2127
2131
2133 absl::string_view artifact_name) const {
2134 std::filesystem::path base_dir;
2135 if (!output_folder.empty()) {
2136 base_dir = output_folder;
2137 } else if (!z3dk_settings.config_path.empty()) {
2138 base_dir = std::filesystem::path(z3dk_settings.config_path).parent_path();
2139 } else if (!code_folder.empty()) {
2140 base_dir = code_folder;
2141 } else if (!filepath.empty()) {
2142 base_dir = std::filesystem::path(filepath).parent_path();
2143 }
2144
2145 if (base_dir.empty()) {
2146 return std::string(artifact_name);
2147 }
2148 return (base_dir / std::string(artifact_name)).lexically_normal().string();
2149}
2150
2153
2154#if defined(YAZE_WITH_Z3DK) && __has_include("z3dk_core/config.h")
2155 std::vector<std::filesystem::path> candidates;
2156 auto add_candidate = [&candidates](const std::filesystem::path& candidate) {
2157 if (candidate.empty()) {
2158 return;
2159 }
2160 auto normalized = candidate.lexically_normal();
2161 if (std::find(candidates.begin(), candidates.end(), normalized) ==
2162 candidates.end()) {
2163 candidates.push_back(normalized);
2164 }
2165 };
2166
2167 if (!code_folder.empty()) {
2168 std::filesystem::path code_path(code_folder);
2169 if (!std::filesystem::is_directory(code_path)) {
2170 code_path = code_path.parent_path();
2171 }
2172 add_candidate(code_path / "z3dk.toml");
2173 }
2174
2175 if (!hack_manifest_file.empty()) {
2176 add_candidate(std::filesystem::path(hack_manifest_file).parent_path() /
2177 "z3dk.toml");
2178 }
2179
2180 if (!filepath.empty()) {
2181 add_candidate(std::filesystem::path(filepath).parent_path() / "z3dk.toml");
2182 }
2183
2184 for (const auto& candidate : candidates) {
2185 if (!std::filesystem::exists(candidate)) {
2186 continue;
2187 }
2188
2189 std::string error;
2190 z3dk::Config config = z3dk::LoadConfigFile(candidate.string(), &error);
2191 if (!error.empty()) {
2192 LOG_WARN("Project", "Failed to parse z3dk config '%s': %s",
2193 candidate.string().c_str(), error.c_str());
2194 continue;
2195 }
2196
2197 const std::filesystem::path base_dir = candidate.parent_path();
2198 z3dk_settings.loaded = true;
2199 z3dk_settings.config_path = candidate.string();
2200 if (config.preset.has_value()) {
2201 z3dk_settings.preset = *config.preset;
2202 }
2203
2204 z3dk_settings.include_paths.reserve(config.include_paths.size());
2205 for (const auto& include_path : config.include_paths) {
2206 z3dk_settings.include_paths.push_back(
2207 ResolveOptionalPath(base_dir, include_path));
2208 }
2209
2210 z3dk_settings.defines.reserve(config.defines.size());
2211 for (const auto& define : config.defines) {
2212 z3dk_settings.defines.push_back(ParseDefineToken(define));
2213 }
2214
2215 z3dk_settings.main_files.reserve(config.main_files.size());
2216 for (const auto& main_file : config.main_files) {
2217 z3dk_settings.main_files.push_back(
2218 ResolveOptionalPath(base_dir, main_file));
2219 }
2220
2221 if (config.std_includes_path.has_value()) {
2223 ResolveOptionalPath(base_dir, *config.std_includes_path);
2224 }
2225 if (config.std_defines_path.has_value()) {
2227 ResolveOptionalPath(base_dir, *config.std_defines_path);
2228 }
2229 if (config.mapper.has_value()) {
2230 z3dk_settings.mapper = *config.mapper;
2231 }
2232 if (config.rom_size.has_value()) {
2233 z3dk_settings.rom_size = *config.rom_size;
2234 }
2235 if (config.symbols_format.has_value()) {
2236 z3dk_settings.symbols_format = *config.symbols_format;
2237 }
2238 z3dk_settings.lsp_log_enabled = config.lsp_log_enabled;
2239 if (config.lsp_log_path.has_value()) {
2241 ResolveOptionalPath(base_dir, *config.lsp_log_path);
2242 }
2243
2244 z3dk_settings.emits.reserve(config.emits.size());
2245 for (const auto& emit_path : config.emits) {
2246 z3dk_settings.emits.push_back(ResolveOptionalPath(base_dir, emit_path));
2247 }
2248
2249 for (const auto& range : config.prohibited_memory_ranges) {
2251 {.start = range.start, .end = range.end, .reason = range.reason});
2252 }
2253
2255 config.warn_unused_symbols.value_or(true);
2257 config.warn_branch_outside_bank.value_or(true);
2258 z3dk_settings.warn_unknown_width = config.warn_unknown_width.value_or(true);
2259 z3dk_settings.warn_org_collision = config.warn_org_collision.value_or(true);
2261 config.warn_unauthorized_hook.value_or(true);
2262 z3dk_settings.warn_stack_balance = config.warn_stack_balance.value_or(true);
2263 z3dk_settings.warn_hook_return = config.warn_hook_return.value_or(true);
2264
2265 if (config.rom_path.has_value()) {
2266 z3dk_settings.rom_path = ResolveOptionalPath(base_dir, *config.rom_path);
2267 }
2268 if (config.symbols_path.has_value()) {
2270 ResolveOptionalPath(base_dir, *config.symbols_path);
2271 }
2272
2273 for (const auto& emit_path : z3dk_settings.emits) {
2274 const std::string basename = BasenameLower(emit_path);
2275 if (basename.ends_with(".mlb") &&
2278 } else if (basename == "sourcemap.json") {
2280 } else if (basename == "annotations.json") {
2282 } else if (basename == "hooks.json") {
2284 } else if (basename == "lint.json") {
2286 }
2287 }
2288
2290 if (!z3dk_settings.symbols_path.empty() &&
2291 BasenameLower(z3dk_settings.symbols_path).ends_with(".mlb")) {
2293 } else {
2295 GetZ3dkArtifactPath("symbols.mlb");
2296 }
2297 }
2300 GetZ3dkArtifactPath("sourcemap.json");
2301 }
2304 GetZ3dkArtifactPath("annotations.json");
2305 }
2308 GetZ3dkArtifactPath("hooks.json");
2309 }
2312 }
2313
2314 LOG_INFO("Project",
2315 "Loaded z3dk config from %s (%zu include paths, %zu defines)",
2316 z3dk_settings.config_path.c_str(),
2318 return;
2319 }
2320#endif
2321}
2322
2324#ifdef __EMSCRIPTEN__
2327 return; // Hack manifests not supported in web builds
2328#endif
2329
2330 // Clear previous state so we never keep a stale manifest across project loads.
2333
2334 std::filesystem::path loaded_manifest_path;
2335 auto load_manifest = [&](const std::filesystem::path& candidate,
2336 bool update_project_setting) -> bool {
2337 if (candidate.empty() || !std::filesystem::exists(candidate)) {
2338 return false;
2339 }
2340 auto status = hack_manifest.LoadFromFile(candidate.string());
2341 if (!status.ok()) {
2342 LOG_WARN("Project", "Failed to load hack manifest %s: %s",
2343 candidate.string().c_str(),
2344 std::string(status.message()).c_str());
2345 return false;
2346 }
2347 loaded_manifest_path = candidate;
2348 if (update_project_setting) {
2349 hack_manifest_file = GetRelativePath(candidate.string());
2350 }
2351 LOG_DEBUG("Project", "Loaded hack manifest: %s",
2352 candidate.string().c_str());
2354 return true;
2355 };
2356
2357 // Priority 1: An explicit hack_manifest_file setting is authoritative. Do
2358 // not silently replace a missing or malformed configured manifest with an
2359 // auto-discovered file; project validation must surface the bad reference.
2360 const bool has_explicit_manifest = !hack_manifest_file.empty();
2361 if (has_explicit_manifest) {
2362 (void)load_manifest(GetAbsolutePath(hack_manifest_file), false);
2363 }
2364
2365 // Priority 2: Auto-discover hack_manifest.json in code_folder.
2366 if (!has_explicit_manifest && !hack_manifest.loaded() &&
2367 !code_folder.empty()) {
2368 auto code_path = GetAbsolutePath(code_folder);
2369 auto candidate = std::filesystem::path(code_path) / "hack_manifest.json";
2370 (void)load_manifest(candidate, true);
2371 }
2372
2373 // Priority 3: Fallback to the project file directory (or its parent).
2374 if (!has_explicit_manifest && !hack_manifest.loaded() && !filepath.empty()) {
2375 const std::filesystem::path project_dir =
2376 std::filesystem::path(filepath).parent_path();
2377 (void)load_manifest(project_dir / "hack_manifest.json", true);
2378 if (!hack_manifest.loaded() && project_dir.has_parent_path()) {
2379 (void)load_manifest(project_dir.parent_path() / "hack_manifest.json",
2380 true);
2381 }
2382 }
2383
2384 if (hack_manifest.loaded()) {
2386 }
2387
2388 auto try_load_registry = [&](const std::filesystem::path& base) -> bool {
2389 if (base.empty()) {
2390 return false;
2391 }
2392 const auto planning = base / "Docs" / "Dev" / "Planning";
2393 if (!std::filesystem::exists(planning)) {
2394 return false;
2395 }
2396 auto status = hack_manifest.LoadProjectRegistry(base.string());
2397 if (!status.ok()) {
2398 LOG_WARN("Project", "Failed to load project registry from %s: %s",
2399 base.string().c_str(), std::string(status.message()).c_str());
2400 return false;
2401 }
2403 };
2404
2405 bool registry_loaded = false;
2406
2407 // Prefer configured code_folder when valid.
2408 if (!code_folder.empty()) {
2409 registry_loaded =
2410 try_load_registry(std::filesystem::path(GetAbsolutePath(code_folder)));
2411 }
2412
2413 // Fallback to the manifest directory if code_folder is stale/misconfigured.
2414 if (!registry_loaded && !loaded_manifest_path.empty()) {
2415 registry_loaded = try_load_registry(loaded_manifest_path.parent_path());
2416 }
2417
2418 // Last fallback: project directory.
2419 if (!registry_loaded && !filepath.empty()) {
2420 registry_loaded =
2421 try_load_registry(std::filesystem::path(filepath).parent_path());
2422 }
2423
2424 if (!registry_loaded) {
2425 if (!hack_manifest.loaded()) {
2426 return;
2427 }
2428 LOG_WARN("Project",
2429 "Hack manifest loaded but project registry was not found "
2430 "(code_folder='%s', manifest='%s')",
2431 code_folder.c_str(), loaded_manifest_path.string().c_str());
2432 return;
2433 }
2434
2435 // Inject all Oracle resource labels into project resource_labels.
2436 size_t injected = 0;
2437 for (const auto& [type_key, labels] :
2439 for (const auto& [id_str, label] : labels) {
2440 resource_labels[type_key][id_str] = label;
2441 ++injected;
2442 }
2443 }
2444 LOG_DEBUG("Project", "Loaded project registry: %zu resource labels injected",
2445 injected);
2446}
2447
2449 sprite_catalog_file.clear();
2450 sprite_source_root.clear();
2451 sprite_assets.clear();
2452 terrain_catalog_file.clear();
2453 palette_catalog_file.clear();
2454 dungeon_tile_patterns.clear();
2455 if (metadata.project_id.empty()) {
2457 }
2458
2459 // Initialize default feature flags
2475 // REMOVED: kLogInstructions (deprecated)
2476
2477 // Initialize default workspace settings
2480 workspace_settings.ui_theme = "default";
2482 workspace_settings.autosave_interval_secs = 300.0f; // 5 minutes
2489
2490 // Initialize default dungeon overlay settings (minecart tracks)
2492 for (uint16_t tile = 0xB0; tile <= 0xBE; ++tile) {
2493 dungeon_overlay.track_tiles.push_back(tile);
2494 }
2495 dungeon_overlay.track_stop_tiles = {0xB7, 0xB8, 0xB9, 0xBA};
2496 dungeon_overlay.track_switch_tiles = {0xD0, 0xD1, 0xD2, 0xD3};
2499
2503
2504 // Initialize default build configurations
2505 build_configurations = {"Debug", "Release", "Distribution"};
2506 build_target.clear();
2507 asm_entry_point = "asm/main.asm";
2508 asm_sources = {"asm"};
2509 last_build_hash.clear();
2510 build_number = 0;
2511
2512 track_changes = true;
2513
2517
2518 if (metadata.created_by.empty()) {
2519 metadata.created_by = "YAZE";
2520 }
2521}
2522
2524 auto now = std::chrono::system_clock::now().time_since_epoch();
2525 auto timestamp =
2526 std::chrono::duration_cast<std::chrono::milliseconds>(now).count();
2527 return absl::StrFormat("yaze_project_%lld", timestamp);
2528}
2529
2530// ProjectManager Implementation
2531std::vector<ProjectManager::ProjectTemplate>
2533 std::vector<ProjectTemplate> templates;
2534
2535 // ==========================================================================
2536 // ZSCustomOverworld Templates (Recommended)
2537 // ==========================================================================
2538
2539 // Vanilla ROM Hack - no ZSO
2540 {
2542 t.name = "Vanilla ROM Hack";
2543 t.description =
2544 "Standard ROM editing without custom ASM. Limited to vanilla features.";
2548 false;
2553 templates.push_back(t);
2554 }
2555
2556 // ZSCustomOverworld v2 - Basic expansion
2557 {
2559 t.name = "ZSCustomOverworld v2";
2560 t.description =
2561 "Basic overworld expansion: custom BG colors, main palettes, parent "
2562 "system.";
2563 t.icon = ICON_MD_MAP;
2566 true;
2573 t.template_project.metadata.tags = {"zso_v2", "overworld", "expansion"};
2574 templates.push_back(t);
2575 }
2576
2577 // ZSCustomOverworld v3 - Full features (Recommended)
2578 {
2580 t.name = "ZSCustomOverworld v3 (Recommended)";
2581 t.description =
2582 "Full overworld expansion: wide/tall areas, animated GFX, overlays, "
2583 "all features.";
2587 true;
2597 t.template_project.metadata.tags = {"zso_v3", "overworld", "full",
2598 "recommended"};
2599 templates.push_back(t);
2600 }
2601
2602 // Randomizer Compatible
2603 {
2605 t.name = "Randomizer Compatible";
2606 t.description =
2607 "Compatible with ALttP Randomizer. Minimal custom features to avoid "
2608 "conflicts.";
2612 false;
2615 t.template_project.metadata.tags = {"randomizer", "compatible", "minimal"};
2616 templates.push_back(t);
2617 }
2618
2619 // ==========================================================================
2620 // Editor-Focused Templates
2621 // ==========================================================================
2622
2623 // Dungeon Designer
2624 {
2626 t.name = "Dungeon Designer";
2627 t.description = "Focused on dungeon creation and modification.";
2628 t.icon = ICON_MD_DOMAIN;
2632 "dungeon_default";
2633 t.template_project.metadata.tags = {"dungeons", "rooms", "design"};
2634 templates.push_back(t);
2635 }
2636
2637 // Graphics Pack
2638 {
2640 t.name = "Graphics Pack";
2641 t.description =
2642 "Project focused on graphics, sprites, and visual modifications.";
2649 "graphics_default";
2650 t.template_project.metadata.tags = {"graphics", "sprites", "palettes"};
2651 templates.push_back(t);
2652 }
2653
2654 // Complete Overhaul
2655 {
2657 t.name = "Complete Overhaul";
2658 t.description = "Full-scale ROM hack with all features enabled.";
2659 t.icon = ICON_MD_BUILD;
2662 true;
2672 t.template_project.metadata.tags = {"complete", "overhaul", "full-mod"};
2673 templates.push_back(t);
2674 }
2675
2676 return templates;
2677}
2678
2679absl::StatusOr<YazeProject> ProjectManager::CreateFromTemplate(
2680 const std::string& template_name, const std::string& project_name,
2681 const std::string& base_path) {
2682 YazeProject project;
2683 auto status = project.Create(project_name, base_path);
2684 if (!status.ok()) {
2685 return status;
2686 }
2687
2688 // Customize based on template
2689 if (template_name == "Full Overworld Mod") {
2692 project.metadata.description = "Overworld modification project";
2693 project.metadata.tags = {"overworld", "maps", "graphics"};
2694 } else if (template_name == "Dungeon Designer") {
2695 project.feature_flags.kSaveDungeonMaps = true;
2696 project.workspace_settings.show_grid = true;
2697 project.metadata.description = "Dungeon design and modification project";
2698 project.metadata.tags = {"dungeons", "rooms", "design"};
2699 } else if (template_name == "Graphics Pack") {
2700 project.feature_flags.kSaveGraphicsSheet = true;
2701 project.workspace_settings.show_grid = true;
2702 project.metadata.description = "Graphics and sprite modification project";
2703 project.metadata.tags = {"graphics", "sprites", "palettes"};
2704 } else if (template_name == "Complete Overhaul") {
2707 project.feature_flags.kSaveDungeonMaps = true;
2708 project.feature_flags.kSaveGraphicsSheet = true;
2709 project.metadata.description = "Complete ROM overhaul project";
2710 project.metadata.tags = {"complete", "overhaul", "full-mod"};
2711 }
2712
2713 status = project.Save();
2714 if (!status.ok()) {
2715 return status;
2716 }
2717
2718 return project;
2719}
2720
2722 const std::string& directory) {
2723#ifdef __EMSCRIPTEN__
2724 (void)directory;
2725 return {};
2726#else
2727 std::vector<std::string> projects;
2728
2729 try {
2730 for (const auto& entry : std::filesystem::directory_iterator(directory)) {
2731 if (entry.is_regular_file()) {
2732 std::string filename = entry.path().filename().string();
2733 if (filename.ends_with(".yaze") || filename.ends_with(".zsproj")) {
2734 projects.push_back(entry.path().string());
2735 }
2736 } else if (entry.is_directory()) {
2737 std::string filename = entry.path().filename().string();
2738 if (filename.ends_with(".yazeproj")) {
2739 projects.push_back(entry.path().string());
2740 }
2741 }
2742 }
2743 } catch (const std::filesystem::filesystem_error& e) {
2744 // Directory doesn't exist or can't be accessed
2745 }
2746
2747 return projects;
2748#endif // __EMSCRIPTEN__
2749}
2750
2751absl::Status ProjectManager::BackupProject(const YazeProject& project) {
2752#ifdef __EMSCRIPTEN__
2753 (void)project;
2754 return absl::UnimplementedError(
2755 "Project backups are not supported in the web build");
2756#else
2757 if (project.filepath.empty()) {
2758 return absl::InvalidArgumentError("Project has no file path");
2759 }
2760
2761 std::filesystem::path project_path(project.filepath);
2762 std::filesystem::path backup_dir = project_path.parent_path() / "backups";
2763 std::filesystem::create_directories(backup_dir);
2764
2765 auto now = std::chrono::system_clock::now();
2766 auto time_t = std::chrono::system_clock::to_time_t(now);
2767 std::stringstream ss;
2768 ss << std::put_time(std::localtime(&time_t), "%Y%m%d_%H%M%S");
2769
2770 std::string backup_filename = project.name + "_backup_" + ss.str() + ".yaze";
2771 std::filesystem::path backup_path = backup_dir / backup_filename;
2772
2773 try {
2774 std::filesystem::copy_file(project.filepath, backup_path);
2775 } catch (const std::filesystem::filesystem_error& e) {
2776 return absl::InternalError(
2777 absl::StrFormat("Failed to backup project: %s", e.what()));
2778 }
2779
2780 return absl::OkStatus();
2781#endif
2782}
2783
2785 const YazeProject& project) {
2786 return project.Validate();
2787}
2788
2790 const YazeProject& project) {
2791 std::vector<std::string> recommendations;
2792
2793 if (project.rom_filename.empty()) {
2794 recommendations.push_back("Add a ROM file to begin editing");
2795 }
2796
2797 if (project.code_folder.empty()) {
2798 recommendations.push_back("Set up a code folder for assembly patches");
2799 }
2800
2801 if (project.labels_filename.empty()) {
2802 recommendations.push_back("Create a labels file for better organization");
2803 }
2804
2805 if (project.metadata.description.empty()) {
2806 recommendations.push_back("Add a project description for documentation");
2807 }
2808
2809 if (project.git_repository.empty() && project.track_changes) {
2810 recommendations.push_back(
2811 "Consider setting up version control for your project");
2812 }
2813
2814 auto missing_files = project.GetMissingFiles();
2815 if (!missing_files.empty()) {
2816 recommendations.push_back(
2817 "Some project files are missing - use Project > Repair to fix");
2818 }
2819
2820 return recommendations;
2821}
2822
2823// Compatibility implementations for ResourceLabelManager and related classes
2824bool ResourceLabelManager::LoadLabels(const std::string& filename) {
2825 filename_ = filename;
2826 std::ifstream file(filename);
2827 if (!file.is_open()) {
2828 labels_loaded_ = false;
2829 return false;
2830 }
2831
2832 labels_.clear();
2833 std::string line;
2834 std::string current_type = "";
2835
2836 while (std::getline(file, line)) {
2837 if (line.empty() || line[0] == '#')
2838 continue;
2839
2840 // Check for type headers [type_name]
2841 if (line[0] == '[' && line.back() == ']') {
2842 current_type = line.substr(1, line.length() - 2);
2843 continue;
2844 }
2845
2846 // Parse key=value pairs
2847 size_t eq_pos = line.find('=');
2848 if (eq_pos != std::string::npos && !current_type.empty()) {
2849 std::string key = line.substr(0, eq_pos);
2850 std::string value = line.substr(eq_pos + 1);
2851 labels_[current_type][key] = value;
2852 }
2853 }
2854
2855 file.close();
2856 MigrateGfxGroupLabelKeys(labels_);
2857 labels_loaded_ = true;
2858 return true;
2859}
2860
2862 if (filename_.empty())
2863 return false;
2864
2865 std::ofstream file(filename_);
2866 if (!file.is_open())
2867 return false;
2868
2869 file << "# yaze Resource Labels\n";
2870 file << "# Format: [type] followed by key=value pairs\n\n";
2871
2872 for (const auto& [type, type_labels] : labels_) {
2873 if (!type_labels.empty()) {
2874 file << "[" << type << "]\n";
2875 for (const auto& [key, value] : type_labels) {
2876 file << key << "=" << value << "\n";
2877 }
2878 file << "\n";
2879 }
2880 }
2881
2882 file.close();
2883 return true;
2884}
2885
2887 if (!p_open || !*p_open)
2888 return;
2889
2890 // Basic implementation - can be enhanced later
2891 if (ImGui::Begin("Resource Labels", p_open)) {
2892 ImGui::Text("Resource Labels Manager");
2893 ImGui::Text("Labels loaded: %s", labels_loaded_ ? "Yes" : "No");
2894 ImGui::Text("Total types: %zu", labels_.size());
2895
2896 for (const auto& [type, type_labels] : labels_) {
2897 if (ImGui::TreeNode(type.c_str())) {
2898 ImGui::Text("Labels: %zu", type_labels.size());
2899 for (const auto& [key, value] : type_labels) {
2900 ImGui::Text("%s = %s", key.c_str(), value.c_str());
2901 }
2902 ImGui::TreePop();
2903 }
2904 }
2905 }
2906 ImGui::End();
2907}
2908
2909void ResourceLabelManager::EditLabel(const std::string& type,
2910 const std::string& key,
2911 const std::string& newValue) {
2912 labels_[type][key] = newValue;
2913}
2914
2916 bool selected, const std::string& type, const std::string& key,
2917 const std::string& defaultValue) {
2918 const auto custom_label = GetLabel(type, key);
2919 const auto& label = custom_label.empty() ? defaultValue : custom_label;
2920 if (ImGui::Selectable(
2921 absl::StrFormat("%s: %s", key.c_str(), label.c_str()).c_str(),
2922 selected)) {
2923 // Handle selection
2924 }
2925}
2926
2927std::string ResourceLabelManager::GetLabel(const std::string& type,
2928 const std::string& key) {
2929 auto type_it = labels_.find(type);
2930 if (type_it == labels_.end())
2931 return "";
2932
2933 auto label_it = type_it->second.find(key);
2934 if (label_it == type_it->second.end())
2935 return "";
2936
2937 return label_it->second;
2938}
2939
2941 const std::string& type, const std::string& key,
2942 const std::string& defaultValue) {
2943 auto existing = GetLabel(type, key);
2944 if (!existing.empty())
2945 return existing;
2946
2947 labels_[type][key] = defaultValue;
2948 return defaultValue;
2949}
2950
2951// ============================================================================
2952// Embedded Labels Support
2953// ============================================================================
2954
2956 const std::unordered_map<
2957 std::string, std::unordered_map<std::string, std::string>>& labels) {
2958 try {
2959 // Load all default Zelda3 resource names into resource_labels
2960 // We merge them with existing labels, prioritizing existing overrides?
2961 // Or just overwrite? The previous code was:
2962 // resource_labels = zelda3::Zelda3Labels::ToResourceLabels();
2963 // which implies overwriting. But we want to keep overrides if possible.
2964 // However, this is usually called on load.
2965
2966 // Let's overwrite for now to match previous behavior, assuming overrides
2967 // are loaded afterwards or this is initial setup.
2968 // Actually, if we load project then init embedded labels, we might lose overrides.
2969 // But typically overrides are loaded from the project file *into* resource_labels.
2970 // If we call this, we might clobber them.
2971 // The previous implementation clobbered resource_labels.
2972
2973 // However, if we want to support overrides + embedded, we should merge.
2974 // But `resource_labels` was treated as "overrides" in the old code?
2975 // No, `resource_labels` was the container for loaded labels.
2976
2977 // If I look at `LoadFromYazeFormat`:
2978 // It parses `[labels_type]` into `resource_labels`.
2979
2980 // If `use_embedded_labels` is true, `InitializeEmbeddedLabels` is called?
2981 // I need to check when `InitializeEmbeddedLabels` is called.
2982
2983 resource_labels = labels;
2984 use_embedded_labels = true;
2985
2986 LOG_DEBUG("Project", "Initialized embedded labels:");
2987 LOG_DEBUG("Project", " - %d room names", resource_labels["room"].size());
2988 LOG_DEBUG("Project", " - %d entrance names",
2989 resource_labels["entrance"].size());
2990 LOG_DEBUG("Project", " - %d sprite names",
2991 resource_labels["sprite"].size());
2992 LOG_DEBUG("Project", " - %d overlord names",
2993 resource_labels["overlord"].size());
2994 LOG_DEBUG("Project", " - %d item names", resource_labels["item"].size());
2995 LOG_DEBUG("Project", " - %d music names",
2996 resource_labels["music"].size());
2997 LOG_DEBUG("Project", " - %d graphics names",
2998 resource_labels["graphics"].size());
2999 LOG_DEBUG("Project", " - %d room effect names",
3000 resource_labels["room_effect"].size());
3001 LOG_DEBUG("Project", " - %d room tag names",
3002 resource_labels["room_tag"].size());
3003 LOG_DEBUG("Project", " - %d tile type names",
3004 resource_labels["tile_type"].size());
3005
3006 return absl::OkStatus();
3007 } catch (const std::exception& e) {
3008 return absl::InternalError(
3009 absl::StrCat("Failed to initialize embedded labels: ", e.what()));
3010 }
3011}
3012
3013std::string YazeProject::GetLabel(const std::string& resource_type, int id,
3014 const std::string& default_value) const {
3015 // First check if we have a custom label override
3016 auto type_it = resource_labels.find(resource_type);
3017 if (type_it != resource_labels.end()) {
3018 auto label_it = type_it->second.find(std::to_string(id));
3019 if (label_it != type_it->second.end()) {
3020 return label_it->second;
3021 }
3022 }
3023
3024 return default_value.empty() ? resource_type + "_" + std::to_string(id)
3025 : default_value;
3026}
3027
3028absl::Status YazeProject::ImportLabelsFromZScream(const std::string& filepath) {
3029#ifdef __EMSCRIPTEN__
3030 (void)filepath;
3031 return absl::UnimplementedError(
3032 "File-based label import is not supported in the web build");
3033#else
3034 std::ifstream file(filepath);
3035 if (!file.is_open()) {
3036 return absl::InvalidArgumentError(
3037 absl::StrFormat("Cannot open labels file: %s", filepath));
3038 }
3039
3040 std::stringstream buffer;
3041 buffer << file.rdbuf();
3042 file.close();
3043
3044 return ImportLabelsFromZScreamContent(buffer.str());
3045#endif
3046}
3047
3049 const std::string& content) {
3050 // Initialize the global provider with our labels
3051 auto& provider = zelda3::GetResourceLabels();
3052 provider.SetProjectLabels(&resource_labels);
3053 provider.SetPreferHMagicNames(workspace_settings.prefer_hmagic_names);
3054
3055 // Use the provider to parse ZScream format
3056 auto status = provider.ImportFromZScreamFormat(content);
3057 if (!status.ok()) {
3058 return status;
3059 }
3060
3061 LOG_DEBUG("Project", "Imported ZScream labels:");
3062 LOG_DEBUG("Project", " - %d sprite labels",
3063 resource_labels["sprite"].size());
3064 LOG_DEBUG("Project", " - %d room labels", resource_labels["room"].size());
3065 LOG_DEBUG("Project", " - %d item labels", resource_labels["item"].size());
3066 LOG_DEBUG("Project", " - %d room tag labels",
3067 resource_labels["room_tag"].size());
3068
3069 return absl::OkStatus();
3070}
3071
3073 auto& provider = zelda3::GetResourceLabels();
3074 provider.SetProjectLabels(&resource_labels);
3075 provider.SetPreferHMagicNames(workspace_settings.prefer_hmagic_names);
3076 provider.SetHackManifest(hack_manifest.loaded() ? &hack_manifest : nullptr);
3077
3078 LOG_DEBUG("Project", "Initialized ResourceLabelProvider with project labels");
3079 LOG_DEBUG("Project", " - prefer_hmagic_names: %s",
3080 workspace_settings.prefer_hmagic_names ? "true" : "false");
3081 LOG_DEBUG("Project", " - hack_manifest: %s",
3082 hack_manifest.loaded() ? "loaded" : "not loaded");
3083}
3084
3085// ============================================================================
3086// JSON Format Support (Optional)
3087// ============================================================================
3088
3089#ifdef YAZE_ENABLE_JSON_PROJECT_FORMAT
3090
3091absl::Status YazeProject::LoadFromJsonFormat(const std::string& project_path) {
3092 terrain_catalog_file.clear();
3093 palette_catalog_file.clear();
3094 dungeon_tile_patterns.clear();
3095#ifdef __EMSCRIPTEN__
3096 return absl::UnimplementedError(
3097 "JSON project format loading is not supported in the web build");
3098#endif
3099 std::ifstream file(project_path);
3100 if (!file.is_open()) {
3101 return absl::InvalidArgumentError(
3102 absl::StrFormat("Cannot open JSON project file: %s", project_path));
3103 }
3104
3105 try {
3106 json j;
3107 file >> j;
3108
3109 sprite_catalog_file.clear();
3110 sprite_source_root.clear();
3111 sprite_assets.clear();
3112
3113 // Parse project metadata
3114 if (j.contains("yaze_project")) {
3115 auto& proj = j["yaze_project"];
3116
3117 if (proj.contains("name"))
3118 name = proj["name"].get<std::string>();
3119 if (proj.contains("description"))
3120 metadata.description = proj["description"].get<std::string>();
3121 if (proj.contains("author"))
3122 metadata.author = proj["author"].get<std::string>();
3123 if (proj.contains("version"))
3124 metadata.version = proj["version"].get<std::string>();
3125 if (proj.contains("created"))
3126 metadata.created_date = proj["created"].get<std::string>();
3127 if (proj.contains("modified"))
3128 metadata.last_modified = proj["modified"].get<std::string>();
3129 if (proj.contains("created_by"))
3130 metadata.created_by = proj["created_by"].get<std::string>();
3131
3132 // Files
3133 if (proj.contains("rom_filename"))
3134 rom_filename = proj["rom_filename"].get<std::string>();
3135 if (proj.contains("rom_backup_folder"))
3136 rom_backup_folder = proj["rom_backup_folder"].get<std::string>();
3137 if (proj.contains("code_folder"))
3138 code_folder = proj["code_folder"].get<std::string>();
3139 if (proj.contains("assets_folder"))
3140 assets_folder = proj["assets_folder"].get<std::string>();
3141 if (proj.contains("patches_folder"))
3142 patches_folder = proj["patches_folder"].get<std::string>();
3143 if (proj.contains("labels_filename"))
3144 labels_filename = proj["labels_filename"].get<std::string>();
3145 if (proj.contains("symbols_filename"))
3146 symbols_filename = proj["symbols_filename"].get<std::string>();
3147 if (proj.contains("dungeon_tile_patterns"))
3149 proj["dungeon_tile_patterns"].get<std::string>();
3150 if (proj.contains("palette_catalog_file"))
3151 palette_catalog_file = proj["palette_catalog_file"].get<std::string>();
3152 if (proj.contains("terrain_catalog_file"))
3153 terrain_catalog_file = proj["terrain_catalog_file"].get<std::string>();
3154 if (proj.contains("hack_manifest_file"))
3155 hack_manifest_file = proj["hack_manifest_file"].get<std::string>();
3156 if (proj.contains("sprite_catalog_file"))
3157 sprite_catalog_file = proj["sprite_catalog_file"].get<std::string>();
3158 if (proj.contains("sprite_source_root"))
3159 sprite_source_root = proj["sprite_source_root"].get<std::string>();
3160 if (proj.contains("sprite_assets")) {
3161 if (!proj["sprite_assets"].is_array() ||
3162 proj["sprite_assets"].size() > 4096)
3163 return absl::InvalidArgumentError("Invalid sprite_assets array");
3164 for (const auto& value : proj["sprite_assets"]) {
3165 auto asset = SpriteAssetFromJson(value);
3166 if (!asset.ok())
3167 return asset.status();
3168 for (const auto& prior : sprite_assets)
3169 if (prior.zsm_path == asset->zsm_path)
3170 return absl::InvalidArgumentError("Duplicate sprite asset path");
3171 sprite_assets.push_back(std::move(*asset));
3172 }
3173 }
3174
3175 if (proj.contains("rom") && proj["rom"].is_object()) {
3176 auto& rom = proj["rom"];
3177 if (rom.contains("role"))
3178 rom_metadata.role = ParseRomRole(rom["role"].get<std::string>());
3179 if (rom.contains("expected_hash"))
3180 rom_metadata.expected_hash = rom["expected_hash"].get<std::string>();
3181 if (rom.contains("write_policy"))
3183 ParseRomWritePolicy(rom["write_policy"].get<std::string>());
3184 }
3185
3186 // Embedded labels flag
3187 if (proj.contains("use_embedded_labels")) {
3188 use_embedded_labels = proj["use_embedded_labels"].get<bool>();
3189 }
3190
3191 // Feature flags
3192 if (proj.contains("feature_flags")) {
3193 auto& flags = proj["feature_flags"];
3194 // REMOVED: kLogInstructions (deprecated - DisassemblyViewer always
3195 // active)
3196 if (flags.contains("kSaveDungeonMaps"))
3198 flags["kSaveDungeonMaps"].get<bool>();
3199 if (flags.contains("kSaveOverworldMaps"))
3201 flags["kSaveOverworldMaps"].get<bool>();
3202 if (flags.contains("kSaveOverworldEntrances"))
3204 flags["kSaveOverworldEntrances"].get<bool>();
3205 if (flags.contains("kSaveOverworldExits"))
3207 flags["kSaveOverworldExits"].get<bool>();
3208 if (flags.contains("kSaveOverworldItems"))
3210 flags["kSaveOverworldItems"].get<bool>();
3211 if (flags.contains("kSaveOverworldProperties"))
3213 flags["kSaveOverworldProperties"].get<bool>();
3214 if (flags.contains("kSaveDungeonObjects"))
3216 flags["kSaveDungeonObjects"].get<bool>();
3217 if (flags.contains("kSaveDungeonSprites"))
3219 flags["kSaveDungeonSprites"].get<bool>();
3220 if (flags.contains("kSaveDungeonRoomHeaders"))
3222 flags["kSaveDungeonRoomHeaders"].get<bool>();
3223 if (flags.contains("kSaveDungeonTorches"))
3225 flags["kSaveDungeonTorches"].get<bool>();
3226 if (flags.contains("kSaveDungeonPits"))
3228 flags["kSaveDungeonPits"].get<bool>();
3229 if (flags.contains("kSaveDungeonBlocks"))
3231 flags["kSaveDungeonBlocks"].get<bool>();
3232 if (flags.contains("kSaveDungeonCollision"))
3234 flags["kSaveDungeonCollision"].get<bool>();
3235 if (flags.contains("kSaveDungeonWaterFillZones"))
3237 flags["kSaveDungeonWaterFillZones"].get<bool>();
3238 if (flags.contains("kSaveDungeonChests"))
3240 flags["kSaveDungeonChests"].get<bool>();
3241 if (flags.contains("kSaveDungeonPotItems"))
3243 flags["kSaveDungeonPotItems"].get<bool>();
3244 if (flags.contains("kSaveDungeonEntrances"))
3246 flags["kSaveDungeonEntrances"].get<bool>();
3247 if (flags.contains("kSaveDungeonPalettes"))
3249 flags["kSaveDungeonPalettes"].get<bool>();
3250 if (flags.contains("kSaveGraphicsSheet"))
3252 flags["kSaveGraphicsSheet"].get<bool>();
3253 if (flags.contains("kSaveAllPalettes"))
3255 flags["kSaveAllPalettes"].get<bool>();
3256 if (flags.contains("kSaveGfxGroups"))
3257 feature_flags.kSaveGfxGroups = flags["kSaveGfxGroups"].get<bool>();
3258 if (flags.contains("kSaveMessages"))
3259 feature_flags.kSaveMessages = flags["kSaveMessages"].get<bool>();
3260 }
3261
3262 // Workspace settings
3263 if (proj.contains("workspace_settings")) {
3264 auto& ws = proj["workspace_settings"];
3265 if (ws.contains("auto_save_enabled"))
3267 ws["auto_save_enabled"].get<bool>();
3268 if (ws.contains("auto_save_interval"))
3270 ws["auto_save_interval"].get<float>();
3271 if (ws.contains("backup_on_save"))
3272 workspace_settings.backup_on_save = ws["backup_on_save"].get<bool>();
3273 if (ws.contains("backup_retention_count"))
3275 ws["backup_retention_count"].get<int>();
3276 if (ws.contains("backup_keep_daily"))
3278 ws["backup_keep_daily"].get<bool>();
3279 if (ws.contains("backup_keep_daily_days"))
3281 ws["backup_keep_daily_days"].get<int>();
3282 }
3283
3284 if (proj.contains("rom_addresses") && proj["rom_addresses"].is_object()) {
3286 for (auto it = proj["rom_addresses"].begin();
3287 it != proj["rom_addresses"].end(); ++it) {
3288 if (it.value().is_number_unsigned()) {
3290 it.value().get<uint32_t>();
3291 } else if (it.value().is_string()) {
3292 auto parsed = ParseHexUint32(it.value().get<std::string>());
3293 if (parsed.has_value()) {
3294 rom_address_overrides.addresses[it.key()] = *parsed;
3295 }
3296 }
3297 }
3298 }
3299
3300 if (proj.contains("custom_objects") &&
3301 proj["custom_objects"].is_object()) {
3302 custom_object_files.clear();
3303 for (auto it = proj["custom_objects"].begin();
3304 it != proj["custom_objects"].end(); ++it) {
3305 if (!it.value().is_array())
3306 continue;
3307 auto parsed = ParseHexUint32(it.key());
3308 if (!parsed.has_value()) {
3309 continue;
3310 }
3311 std::vector<std::string> files;
3312 for (const auto& entry : it.value()) {
3313 if (entry.is_string()) {
3314 files.push_back(entry.get<std::string>());
3315 }
3316 }
3317 if (!files.empty()) {
3318 custom_object_files[static_cast<int>(*parsed)] = std::move(files);
3319 }
3320 }
3321 }
3322
3323 if (proj.contains("agent_settings") &&
3324 proj["agent_settings"].is_object()) {
3325 auto& agent = proj["agent_settings"];
3327 agent.value("ai_provider", agent_settings.ai_provider);
3329 agent.value("ai_model", agent_settings.ai_model);
3331 agent.value("ollama_host", agent_settings.ollama_host);
3333 agent.value("gemini_api_key", agent_settings.gemini_api_key);
3335 agent.value("use_custom_prompt", agent_settings.use_custom_prompt);
3337 "custom_system_prompt", agent_settings.custom_system_prompt);
3339 agent.value("show_reasoning", agent_settings.show_reasoning);
3340 agent_settings.verbose = agent.value("verbose", agent_settings.verbose);
3341 agent_settings.max_tool_iterations = agent.value(
3342 "max_tool_iterations", agent_settings.max_tool_iterations);
3343 agent_settings.max_retry_attempts = agent.value(
3344 "max_retry_attempts", agent_settings.max_retry_attempts);
3346 agent.value("temperature", agent_settings.temperature);
3347 agent_settings.top_p = agent.value("top_p", agent_settings.top_p);
3349 agent.value("max_output_tokens", agent_settings.max_output_tokens);
3351 agent.value("stream_responses", agent_settings.stream_responses);
3352 if (agent.contains("favorite_models") &&
3353 agent["favorite_models"].is_array()) {
3355 for (const auto& model : agent["favorite_models"]) {
3356 if (model.is_string())
3358 model.get<std::string>());
3359 }
3360 }
3361 if (agent.contains("model_chain") && agent["model_chain"].is_array()) {
3363 for (const auto& model : agent["model_chain"]) {
3364 if (model.is_string())
3365 agent_settings.model_chain.push_back(model.get<std::string>());
3366 }
3367 }
3369 agent.value("chain_mode", agent_settings.chain_mode);
3371 "enable_tool_resources", agent_settings.enable_tool_resources);
3372 agent_settings.enable_tool_dungeon = agent.value(
3373 "enable_tool_dungeon", agent_settings.enable_tool_dungeon);
3375 "enable_tool_overworld", agent_settings.enable_tool_overworld);
3377 "enable_tool_messages", agent_settings.enable_tool_messages);
3379 "enable_tool_dialogue", agent_settings.enable_tool_dialogue);
3381 agent.value("enable_tool_gui", agent_settings.enable_tool_gui);
3383 agent.value("enable_tool_music", agent_settings.enable_tool_music);
3384 agent_settings.enable_tool_sprite = agent.value(
3385 "enable_tool_sprite", agent_settings.enable_tool_sprite);
3387 "enable_tool_emulator", agent_settings.enable_tool_emulator);
3389 agent.value("enable_tool_memory_inspector",
3392 "builder_blueprint_path", agent_settings.builder_blueprint_path);
3393 }
3394
3395 // Build settings
3396 if (proj.contains("build_script"))
3397 build_script = proj["build_script"].get<std::string>();
3398 if (proj.contains("output_folder"))
3399 output_folder = proj["output_folder"].get<std::string>();
3400 if (proj.contains("git_repository"))
3401 git_repository = proj["git_repository"].get<std::string>();
3402 if (proj.contains("track_changes"))
3403 track_changes = proj["track_changes"].get<bool>();
3404 }
3405
3406 return absl::OkStatus();
3407 } catch (const json::exception& e) {
3408 return absl::InvalidArgumentError(
3409 absl::StrFormat("JSON parse error: %s", e.what()));
3410 }
3411}
3412
3413absl::Status YazeProject::SaveToJsonFormat() {
3414#ifdef __EMSCRIPTEN__
3415 return absl::UnimplementedError(
3416 "JSON project format saving is not supported in the web build");
3417#endif
3418 json j;
3419 auto& proj = j["yaze_project"];
3420
3421 // Metadata
3422 proj["version"] = metadata.version;
3423 proj["name"] = name;
3424 proj["author"] = metadata.author;
3425 proj["created_by"] = metadata.created_by;
3426 proj["description"] = metadata.description;
3427 proj["created"] = metadata.created_date;
3428 proj["modified"] = metadata.last_modified;
3429
3430 // Files
3431 proj["rom_filename"] = rom_filename;
3432 proj["rom_backup_folder"] = rom_backup_folder;
3433 proj["code_folder"] = code_folder;
3434 proj["assets_folder"] = assets_folder;
3435 proj["patches_folder"] = patches_folder;
3436 proj["labels_filename"] = labels_filename;
3437 proj["symbols_filename"] = symbols_filename;
3438 proj["dungeon_tile_patterns"] =
3440 proj["palette_catalog_file"] =
3442 proj["terrain_catalog_file"] =
3444 proj["hack_manifest_file"] = hack_manifest_file;
3445 proj["sprite_catalog_file"] =
3447 proj["sprite_source_root"] =
3449 proj["sprite_assets"] = nlohmann::json::array();
3450 for (auto asset : sprite_assets) {
3451 asset.zsm_path = GetRelativePath(GetAbsolutePath(asset.zsm_path));
3452 proj["sprite_assets"].push_back(SpriteAssetToJson(asset));
3453 }
3454 proj["output_folder"] = output_folder;
3455
3456 proj["rom"]["role"] = RomRoleToString(rom_metadata.role);
3457 proj["rom"]["expected_hash"] = rom_metadata.expected_hash;
3458 proj["rom"]["write_policy"] =
3460
3461 // Embedded labels
3462 proj["use_embedded_labels"] = use_embedded_labels;
3463
3464 // Feature flags
3465 // REMOVED: kLogInstructions (deprecated)
3466 proj["feature_flags"]["kSaveDungeonMaps"] = feature_flags.kSaveDungeonMaps;
3467 proj["feature_flags"]["kSaveOverworldMaps"] =
3469 proj["feature_flags"]["kSaveOverworldEntrances"] =
3471 proj["feature_flags"]["kSaveOverworldExits"] =
3473 proj["feature_flags"]["kSaveOverworldItems"] =
3475 proj["feature_flags"]["kSaveOverworldProperties"] =
3477 proj["feature_flags"]["kSaveDungeonObjects"] =
3479 proj["feature_flags"]["kSaveDungeonSprites"] =
3481 proj["feature_flags"]["kSaveDungeonRoomHeaders"] =
3483 proj["feature_flags"]["kSaveDungeonTorches"] =
3485 proj["feature_flags"]["kSaveDungeonPits"] = feature_flags.dungeon.kSavePits;
3486 proj["feature_flags"]["kSaveDungeonBlocks"] =
3488 proj["feature_flags"]["kSaveDungeonCollision"] =
3490 proj["feature_flags"]["kSaveDungeonWaterFillZones"] =
3492 proj["feature_flags"]["kSaveDungeonChests"] =
3494 proj["feature_flags"]["kSaveDungeonPotItems"] =
3496 proj["feature_flags"]["kSaveDungeonEntrances"] =
3498 proj["feature_flags"]["kSaveDungeonPalettes"] =
3500 proj["feature_flags"]["kSaveGraphicsSheet"] =
3502 proj["feature_flags"]["kSaveAllPalettes"] = feature_flags.kSaveAllPalettes;
3503 proj["feature_flags"]["kSaveGfxGroups"] = feature_flags.kSaveGfxGroups;
3504 proj["feature_flags"]["kSaveMessages"] = feature_flags.kSaveMessages;
3505
3506 // Workspace settings
3507 proj["workspace_settings"]["auto_save_enabled"] =
3509 proj["workspace_settings"]["auto_save_interval"] =
3511 proj["workspace_settings"]["backup_on_save"] =
3513 proj["workspace_settings"]["backup_retention_count"] =
3515 proj["workspace_settings"]["backup_keep_daily"] =
3517 proj["workspace_settings"]["backup_keep_daily_days"] =
3519
3520 auto& agent = proj["agent_settings"];
3521 agent["ai_provider"] = agent_settings.ai_provider;
3522 agent["ai_model"] = agent_settings.ai_model;
3523 agent["ollama_host"] = agent_settings.ollama_host;
3524 agent["gemini_api_key"] = agent_settings.gemini_api_key;
3525 agent["use_custom_prompt"] = agent_settings.use_custom_prompt;
3526 agent["custom_system_prompt"] = agent_settings.custom_system_prompt;
3527 agent["show_reasoning"] = agent_settings.show_reasoning;
3528 agent["verbose"] = agent_settings.verbose;
3529 agent["max_tool_iterations"] = agent_settings.max_tool_iterations;
3530 agent["max_retry_attempts"] = agent_settings.max_retry_attempts;
3531 agent["temperature"] = agent_settings.temperature;
3532 agent["top_p"] = agent_settings.top_p;
3533 agent["max_output_tokens"] = agent_settings.max_output_tokens;
3534 agent["stream_responses"] = agent_settings.stream_responses;
3535 agent["favorite_models"] = agent_settings.favorite_models;
3536 agent["model_chain"] = agent_settings.model_chain;
3537 agent["chain_mode"] = agent_settings.chain_mode;
3538 agent["enable_tool_resources"] = agent_settings.enable_tool_resources;
3539 agent["enable_tool_dungeon"] = agent_settings.enable_tool_dungeon;
3540 agent["enable_tool_overworld"] = agent_settings.enable_tool_overworld;
3541 agent["enable_tool_messages"] = agent_settings.enable_tool_messages;
3542 agent["enable_tool_dialogue"] = agent_settings.enable_tool_dialogue;
3543 agent["enable_tool_gui"] = agent_settings.enable_tool_gui;
3544 agent["enable_tool_music"] = agent_settings.enable_tool_music;
3545 agent["enable_tool_sprite"] = agent_settings.enable_tool_sprite;
3546 agent["enable_tool_emulator"] = agent_settings.enable_tool_emulator;
3547 agent["enable_tool_memory_inspector"] =
3549 agent["builder_blueprint_path"] = agent_settings.builder_blueprint_path;
3550
3551 if (!rom_address_overrides.addresses.empty()) {
3552 auto& addrs = proj["rom_addresses"];
3553 for (const auto& [key, value] : rom_address_overrides.addresses) {
3554 addrs[key] = value;
3555 }
3556 }
3557
3558 if (!custom_object_files.empty()) {
3559 auto& objs = proj["custom_objects"];
3560 for (const auto& [object_id, files] : custom_object_files) {
3561 objs[absl::StrFormat("0x%X", object_id)] = files;
3562 }
3563 }
3564
3565 // Build settings
3566 proj["build_script"] = build_script;
3567 proj["git_repository"] = git_repository;
3568 proj["track_changes"] = track_changes;
3569
3570 // Write to file
3571 std::ofstream file(filepath);
3572 if (!file.is_open()) {
3573 return absl::InvalidArgumentError(
3574 absl::StrFormat("Cannot write JSON project file: %s", filepath));
3575 }
3576
3577 file << j.dump(2); // Pretty print with 2-space indent
3578 return absl::OkStatus();
3579}
3580
3581#endif // YAZE_ENABLE_JSON_PROJECT_FORMAT
3582
3583// RecentFilesManager implementation
3585 auto config_dir = util::PlatformPaths::GetConfigDirectory();
3586 if (!config_dir.ok()) {
3587 return ""; // Or handle error appropriately
3588 }
3589 return (*config_dir / kRecentFilesFilename).string();
3590}
3591
3593#ifdef __EMSCRIPTEN__
3594 auto status = platform::WasmStorage::SaveProject(
3595 kRecentFilesFilename, absl::StrJoin(recent_files_, "\n"));
3596 if (!status.ok()) {
3597 LOG_WARN("RecentFilesManager", "Could not persist recent files: %s",
3598 status.ToString().c_str());
3599 }
3600 return;
3601#endif
3602 // Ensure config directory exists
3603 auto config_dir_status = util::PlatformPaths::GetConfigDirectory();
3604 if (!config_dir_status.ok()) {
3605 LOG_ERROR("Project", "Failed to get or create config directory: %s",
3606 config_dir_status.status().ToString().c_str());
3607 return;
3608 }
3609
3610 std::string filepath = GetFilePath();
3611 std::ofstream file(filepath);
3612 if (!file.is_open()) {
3613 LOG_WARN("RecentFilesManager", "Could not save recent files to %s",
3614 filepath.c_str());
3615 return;
3616 }
3617
3618 for (const auto& file_path : recent_files_) {
3619 file << file_path << std::endl;
3620 }
3621}
3622
3624#ifdef __EMSCRIPTEN__
3625 auto storage_or = platform::WasmStorage::LoadProject(kRecentFilesFilename);
3626 if (!storage_or.ok()) {
3627 return;
3628 }
3629 recent_files_.clear();
3630 std::istringstream stream(storage_or.value());
3631 std::string line;
3632 while (std::getline(stream, line)) {
3633 if (!line.empty()) {
3634 recent_files_.push_back(line);
3635 }
3636 }
3638 return;
3639#else
3640 std::string filepath = GetFilePath();
3641 std::ifstream file(filepath);
3642 if (!file.is_open()) {
3643 // File doesn't exist yet, which is fine
3644 return;
3645 }
3646
3647 recent_files_.clear();
3648 std::string line;
3649 while (std::getline(file, line)) {
3650 if (!line.empty()) {
3651 recent_files_.push_back(line);
3652 }
3653 }
3655#endif
3656}
3657
3658} // namespace project
3659} // 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:2721
static absl::Status ValidateProjectStructure(const YazeProject &project)
Definition project.cc:2784
static absl::StatusOr< YazeProject > CreateFromTemplate(const std::string &template_name, const std::string &project_name, const std::string &base_path)
Definition project.cc:2679
static std::vector< std::string > GetRecommendedFixesForProject(const YazeProject &project)
Definition project.cc:2789
static std::vector< ProjectTemplate > GetProjectTemplates()
Definition project.cc:2532
static absl::Status BackupProject(const YazeProject &project)
Definition project.cc:2751
std::string GetFilePath() const
Definition project.cc:3584
std::vector< std::string > recent_files_
Definition project.h:656
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:133
#define LOG_ERROR(category, format,...)
Definition log.h:140
#define LOG_WARN(category, format,...)
Definition log.h:138
#define LOG_INFO(category, format,...)
Definition log.h:136
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
Rom * rom()
Get the current ROM instance.
float ParseFloat(const std::string &value)
Definition project.cc:81
std::string ResolveOptionalPath(const std::filesystem::path &base_dir, const std::string &value)
Definition project.cc:280
void RemoveProjectSaveTempFile(const std::filesystem::path &temp_path)
Definition project.cc:315
bool ContainsLoneCarriageReturn(const std::string &content)
Definition project.cc:766
std::vector< std::string > ParsePositionalStringList(const std::string &value)
Definition project.cc:106
absl::Status WriteProjectFileAtomicallyImpl(const std::filesystem::path &target_path, absl::string_view contents, bool replace_existing)
Definition project.cc:320
std::vector< uint16_t > ParseHexUintList(const std::string &value)
Definition project.cc:122
std::string ToLowerCopy(std::string value)
Definition project.cc:52
constexpr absl::string_view kProjectFileFooter
Definition project.cc:776
bool ParseBool(const std::string &value)
Definition project.cc:77
std::pair< std::string, std::string > ParseDefineToken(const std::string &value)
Definition project.cc:272
std::optional< uint32_t > ParseHexUint32(const std::string &value)
Definition project.cc:150
bool IsKnownProjectSection(absl::string_view name)
Definition project.cc:779
std::string FormatHexUint32(uint32_t value)
Definition project.cc:255
ProjectFilePassthrough::Section & PassthroughSection(ProjectFilePassthrough &passthrough, const std::string &name)
Definition project.cc:820
std::map< uint16_t, std::vector< uint16_t > > ParseHexUintListMap(const std::string &value)
Definition project.cc:229
std::string FormatHexUintListMap(const std::map< uint16_t, std::vector< uint16_t > > &values)
Definition project.cc:247
bool IsGeneratedProjectComment(absl::string_view line, bool before_sections)
Definition project.cc:807
std::string SanitizeStorageKey(absl::string_view input)
Definition project.cc:259
std::filesystem::path MakeProjectSaveTempPath(const std::filesystem::path &target_path)
Definition project.cc:305
std::string MergeProjectFilePassthrough(const std::string &canonical, const ProjectFilePassthrough &passthrough)
Definition project.cc:838
std::string FormatHexUintList(const std::vector< uint16_t > &values)
Definition project.cc:170
void MigrateGfxGroupLabelKeys(std::unordered_map< std::string, std::unordered_map< std::string, std::string > > &labels)
Definition project.cc:182
bool IsKeyLine(absl::string_view line)
Definition project.cc:831
std::string BasenameLower(const std::string &path)
Definition project.cc:292
std::vector< std::string > ParseStringList(const std::string &value)
Definition project.cc:89
std::string RomRoleToString(RomRole role)
Definition project.cc:396
absl::Status WriteProjectFileAtomically(absl::string_view target_path, absl::string_view contents, bool replace_existing)
Definition project.cc:387
RomRole ParseRomRole(absl::string_view value)
Definition project.cc:410
const std::string kRecentFilesFilename
Definition project.h:593
RomWritePolicy ParseRomWritePolicy(absl::string_view value)
Definition project.cc:436
std::string RomWritePolicyToString(RomWritePolicy policy)
Definition project.cc:424
absl::StatusOr< SpriteAssetBinding > ParseSpriteAssetBinding(const std::string &text)
nlohmann::json SpriteAssetToJson(const SpriteAssetBinding &asset)
absl::StatusOr< SpriteAssetBinding > SpriteAssetFromJson(const nlohmann::json &value)
bool IsKnownProjectFileSection(absl::string_view name)
Definition project.cc:991
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
Atlas map views ([atlas]).
Definition project.h:134
std::vector< std::string > annotations
Definition project.h:137
std::vector< uint16_t > track_object_ids
Definition project.h:101
std::vector< uint16_t > minecart_sprite_ids
Definition project.h:102
std::vector< uint16_t > track_stop_tiles
Definition project.h:97
std::vector< uint16_t > track_tiles
Definition project.h:96
std::vector< uint16_t > track_switch_tiles
Definition project.h:98
std::vector< uint16_t > reserved_sheets
Definition project.h:115
std::map< uint16_t, std::vector< uint16_t > > reserved_blocks
Definition project.h:117
std::vector< uint16_t > flagged_sheets
Definition project.h:116
Planning editor sidecar ([planning]).
Definition project.h:167
Lines of a .yaze file that this build does not model.
Definition project.h:193
std::vector< Section > sections
Definition project.h:205
std::vector< std::string > tags
Definition project.h:44
std::string CreateOrGetLabel(const std::string &type, const std::string &key, const std::string &defaultValue)
Definition project.cc:2940
std::string GetLabel(const std::string &type, const std::string &key)
Definition project.cc:2927
void EditLabel(const std::string &type, const std::string &key, const std::string &newValue)
Definition project.cc:2909
bool LoadLabels(const std::string &filename)
Definition project.cc:2824
void SelectableLabelWithNameEdit(bool selected, const std::string &type, const std::string &key, const std::string &defaultValue)
Definition project.cc:2915
std::unordered_map< std::string, std::unordered_map< std::string, std::string > > labels_
Definition project.h:589
std::string expected_hash
Definition project.h:224
RomWritePolicy write_policy
Definition project.h:225
std::map< std::string, std::string > custom_keybindings
Definition project.h:85
std::vector< std::string > saved_layouts
Definition project.h:66
std::map< std::string, bool > editor_visibility
Definition project.h:87
std::vector< std::string > recent_files
Definition project.h:86
std::vector< std::string > favorite_models
Definition project.h:397
std::vector< std::string > model_chain
Definition project.h:398
Modern project structure with comprehensive settings consolidation.
Definition project.h:287
std::string rom_backup_folder
Definition project.h:306
std::unordered_map< int, std::vector< std::string > > custom_object_files
Definition project.h:342
absl::Status ResetToDefaults()
Definition project.cc:1892
std::string custom_objects_folder
Definition project.h:327
absl::Status RepairProject()
Definition project.cc:1967
std::string MakeStorageKey(absl::string_view suffix) const
Definition project.cc:746
static std::string ResolveBundleRoot(const std::string &path)
Definition project.cc:511
struct yaze::project::YazeProject::MusicPersistence music_persistence
std::string sprite_catalog_file
Definition project.h:331
absl::StatusOr< std::string > SerializeToString() const
Definition project.cc:995
std::string zscream_project_file
Definition project.h:414
absl::Status ExportForZScream(const std::string &target_path)
Definition project.cc:1859
ProjectMetadata metadata
Definition project.h:299
absl::Status SaveToYazeFormat(bool replace_existing=true)
Definition project.cc:1803
absl::Status ImportZScreamProject(const std::string &zscream_project_path)
Definition project.cc:1836
absl::Status SaveAllSettings()
Definition project.cc:1887
absl::Status LoadFromString(const std::string &content, const std::string &project_path)
Definition project.cc:703
absl::Status ImportLabelsFromZScreamContent(const std::string &content)
Import labels from ZScream format content directly.
Definition project.cc:3048
std::string git_repository
Definition project.h:374
core::HackManifest hack_manifest
Definition project.h:360
void InitializeResourceLabelProvider()
Initialize the global ResourceLabelProvider with this project's labels.
Definition project.cc:3072
absl::Status ParseFromString(const std::string &content)
Definition project.cc:1365
std::string cutscene_shots
Definition project.h:318
std::vector< std::string > additional_roms
Definition project.h:307
std::string terrain_catalog_file
Definition project.h:336
std::string patches_folder
Definition project.h:313
absl::Status LoadFromYazeFormat(const std::string &project_path)
Definition project.cc:1782
std::unordered_map< std::string, std::unordered_map< std::string, std::string > > resource_labels
Definition project.h:353
std::vector< std::string > AtlasAnnotationPaths() const
Definition project.cc:2089
std::string GenerateProjectId() const
Definition project.cc:2523
absl::Status Create(const std::string &project_name, const std::string &base_path)
Definition project.cc:458
std::string assets_folder
Definition project.h:312
GraphicsSheetSettings graphics_sheets
Definition project.h:348
std::string AtlasSymbolsPath() const
Definition project.cc:2098
absl::Status LoadAllSettings()
Definition project.cc:1881
std::string labels_filename
Definition project.h:314
std::vector< std::string > asm_sources
Definition project.h:371
std::string hack_manifest_file
Definition project.h:329
std::string GetDisplayName() const
Definition project.cc:2001
PlanningSettings planning
Definition project.h:350
std::string PlanningFilePath() const
Definition project.cc:2102
std::vector< std::string > GetMissingFiles() const
Definition project.cc:1942
WorkspaceSettings workspace_settings
Definition project.h:346
std::string GetZ3dkArtifactPath(absl::string_view artifact_name) const
Definition project.cc:2132
std::string output_folder
Definition project.h:367
std::string asm_entry_point
Definition project.h:370
std::string GetRelativePath(const std::string &absolute_path) const
Definition project.cc:2008
std::vector< SpriteAssetBinding > sprite_assets
Definition project.h:333
absl::Status InitializeEmbeddedLabels(const std::unordered_map< std::string, std::unordered_map< std::string, std::string > > &labels)
Definition project.cc:2955
ProjectFilePassthrough file_passthrough
Definition project.h:426
absl::Status SaveAs(const std::string &new_path)
Definition project.cc:734
absl::Status SaveNew()
Definition project.cc:699
struct yaze::project::YazeProject::AgentSettings agent_settings
std::string custom_collision_json
Definition project.h:325
DungeonOverlaySettings dungeon_overlay
Definition project.h:347
std::string palette_catalog_file
Definition project.h:339
absl::Status ImportFromZScreamFormat(const std::string &project_path)
Definition project.cc:2110
std::string GetAbsolutePath(const std::string &relative_path) const
Definition project.cc:2028
std::string overworld_graphics_preview
Definition project.h:320
std::string GetLabel(const std::string &resource_type, int id, const std::string &default_value="") const
Definition project.cc:3013
std::string sprite_source_root
Definition project.h:332
absl::Status Open(const std::string &project_path)
Definition project.cc:537
absl::Status ImportLabelsFromZScream(const std::string &filepath)
Import labels from a ZScream DefaultNames.txt file.
Definition project.cc:3028
std::string last_build_hash
Definition project.h:376
std::map< std::string, std::string > zscream_mappings
Definition project.h:415
std::string dungeon_tile_patterns
Definition project.h:322
absl::Status Validate() const
Definition project.cc:1897
core::FeatureFlags::Flags feature_flags
Definition project.h:345
std::vector< std::string > build_configurations
Definition project.h:368
core::RomAddressOverrides rom_address_overrides
Definition project.h:351
std::string symbols_filename
Definition project.h:315
Z3dkSettings z3dk_settings
Definition project.h:363
std::vector< std::string > include_paths
Definition project.h:246
std::string std_includes_path
Definition project.h:250
std::vector< std::string > main_files
Definition project.h:249
std::vector< std::string > emits
Definition project.h:248
std::vector< std::pair< std::string, std::string > > defines
Definition project.h:247
Z3dkArtifactPaths artifact_paths
Definition project.h:267
std::optional< bool > lsp_log_enabled
Definition project.h:257
std::vector< Z3dkMemoryRange > prohibited_memory_ranges
Definition project.h:256
std::string std_defines_path
Definition project.h:251