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 <cstdio>
12#include <filesystem>
13#include <fstream>
14#include <iomanip>
15#include <iterator>
16#include <sstream>
17#include <stdexcept>
18
19#include "absl/strings/match.h"
20#include "absl/strings/str_format.h"
21#include "absl/strings/str_join.h"
22#include "absl/strings/str_split.h"
23#include "absl/strings/strip.h"
24#include "app/gui/core/icons.h"
28#include "imgui/imgui.h"
29#include "util/file_util.h"
30#include "util/json.h"
31#include "util/log.h"
32#include "util/macro.h"
33#include "util/platform_paths.h"
34#include "yaze_config.h"
36
37#if defined(YAZE_WITH_Z3DK) && __has_include("z3dk_core/config.h")
38#include "z3dk_core/config.h"
39#endif
40
41#ifdef __EMSCRIPTEN__
43#elif defined(_WIN32)
44#include <windows.h>
45#else
46#include <unistd.h>
47#endif
48
49// #ifdef YAZE_ENABLE_JSON_PROJECT_FORMAT
50// #include "nlohmann/json.hpp"
51// using json = nlohmann::json;
52// #endif
53
54namespace yaze {
55namespace project {
56
57namespace {
58std::string ToLowerCopy(std::string value) {
59 std::transform(
60 value.begin(), value.end(), value.begin(),
61 [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
62 return value;
63}
64
65// Helper functions for parsing key-value pairs
66std::pair<std::string, std::string> ParseKeyValue(const std::string& line) {
67 size_t eq_pos = line.find('=');
68 if (eq_pos == std::string::npos)
69 return {"", ""};
70
71 std::string key = line.substr(0, eq_pos);
72 std::string value = line.substr(eq_pos + 1);
73
74 // Trim whitespace
75 key.erase(0, key.find_first_not_of(" \t"));
76 key.erase(key.find_last_not_of(" \t") + 1);
77 value.erase(0, value.find_first_not_of(" \t"));
78 value.erase(value.find_last_not_of(" \t") + 1);
79
80 return {key, value};
81}
82
83bool ParseBool(const std::string& value) {
84 return value == "true" || value == "1" || value == "yes";
85}
86
87float ParseFloat(const std::string& value) {
88 try {
89 return std::stof(value);
90 } catch (...) {
91 return 0.0f;
92 }
93}
94
95std::vector<std::string> ParseStringList(const std::string& value) {
96 std::vector<std::string> result;
97 if (value.empty())
98 return result;
99
100 std::vector<std::string> parts = absl::StrSplit(value, ',');
101 for (const auto& part : parts) {
102 std::string trimmed = std::string(part);
103 trimmed.erase(0, trimmed.find_first_not_of(" \t"));
104 trimmed.erase(trimmed.find_last_not_of(" \t") + 1);
105 if (!trimmed.empty()) {
106 result.push_back(trimmed);
107 }
108 }
109 return result;
110}
111
112std::vector<std::string> ParsePositionalStringList(const std::string& value) {
113 std::vector<std::string> result;
114 if (value.empty())
115 return result;
116
117 std::vector<std::string> parts = absl::StrSplit(value, ',');
118 result.reserve(parts.size());
119 for (const auto& part : parts) {
120 std::string trimmed = part;
121 trimmed.erase(0, trimmed.find_first_not_of(" \t"));
122 trimmed.erase(trimmed.find_last_not_of(" \t") + 1);
123 result.push_back(std::move(trimmed));
124 }
125 return result;
126}
127
128std::vector<uint16_t> ParseHexUintList(const std::string& value) {
129 std::vector<uint16_t> result;
130 if (value.empty()) {
131 return result;
132 }
133
134 auto parts = ParseStringList(value);
135 result.reserve(parts.size());
136 for (const auto& part : parts) {
137 std::string token = part;
138 if (token.rfind("0x", 0) == 0 || token.rfind("0X", 0) == 0) {
139 token = token.substr(2);
140 try {
141 result.push_back(static_cast<uint16_t>(std::stoul(token, nullptr, 16)));
142 } catch (...) {
143 // Ignore malformed entries
144 }
145 } else {
146 try {
147 result.push_back(static_cast<uint16_t>(std::stoul(token, nullptr, 10)));
148 } catch (...) {
149 // Ignore malformed entries
150 }
151 }
152 }
153 return result;
154}
155
156std::optional<uint32_t> ParseHexUint32(const std::string& value) {
157 if (value.empty()) {
158 return std::nullopt;
159 }
160 std::string token = value;
161 if (token.rfind("0x", 0) == 0 || token.rfind("0X", 0) == 0) {
162 token = token.substr(2);
163 try {
164 return static_cast<uint32_t>(std::stoul(token, nullptr, 16));
165 } catch (...) {
166 return std::nullopt;
167 }
168 }
169 try {
170 return static_cast<uint32_t>(std::stoul(token, nullptr, 10));
171 } catch (...) {
172 return std::nullopt;
173 }
174}
175
176std::string FormatHexUintList(const std::vector<uint16_t>& values) {
177 return absl::StrJoin(values, ",", [](std::string* out, uint16_t value) {
178 out->append(absl::StrFormat("0x%02X", value));
179 });
180}
181
182// Before 2026-09-25 the gfx group editor keyed blockset, roomset and
183// spriteset labels as "0x" + decimal digits (spriteset 12 -> "0x12", which the
184// label resolver reads as hex 18); the paletteset editor did the same until
185// 2026-09-26. Canonical keys are decimal. On load:
186// existing decimal keys win; "0x" + only decimal digits is that old format and
187// is read as decimal; other hex keys ("0x0C") are read as hex.
189 std::unordered_map<std::string,
190 std::unordered_map<std::string, std::string>>& labels) {
191 auto all_of = [](absl::string_view text, auto predicate) {
192 return !text.empty() && std::all_of(text.begin(), text.end(), predicate);
193 };
194 auto is_decimal = [](char c) {
195 return c >= '0' && c <= '9';
196 };
197 auto is_hex = [](char c) {
198 return std::isxdigit(static_cast<unsigned char>(c)) != 0;
199 };
200 for (const char* type : {"blockset", "roomset", "spriteset", "paletteset"}) {
201 auto found = labels.find(type);
202 if (found == labels.end()) {
203 continue;
204 }
205 std::unordered_map<std::string, std::string> migrated;
206 // Pass 0: canonical decimal keys. Pass 1: old "0x"+decimal. Pass 2: hex.
207 for (int pass = 0; pass < 3; ++pass) {
208 for (const auto& [key, value] : found->second) {
209 absl::string_view digits = key;
210 const bool prefixed = absl::ConsumePrefix(&digits, "0x") ||
211 absl::ConsumePrefix(&digits, "0X");
212 std::string canonical;
213 if (pass == 0 && !prefixed && all_of(digits, is_decimal)) {
214 canonical = std::string(digits);
215 } else if (pass == 1 && prefixed && all_of(digits, is_decimal)) {
216 canonical =
217 std::to_string(std::stoul(std::string(digits), nullptr, 10));
218 } else if (pass == 2 && !all_of(digits, is_decimal)) {
219 // Real hex keys become decimal; anything else is kept as written.
220 canonical =
221 prefixed && all_of(digits, is_hex)
222 ? std::to_string(std::stoul(std::string(digits), nullptr, 16))
223 : key;
224 }
225 if (!canonical.empty()) {
226 migrated.emplace(canonical, value);
227 }
228 }
229 }
230 found->second = std::move(migrated);
231 }
232}
233
234// "0x55:0,1,2;0x54:3" -> {0x55: {0, 1, 2}, 0x54: {3}}
235std::map<uint16_t, std::vector<uint16_t>> ParseHexUintListMap(
236 const std::string& value) {
237 std::map<uint16_t, std::vector<uint16_t>> result;
238 for (absl::string_view entry : absl::StrSplit(value, ';')) {
239 const size_t colon = entry.find(':');
240 if (colon == absl::string_view::npos) {
241 continue;
242 }
243 const auto keys = ParseHexUintList(std::string(entry.substr(0, colon)));
244 if (keys.size() != 1) {
245 continue;
246 }
247 result[keys.front()] =
248 ParseHexUintList(std::string(entry.substr(colon + 1)));
249 }
250 return result;
251}
252
254 const std::map<uint16_t, std::vector<uint16_t>>& values) {
255 return absl::StrJoin(values, ";", [](std::string* out, const auto& entry) {
256 out->append(absl::StrFormat("0x%02X:", entry.first));
257 out->append(absl::StrJoin(entry.second, ","));
258 });
259}
260
261std::string FormatHexUint32(uint32_t value) {
262 return absl::StrFormat("0x%06X", value);
263}
264
265std::string SanitizeStorageKey(absl::string_view input) {
266 std::string key(input);
267 for (char& c : key) {
268 if (!std::isalnum(static_cast<unsigned char>(c))) {
269 c = '_';
270 }
271 }
272 if (key.empty()) {
273 key = "project";
274 }
275 return key;
276}
277
278std::pair<std::string, std::string> ParseDefineToken(const std::string& value) {
279 auto [key, parsed_value] = ParseKeyValue(value);
280 if (key.empty()) {
281 return {value, "1"};
282 }
283 return {key, parsed_value.empty() ? "1" : parsed_value};
284}
285
286std::string ResolveOptionalPath(const std::filesystem::path& base_dir,
287 const std::string& value) {
288 if (value.empty()) {
289 return {};
290 }
291 std::filesystem::path path(value);
292 if (path.is_absolute()) {
293 return path.lexically_normal().string();
294 }
295 return (base_dir / path).lexically_normal().string();
296}
297
298std::string BasenameLower(const std::string& path) {
299 return ToLowerCopy(std::filesystem::path(path).filename().string());
300}
301
303#if defined(_WIN32)
304 return static_cast<uint64_t>(::GetCurrentProcessId());
305#elif defined(__EMSCRIPTEN__)
306 return 0;
307#else
308 return static_cast<uint64_t>(::getpid());
309#endif
310}
311
312std::filesystem::path MakeProjectSaveTempPath(
313 const std::filesystem::path& target_path) {
314 static std::atomic<uint64_t> next_temp_id{0};
315 std::filesystem::path temp_path = target_path;
316 temp_path +=
317 ".tmp." + std::to_string(CurrentProcessIdForProjectSave()) + "." +
318 std::to_string(next_temp_id.fetch_add(1, std::memory_order_relaxed));
319 return temp_path;
320}
321
322void RemoveProjectSaveTempFile(const std::filesystem::path& temp_path) {
323 std::error_code remove_error;
324 std::filesystem::remove(temp_path, remove_error);
325}
326
328 const std::filesystem::path& target_path, absl::string_view contents,
329 bool replace_existing) {
330#ifdef __EMSCRIPTEN__
331 if (!replace_existing) {
332 // MEMFS/IDBFS do not support hard links. Exclusive fopen keeps the existing
333 // synchronous virtual-filesystem workflow without replacing a destination.
334 FILE* file = std::fopen(target_path.c_str(), "wbx");
335 if (file == nullptr) {
336 if (errno == EEXIST) {
337 return absl::AlreadyExistsError(absl::StrFormat(
338 "Project file already exists: %s", target_path.string()));
339 }
340 return absl::InternalError(absl::StrFormat(
341 "Cannot create project file: %s", target_path.string()));
342 }
343 const bool written = std::fwrite(contents.data(), 1, contents.size(),
344 file) == contents.size();
345 const bool closed = std::fclose(file) == 0;
346 if (!written || !closed) {
347 RemoveProjectSaveTempFile(target_path);
348 return absl::InternalError(absl::StrFormat(
349 "Failed to write project file: %s", target_path.string()));
350 }
351 return absl::OkStatus();
352 }
353#endif
354 const std::filesystem::path temp_path = MakeProjectSaveTempPath(target_path);
355#if defined(_WIN32)
356 HANDLE file = CreateFileW(temp_path.c_str(), GENERIC_WRITE, 0, nullptr,
357 CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr);
358 if (file == INVALID_HANDLE_VALUE) {
359 return absl::InvalidArgumentError(absl::StrFormat(
360 "Cannot create temporary project file: %s", temp_path.string()));
361 }
362
363 size_t written_total = 0;
364 while (written_total < contents.size()) {
365 const DWORD chunk = static_cast<DWORD>(
366 std::min<size_t>(contents.size() - written_total, 1024 * 1024));
367 DWORD written = 0;
368 if (!WriteFile(file, contents.data() + written_total, chunk, &written,
369 nullptr) ||
370 written == 0) {
371 CloseHandle(file);
372 RemoveProjectSaveTempFile(temp_path);
373 return absl::InternalError(absl::StrFormat(
374 "Failed to write temporary project file: %s", temp_path.string()));
375 }
376 written_total += written;
377 }
378 if (!FlushFileBuffers(file)) {
379 CloseHandle(file);
380 RemoveProjectSaveTempFile(temp_path);
381 return absl::InternalError(absl::StrFormat(
382 "Failed to write temporary project file: %s", temp_path.string()));
383 }
384 if (!CloseHandle(file)) {
385 RemoveProjectSaveTempFile(temp_path);
386 return absl::InternalError(absl::StrFormat(
387 "Failed to close temporary project file: %s", temp_path.string()));
388 }
389#else
390 // Exclusive creation establishes ownership; never reopen the staging path.
391 FILE* file = std::fopen(temp_path.c_str(), "wbx");
392 if (file == nullptr) {
393 return absl::InvalidArgumentError(absl::StrFormat(
394 "Cannot create temporary project file: %s", temp_path.string()));
395 }
396 const bool written =
397 std::fwrite(contents.data(), 1, contents.size(), file) == contents.size();
398 const bool flushed = std::fflush(file) == 0;
399 const bool closed = std::fclose(file) == 0;
400 if (!written || !flushed) {
401 RemoveProjectSaveTempFile(temp_path);
402 return absl::InternalError(absl::StrFormat(
403 "Failed to write temporary project file: %s", temp_path.string()));
404 }
405 if (!closed) {
406 RemoveProjectSaveTempFile(temp_path);
407 return absl::InternalError(absl::StrFormat(
408 "Failed to close temporary project file: %s", temp_path.string()));
409 }
410#endif
411
412 std::error_code rename_error;
413#if defined(_WIN32)
414 const DWORD move_flags =
415 MOVEFILE_WRITE_THROUGH |
416 (replace_existing ? MOVEFILE_REPLACE_EXISTING : static_cast<DWORD>(0));
417 if (!::MoveFileExW(temp_path.c_str(), target_path.c_str(), move_flags)) {
418 rename_error = std::error_code(static_cast<int>(::GetLastError()),
419 std::system_category());
420 }
421#elif defined(__EMSCRIPTEN__)
422 std::filesystem::rename(temp_path, target_path, rename_error);
423#else
424 if (replace_existing) {
425 std::filesystem::rename(temp_path, target_path, rename_error);
426 } else if (::link(temp_path.c_str(), target_path.c_str()) != 0) {
427 rename_error = std::error_code(errno, std::generic_category());
428 } else {
429 RemoveProjectSaveTempFile(temp_path);
430 }
431#endif
432 if (rename_error) {
433 RemoveProjectSaveTempFile(temp_path);
434 bool target_already_exists = rename_error == std::errc::file_exists;
435#if defined(_WIN32)
436 target_already_exists = target_already_exists ||
437 rename_error.value() == ERROR_FILE_EXISTS ||
438 rename_error.value() == ERROR_ALREADY_EXISTS;
439#endif
440 if (!replace_existing && target_already_exists) {
441 return absl::AlreadyExistsError(absl::StrFormat(
442 "Project file already exists: %s", target_path.string()));
443 }
444 return absl::InternalError(
445 absl::StrFormat("Failed to replace project file %s: %s",
446 target_path.string(), rename_error.message()));
447 }
448
449 return absl::OkStatus();
450}
451} // namespace
452
453absl::Status WriteProjectFileAtomically(absl::string_view target_path,
454 absl::string_view contents,
455 bool replace_existing) {
456 return WriteProjectFileAtomicallyImpl(
457 std::filesystem::path(std::string(target_path)), contents,
458 replace_existing);
459}
460
461std::string RomRoleToString(RomRole role) {
462 switch (role) {
463 case RomRole::kBase:
464 return "base";
466 return "patched";
468 return "release";
469 case RomRole::kDev:
470 default:
471 return "dev";
472 }
473}
474
475RomRole ParseRomRole(absl::string_view value) {
476 std::string lower = ToLowerCopy(std::string(value));
477 if (lower == "base") {
478 return RomRole::kBase;
479 }
480 if (lower == "patched") {
481 return RomRole::kPatched;
482 }
483 if (lower == "release") {
484 return RomRole::kRelease;
485 }
486 return RomRole::kDev;
487}
488
490 switch (policy) {
492 return "allow";
494 return "block";
496 return "read_only";
498 default:
499 return "warn";
500 }
501}
502
503RomWritePolicy ParseRomWritePolicy(absl::string_view value) {
504 std::string lower = ToLowerCopy(std::string(value));
505 if (lower == "allow") {
507 }
508 if (lower == "block") {
510 }
511 if (lower == "read_only") {
513 }
515}
516
517// YazeProject Implementation
519 auto& provider = zelda3::GetResourceLabels();
520 if (provider.hack_manifest() == &hack_manifest) {
521 provider.SetHackManifest(nullptr);
522 }
523 if (provider.GetAllProjectLabels() == &resource_labels) {
524 provider.SetProjectLabels(nullptr);
525 }
526}
527
528absl::Status YazeProject::Create(const std::string& project_name,
529 const std::string& base_path) {
530 name = project_name;
531 filepath = base_path + "/" + project_name + ".yaze";
532
533 // Initialize metadata
534 auto now = std::chrono::system_clock::now();
535 auto time_t = std::chrono::system_clock::to_time_t(now);
536 std::stringstream ss;
537 ss << std::put_time(std::localtime(&time_t), "%Y-%m-%d %H:%M:%S");
538
539 metadata.created_date = ss.str();
540 metadata.last_modified = ss.str();
542 metadata.version = "2.0";
543 metadata.created_by = "YAZE";
545
547
548#ifndef __EMSCRIPTEN__
549 // Create project directory structure
550 std::filesystem::path project_dir(base_path + "/" + project_name);
551 std::filesystem::create_directories(project_dir);
552 std::filesystem::create_directories(project_dir / "code");
553 std::filesystem::create_directories(project_dir / "assets");
554 std::filesystem::create_directories(project_dir / "patches");
555 std::filesystem::create_directories(project_dir / "backups");
556 std::filesystem::create_directories(project_dir / "output");
557
558 // Set folder paths
559 code_folder = (project_dir / "code").string();
560 assets_folder = (project_dir / "assets").string();
561 patches_folder = (project_dir / "patches").string();
562 rom_backup_folder = (project_dir / "backups").string();
563 output_folder = (project_dir / "output").string();
564 labels_filename = (project_dir / "labels.txt").string();
565 symbols_filename = (project_dir / "symbols.txt").string();
566#else
567 // WASM: keep paths relative; persistence handled by WasmStorage/IDBFS
568 code_folder = "code";
569 assets_folder = "assets";
570 patches_folder = "patches";
571 rom_backup_folder = "backups";
572 output_folder = "output";
573 labels_filename = "labels.txt";
574 symbols_filename = "symbols.txt";
575#endif
576
577 return Save();
578}
579
580// static
581std::string YazeProject::ResolveBundleRoot(const std::string& path) {
582 if (path.empty()) {
583 return {};
584 }
585
586 // Walk up the path hierarchy looking for a directory whose filename
587 // (extension) is ".yazeproj". The first match from the leaf upward wins.
588 std::error_code ec;
589 auto current = std::filesystem::path(path).lexically_normal();
590
591 for (; !current.empty(); current = current.parent_path()) {
592 if (current.extension() == ".yazeproj") {
593 // Must be an existing directory to count as a valid bundle root.
594 if (std::filesystem::is_directory(current, ec) && !ec) {
595 return current.string();
596 }
597 }
598 // Guard against infinite loop at the filesystem root.
599 if (current == current.parent_path()) {
600 break;
601 }
602 }
603
604 return {};
605}
606
607absl::Status YazeProject::Open(const std::string& project_path) {
608 // Resolve bundle root: if the user opened a file *inside* a .yazeproj
609 // bundle, normalize to the bundle root directory so the existing
610 // .yazeproj handling takes over.
611 std::string resolved_path = project_path;
612 const std::string bundle_root = ResolveBundleRoot(project_path);
613 if (!bundle_root.empty() && bundle_root != project_path) {
614 // The user pointed at a file inside a bundle; redirect to the root.
615 resolved_path = bundle_root;
616 }
617
618#ifndef __EMSCRIPTEN__
619 // Keep the project root stable even when a caller supplies a relative path.
620 // All project-relative files and registry fallbacks must resolve beside the
621 // project, not against whichever working directory happens to be active
622 // later in the session.
623 std::error_code absolute_ec;
624 const auto absolute_path =
625 std::filesystem::absolute(resolved_path, absolute_ec).lexically_normal();
626 if (!absolute_ec) {
627 resolved_path = absolute_path.string();
628 }
629#endif
630
631 filepath = resolved_path;
632
633#ifdef __EMSCRIPTEN__
634 // Prefer persistent storage in WASM builds
635 auto storage_key = MakeStorageKey("project");
636 auto storage_or = platform::WasmStorage::LoadProject(storage_key);
637 if (storage_or.ok()) {
638 return ParseFromString(storage_or.value());
639 }
640#endif
641
642 // Determine format and load accordingly
643 absl::Status load_status;
644 if (resolved_path.ends_with(".yazeproj")) {
646
647 const std::filesystem::path bundle_path(resolved_path);
648 std::error_code ec;
649 if (!std::filesystem::exists(bundle_path, ec) || ec ||
650 !std::filesystem::is_directory(bundle_path, ec) || ec) {
651 return absl::InvalidArgumentError(
652 absl::StrFormat("Project bundle does not exist: %s", resolved_path));
653 }
654
655 // Bundle convention: store the actual project config at the root so both
656 // desktop and iOS can open the same `.yazeproj` directory.
657 const std::filesystem::path project_file = bundle_path / "project.yaze";
658 filepath = project_file.string();
659
660 if (!std::filesystem::exists(project_file, ec) || ec) {
661 // Create a minimal portable project file for the bundle if missing.
663 name = bundle_path.stem().string();
664
665 // Initialize metadata timestamps (Create() normally does this).
666 auto now = std::chrono::system_clock::now();
667 auto time_t = std::chrono::system_clock::to_time_t(now);
668 std::stringstream ss;
669 ss << std::put_time(std::localtime(&time_t), "%Y-%m-%d %H:%M:%S");
670 if (metadata.created_date.empty()) {
671 metadata.created_date = ss.str();
672 }
673 metadata.last_modified = ss.str();
674 if (metadata.yaze_version.empty()) {
676 }
677 if (metadata.version.empty()) {
678 metadata.version = "2.0";
679 }
680 if (metadata.created_by.empty()) {
681 metadata.created_by = "YAZE";
682 }
683 if (metadata.project_id.empty()) {
685 }
686
687 // Bundle layout defaults (paths stored as absolute; serializer writes
688 // relative values for portability).
689 const std::filesystem::path rom_candidate = bundle_path / "rom";
690 // Always set the expected bundle ROM path even if the file is not yet
691 // present on disk (e.g. still downloading from iCloud). The load
692 // attempt in LoadProjectWithRom() handles the missing-file case without
693 // corrupting the project by saving a temporary path.
694 rom_filename = rom_candidate.string();
695
696 const std::filesystem::path project_dir = bundle_path / "project";
697 const std::filesystem::path code_dir = bundle_path / "code";
698 if (std::filesystem::exists(project_dir, ec) &&
699 std::filesystem::is_directory(project_dir, ec) && !ec) {
700 code_folder = project_dir.string();
701 } else if (std::filesystem::exists(code_dir, ec) &&
702 std::filesystem::is_directory(code_dir, ec) && !ec) {
703 code_folder = code_dir.string();
704 }
705
706 assets_folder = (bundle_path / "assets").string();
707 patches_folder = (bundle_path / "patches").string();
708 rom_backup_folder = (bundle_path / "backups").string();
709 output_folder = (bundle_path / "output").string();
710 labels_filename = (bundle_path / "labels.txt").string();
711 symbols_filename = (bundle_path / "symbols.txt").string();
712
713 load_status = SaveToYazeFormat();
714 } else {
715 load_status = LoadFromYazeFormat(project_file.string());
716 }
717 } else if (resolved_path.ends_with(".yaze")) {
719
720 // Try to detect if it's JSON format by peeking at first character
721 std::ifstream file(resolved_path);
722 if (file.is_open()) {
723 std::stringstream buffer;
724 buffer << file.rdbuf();
725 std::string content = buffer.str();
726
727#ifdef YAZE_ENABLE_JSON_PROJECT_FORMAT
728 if (!content.empty() && content.front() == '{') {
729 LOG_DEBUG("Project", "Detected JSON format project file");
730 load_status = LoadFromJsonFormat(resolved_path);
731 } else {
732 load_status = ParseFromString(content);
733 }
734#else
735 load_status = ParseFromString(content);
736#endif
737 } else {
738 return absl::InvalidArgumentError(
739 absl::StrFormat("Cannot open project file: %s", resolved_path));
740 }
741 } else if (resolved_path.ends_with(".zsproj")) {
743 load_status = ImportFromZScreamFormat(resolved_path);
744 } else {
745 return absl::InvalidArgumentError("Unsupported project file format");
746 }
747
748 if (!load_status.ok()) {
749 return load_status;
750 }
751
752 // Normalize project-relative paths so downstream code never depends on the
753 // process working directory (important for iOS and portable bundles).
755
756 // Auto-load z3dk project config if discoverable.
758
759 // Auto-load hack manifest if configured or discoverable
761
762 return absl::OkStatus();
763}
764
765absl::Status YazeProject::Save() {
766 return SaveToYazeFormat();
767}
768
769absl::Status YazeProject::SaveNew() {
770 return SaveToYazeFormat(false);
771}
772
773absl::Status YazeProject::LoadFromString(const std::string& content,
774 const std::string& project_path) {
775 if (project_path.empty()) {
776 return absl::InvalidArgumentError("Project file path cannot be empty");
777 }
778
779 *this = YazeProject();
780
781#ifndef __EMSCRIPTEN__
782 std::error_code ec;
783 const auto absolute_path =
784 std::filesystem::absolute(project_path, ec).lexically_normal();
785 filepath =
786 ec ? std::filesystem::path(project_path).lexically_normal().string()
787 : absolute_path.string();
788#else
789 filepath = project_path;
790#endif
792 try {
794 } catch (const std::exception& error) {
795 return absl::InvalidArgumentError(
796 absl::StrFormat("Invalid project file value: %s", error.what()));
797 }
801 return absl::OkStatus();
802}
803
805 const std::string& content) const {
806 YazeProject saved;
807 saved.filepath = filepath;
808 try {
809 RETURN_IF_ERROR(saved.ParseFromString(content));
811 } catch (const std::exception& error) {
812 return absl::InvalidArgumentError(
813 std::string("Invalid saved project bindings: ") + error.what());
814 }
815 ASSIGN_OR_RETURN(const auto live_text, SerializeToString());
816 ASSIGN_OR_RETURN(const auto saved_text, saved.SerializeToString());
817 const auto bindings = [](const std::string& text) {
818 std::map<std::pair<std::string, std::string>, std::string> values;
819 std::istringstream input(text);
820 std::string line, section;
821 while (std::getline(input, line)) {
822 const auto trimmed = absl::StripAsciiWhitespace(line);
823 if (trimmed.empty() || trimmed.front() == '#')
824 continue;
825 if (trimmed.front() == '[' && trimmed.back() == ']') {
826 section = std::string(trimmed.substr(1, trimmed.size() - 2));
827 continue;
828 }
829 // These sections are transient application preferences, not sources.
830 if (section == "workspace" || section == "keybindings" ||
831 section == "editor_visibility" || section == "agent_settings")
832 continue;
833 const auto [key, value] = ParseKeyValue(std::string(trimmed));
834 if (key.empty() || (section == "project" &&
835 (key == "last_modified" || key == "created_date")))
836 continue;
837 values[{section, key}] = value;
838 }
839 return values;
840 };
841 const auto live_bindings = bindings(live_text);
842 const auto saved_bindings = bindings(saved_text);
843 const auto mismatch = [](const auto& key) {
844 return absl::FailedPreconditionError(
845 "Saved project source bindings differ from the active project: [" +
846 key.first + "] " + key.second);
847 };
848 for (const auto& [key, value] : live_bindings) {
849 const auto saved = saved_bindings.find(key);
850 if (saved == saved_bindings.end() || saved->second != value)
851 return mismatch(key);
852 }
853 for (const auto& [key, value] : saved_bindings) {
854 if (live_bindings.find(key) == live_bindings.end())
855 return mismatch(key);
856 }
857 return absl::OkStatus();
858}
859
860namespace {
861#if defined(YAZE_WITH_JSON) && !defined(__EMSCRIPTEN__)
862absl::Status ExpandedRunError(const std::string& message) {
863 return absl::FailedPreconditionError("Expanded run target unavailable: " +
864 message);
865}
866absl::Status ExpandedRunRequire(bool valid, const std::string& message) {
867 return valid ? absl::OkStatus() : ExpandedRunError(message);
868}
869std::string ExpandedRunPathText(const std::filesystem::path& path) {
870 const auto text = path.generic_u8string();
871 return std::string(text.begin(), text.end());
872}
873absl::StatusOr<std::string> ExpandedRunCanonical(const std::string& text) {
874 const auto path = std::filesystem::u8path(text);
875 if (text.empty() || !path.is_absolute()) {
876 return ExpandedRunError("an absolute evidence path is required");
877 }
878 std::error_code error;
879 const auto canonical = std::filesystem::weakly_canonical(path, error);
880 if (error) {
881 return ExpandedRunError("cannot resolve evidence path: " + text);
882 }
883 return ExpandedRunPathText(canonical);
884}
885#endif
886} // namespace
887
888absl::StatusOr<std::optional<std::string>>
890 const std::string& editing_source_path) const {
891 if (lyra_build_request_file.empty()) {
892 return std::optional<std::string>{};
893 }
894#if !defined(YAZE_WITH_JSON) || defined(__EMSCRIPTEN__)
895 return absl::UnimplementedError(
896 "Expanded run target verification requires desktop JSON support");
897#else
898 namespace fs = std::filesystem;
899 if (!project_opened() || native_save_producer_file.empty() ||
901 build_target.empty() || code_folder.empty() ||
902 editing_source_path.empty()) {
903 return ExpandedRunError("complete saved expanded references are required");
904 }
905 try {
906 ASSIGN_OR_RETURN(const auto descriptor, ExpandedRunCanonical(filepath));
907 const auto resolve = [&](const std::string& ref) {
908 return ExpandedRunCanonical(GetAbsolutePath(ref));
909 };
910 ASSIGN_OR_RETURN(const auto rom, resolve(rom_filename));
911 ASSIGN_OR_RETURN(const auto terrain, resolve(lyra_terrain_file));
912 ASSIGN_OR_RETURN(const auto request_path, resolve(lyra_build_request_file));
913 ASSIGN_OR_RETURN(const auto producer_path,
915 ASSIGN_OR_RETURN(const auto future_proof,
917 ASSIGN_OR_RETURN(const auto candidate, resolve(build_target));
918 ASSIGN_OR_RETURN(const auto engine, resolve(code_folder));
920 const auto authority,
921 core::GetNativeSaveContext(editing_source_path, descriptor));
922 const fs::path target = fs::u8path(candidate);
923 const auto relative =
924 target.lexically_relative(fs::u8path(engine) / "Roms");
925 RETURN_IF_ERROR(ExpandedRunRequire(
926 target.filename() == "oos168x.sfc" && !relative.empty() &&
927 !relative.is_absolute() && *relative.begin() != ".." &&
928 !relative.parent_path().empty(),
929 "build_target must name a generation under the project engine"));
930
931 // These are freshness joins for artifacts already authenticated by Oracle's
932 // compiler. They do not reproduce allocation or native-verifier policy.
933 uint64_t remaining = 8ULL * 1024 * 1024 * 1024;
934 std::vector<Json> observed;
935 const auto check_file = [&](const Json& row) -> absl::Status {
936 ASSIGN_OR_RETURN(const auto name,
937 ExpandedRunCanonical(row.at("path").get<std::string>()));
938 const auto path = fs::u8path(name);
939 const auto expected = row.at("sha256").get<std::string>();
940 std::error_code file_error;
941 const auto size = fs::file_size(path, file_error);
942 RETURN_IF_ERROR(ExpandedRunRequire(
943 !file_error && fs::is_regular_file(path, file_error) && !file_error &&
944 fs::u8path(row.at("path").get<std::string>()) == path &&
945 expected.size() == 64 && size <= 1024ULL * 1024 * 1024 &&
946 size <= remaining,
947 "an evidence file is missing, changed, or exceeds its bounds"));
948 remaining -= size;
949 for (const char* key : {"size", "bytes"}) {
950 if (row.contains(key)) {
952 ExpandedRunRequire(row.at(key).is_number_unsigned() &&
953 row.at(key).get<uint64_t>() == size,
954 "an evidence byte count differs"));
955 }
956 }
957 ASSIGN_OR_RETURN(const auto actual,
959 return ExpandedRunRequire(actual == expected,
960 "evidence changed: " + name);
961 };
962 const auto check = [&](const Json& row) -> absl::Status {
963 RETURN_IF_ERROR(ExpandedRunRequire(observed.size() < 8192,
964 "too many evidence bindings"));
965 RETURN_IF_ERROR(check_file(row));
966 observed.push_back(row);
967 return absl::OkStatus();
968 };
969 const auto rows = [&](const Json& values) -> absl::Status {
970 RETURN_IF_ERROR(ExpandedRunRequire(
971 (values.is_array() || values.is_object()) && values.size() <= 4096,
972 "an evidence binding collection is malformed"));
973 for (const auto& row : values)
974 RETURN_IF_ERROR(check(row));
975 return absl::OkStatus();
976 };
977 const auto read_text =
978 [&](const std::string& name) -> absl::StatusOr<std::string> {
979 std::error_code file_error;
980 const auto path = fs::u8path(name);
981 const auto size = fs::file_size(path, file_error);
983 ExpandedRunRequire(!file_error && size <= 16 * 1024 * 1024,
984 "metadata is missing or too large"));
985 std::ifstream stream(path, std::ios::binary);
986 std::string raw(static_cast<size_t>(size), '\0');
987 stream.read(raw.data(), static_cast<std::streamsize>(raw.size()));
988 char extra;
989 RETURN_IF_ERROR(ExpandedRunRequire(
990 stream.gcount() == static_cast<std::streamsize>(raw.size()) &&
991 !stream.get(extra) && !stream.bad(),
992 "metadata changed while reading"));
993 RETURN_IF_ERROR(check(Json{
994 {"path", name}, {"sha256", core::ComputeSourceArtifactSha256(raw)}}));
995 return raw;
996 };
997 const auto load = [&](const std::string& name) -> absl::StatusOr<Json> {
998 ASSIGN_OR_RETURN(const auto raw, read_text(name));
999 auto document =
1000 Json::parse(raw, [](int depth, Json::parse_event_t, Json&) {
1001 if (depth > 64)
1002 throw std::runtime_error("evidence nesting is too deep");
1003 return true;
1004 });
1005 RETURN_IF_ERROR(ExpandedRunRequire(document.is_object(),
1006 "evidence JSON must be an object"));
1007 return document;
1008 };
1009 const auto same_path = [&](const Json& value,
1010 const std::string& expected) -> absl::Status {
1011 ASSIGN_OR_RETURN(const auto path,
1012 ExpandedRunCanonical(value.get<std::string>()));
1013 return ExpandedRunRequire(path == expected,
1014 "evidence belongs to another path");
1015 };
1016 const auto sibling = [&](const char* name) {
1017 return ExpandedRunPathText(target.parent_path() / name);
1018 };
1019 ASSIGN_OR_RETURN(const auto build, load(sibling("oos168x.build.json")));
1020 RETURN_IF_ERROR(ExpandedRunRequire(
1021 build.at("schema_version") == 1 &&
1022 (build.at("state") == "completed" ||
1023 build.at("state") == "completed_with_gaps") &&
1024 build.at("finish").at("exit_code") == 0 &&
1025 build.at("finish").at("required_gaps") == Json::array(),
1026 "the selected build is incomplete or failed"));
1027 RETURN_IF_ERROR(same_path(build.at("root"), engine));
1028 const auto& artifacts = build.at("artifacts");
1029 for (const char* key :
1030 {"output_rom", "symbols", "lyra_prepare", "lyra_verification"}) {
1032 ExpandedRunRequire(artifacts.at(key).contains("size"),
1033 "a build artifact byte count is absent"));
1034 RETURN_IF_ERROR(check(artifacts.at(key)));
1035 }
1036 RETURN_IF_ERROR(check(build.at("base_rom")));
1037 RETURN_IF_ERROR(same_path(build.at("base_rom").at("path"), rom));
1039 same_path(artifacts.at("output_rom").at("path"), candidate));
1041 ExpandedRunRequire(artifacts.at("output_rom").at("size") == 0x400000,
1042 "the expanded output must be exactly 4 MiB"));
1044 same_path(artifacts.at("symbols").at("path"), sibling("oos168x.sym")));
1045 RETURN_IF_ERROR(same_path(artifacts.at("lyra_verification").at("path"),
1046 sibling("oos168x.lyra-verification.json")));
1048 const auto prepared,
1049 load(artifacts.at("lyra_prepare").at("path").get<std::string>()));
1051 const auto verified,
1052 load(artifacts.at("lyra_verification").at("path").get<std::string>()));
1053 RETURN_IF_ERROR(ExpandedRunRequire(
1054 prepared.at("kind") == "lyra.current_build_prepare.v1" &&
1055 verified.at("kind") == "lyra.current_build_verification.v1" &&
1056 verified.at("compiled_consumers_verified") == true &&
1057 verified.at("prepared_sha256") ==
1058 artifacts.at("lyra_prepare").at("sha256") &&
1059 verified.at("compiled_rom_sha256") ==
1060 artifacts.at("output_rom").at("sha256") &&
1061 verified.at("symbols_sha256") ==
1062 artifacts.at("symbols").at("sha256") &&
1063 verified.at("saved_rom_sha256") ==
1064 build.at("base_rom").at("sha256") &&
1065 prepared.at("saved_rom_sha256") == verified.at("saved_rom_sha256"),
1066 "compiled verification does not bind this build"));
1067 for (const char* flag :
1068 {"selected", "allocation_granted", "producer_enforcement_verified",
1069 "runtime_admission", "ordinary_build_ready"}) {
1070 RETURN_IF_ERROR(ExpandedRunRequire(
1071 prepared.at(flag).is_boolean() && prepared.at(flag).get<bool>(),
1072 "expanded build admission is incomplete"));
1073 }
1075 ExpandedRunRequire(!prepared.contains("runtime_scope"),
1076 "a private preview is not an ordinary run target"));
1077 RETURN_IF_ERROR(same_path(prepared.at("project"), descriptor));
1078 RETURN_IF_ERROR(same_path(prepared.at("base_rom"), rom));
1079 RETURN_IF_ERROR(same_path(prepared.at("request"), request_path));
1080 RETURN_IF_ERROR(rows(prepared.at("inputs")));
1081 const Json* request_binding = nullptr;
1082 for (const auto& row : prepared.at("inputs")) {
1083 ASSIGN_OR_RETURN(const auto path,
1084 ExpandedRunCanonical(row.at("path").get<std::string>()));
1085 if (path == request_path)
1086 request_binding = &row;
1087 }
1089 ExpandedRunRequire(request_binding != nullptr,
1090 "the preparation omits its request binding"));
1091 RETURN_IF_ERROR(check(*request_binding));
1092 ASSIGN_OR_RETURN(const auto request, load(request_path));
1093 const auto& live = request.at("live");
1094 const auto& plan = prepared.at("terrain_plan");
1095 RETURN_IF_ERROR(ExpandedRunRequire(
1096 request.at("kind") == "lyra.current_save_transfer_request.v1" &&
1097 plan.at("target") == request.at("target") &&
1098 plan.at("transfer_id") == request.at("transfer_id") &&
1099 plan.at("generation") == live.at("generation") &&
1100 plan.at("operation_id") == live.at("operation_id") &&
1101 plan.at("save_context") == live.at("save_context"),
1102 "the prepared transfer or saved generation differs"));
1103 RETURN_IF_ERROR(same_path(request.at("target").at("path"), candidate));
1104 RETURN_IF_ERROR(same_path(live.at("project"), descriptor));
1105 RETURN_IF_ERROR(same_path(live.at("producer").at("path"), producer_path));
1106 RETURN_IF_ERROR(same_path(live.at("enforcement_receipt"), future_proof));
1107 RETURN_IF_ERROR(same_path(live.at("inputs").at("rom").at("path"), rom));
1109 same_path(live.at("inputs").at("project").at("path"), descriptor));
1111 same_path(live.at("inputs").at("terrain").at("path"), terrain));
1112 RETURN_IF_ERROR(rows(live.at("inputs")));
1113 RETURN_IF_ERROR(rows(request.at("inputs")));
1114 for (const char* key : {"producer", "save_receipt", "save_context"})
1115 RETURN_IF_ERROR(check(live.at(key)));
1116 for (const char* key : {"observation", "save_snapshot", "producer_snapshot",
1117 "context_snapshot"})
1118 RETURN_IF_ERROR(check(request.at(key)));
1119 const auto& producer = live.at("producer_identity");
1120 RETURN_IF_ERROR(check(producer.at("editor")));
1121 RETURN_IF_ERROR(check(producer.at("verifier")));
1122 RETURN_IF_ERROR(rows(producer.at("source_bindings")));
1123 RETURN_IF_ERROR(check(plan.at("enforcement_receipt")));
1124 if (request.contains("native_verification"))
1125 RETURN_IF_ERROR(check(request.at("native_verification")));
1126 if (plan.at("enforcement_owner") == "oracle.current_builder") {
1127 RETURN_IF_ERROR(ExpandedRunRequire(
1128 plan.at("enforcement_receipt") == request.at("native_verification"),
1129 "the builder enforcement binding differs"));
1130 } else {
1132 ExpandedRunRequire(plan.at("enforcement_owner") == "legacy_producer",
1133 "the enforcement owner is unsupported"));
1135 same_path(plan.at("enforcement_receipt").at("path"), future_proof));
1136 }
1137
1138 const auto receipt_path = descriptor + ".native-save.json";
1140 same_path(live.at("save_receipt").at("path"), receipt_path));
1141 RETURN_IF_ERROR(same_path(live.at("save_context").at("path"),
1142 ExpandedRunPathText(authority.path)));
1143 ASSIGN_OR_RETURN(const auto receipt, load(receipt_path));
1144 ASSIGN_OR_RETURN(const auto context,
1145 load(ExpandedRunPathText(authority.path)));
1146 RETURN_IF_ERROR(ExpandedRunRequire(
1147 receipt.at("kind") == "yaze.native_save.v1" &&
1148 receipt.at("bindings_complete") == true &&
1149 receipt.at("binding_errors") == Json::array() &&
1150 live.at("generation").is_number_unsigned() &&
1151 live.at("generation").get<uint64_t>() > 0 &&
1152 live.at("operation_id").is_string() &&
1153 !live.at("operation_id").get_ref<const std::string&>().empty() &&
1154 receipt.at("generation") == live.at("generation") &&
1155 receipt.at("operation_id") == live.at("operation_id") &&
1156 context.at("kind") == "yaze.native_save_context.v1" &&
1157 context.at("state") == "committed" &&
1158 context.at("native_commit").at("generation") ==
1159 live.at("generation") &&
1160 context.at("native_commit").at("operation_id") ==
1161 live.at("operation_id"),
1162 "native Save completion or current context differs"));
1163 RETURN_IF_ERROR(same_path(receipt.at("receipt_path"), receipt_path));
1164 RETURN_IF_ERROR(same_path(context.at("native_commit").at("receipt_path"),
1165 receipt_path));
1166 RETURN_IF_ERROR(same_path(context.at("editing_source_path"),
1167 authority.editing_source_path));
1168 RETURN_IF_ERROR(same_path(context.at("project_path"), descriptor));
1169 RETURN_IF_ERROR(same_path(context.at("rom_path"), rom));
1170 const Json* saved_producer = nullptr;
1171 for (const auto& row : receipt.at("artifacts")) {
1172 if (row.at("role") == "producer")
1173 saved_producer = &row;
1174 }
1175 RETURN_IF_ERROR(ExpandedRunRequire(
1176 saved_producer != nullptr &&
1177 context.at("producer") == *saved_producer &&
1178 saved_producer->at("sha256") == producer.at("editor").at("sha256"),
1179 "native Save and context producers differ"));
1180 ASSIGN_OR_RETURN(const auto editor_path,
1181 ExpandedRunCanonical(
1182 producer.at("editor").at("path").get<std::string>()));
1183 RETURN_IF_ERROR(same_path(saved_producer->at("path"), editor_path));
1184 ASSIGN_OR_RETURN(const auto saved_descriptor, read_text(descriptor));
1186 for (const auto& binding : observed)
1187 RETURN_IF_ERROR(check_file(binding));
1188 return std::optional<std::string>(candidate);
1189 } catch (const std::exception& error) {
1190 return ExpandedRunError(
1191 std::string("invalid or incomplete build evidence: ") + error.what());
1192 }
1193#endif
1194}
1195absl::Status YazeProject::SaveAs(const std::string& new_path) {
1196 std::string old_filepath = filepath;
1197 filepath = new_path;
1198
1199 auto status = Save();
1200 if (!status.ok()) {
1201 filepath = old_filepath; // Restore on failure
1202 }
1203
1204 return status;
1205}
1206
1208 return hack_manifest.loaded() &&
1209 (hack_manifest.oracle_progression_state().has_value() ||
1210 ToLowerCopy(hack_manifest.hack_name()).find("oracle") !=
1211 std::string::npos);
1212}
1213
1215 const std::string& new_path, const core::LyraTerrainDocument& terrain,
1216 const std::optional<std::string>& expected_source_bytes) {
1217#ifdef __EMSCRIPTEN__
1218 return absl::UnimplementedError(
1219 "Saving an independent Lyra project copy requires filesystem storage");
1220#else
1221 namespace fs = std::filesystem;
1222 if (!project_opened() || lyra_terrain_file.empty() || new_path.empty()) {
1223 return absl::FailedPreconditionError(
1224 "An open project and Lyra terrain are required for Save As");
1225 }
1226 auto read =
1227 [](const fs::path& path) -> absl::StatusOr<std::optional<std::string>> {
1228 std::error_code ec;
1229 if (!fs::exists(path, ec) && !ec) {
1230 return std::nullopt;
1231 }
1232 if (ec || !fs::is_regular_file(path, ec) || ec ||
1233 fs::file_size(path, ec) > 16 * 1024 * 1024 || ec) {
1234 return absl::FailedPreconditionError(
1235 "Project Save As source or destination is not a readable file");
1236 }
1237 std::ifstream stream(path, std::ios::binary);
1238 if (!stream) {
1239 return absl::PermissionDeniedError("Cannot read project Save As file");
1240 }
1241 std::string bytes{std::istreambuf_iterator<char>(stream), {}};
1242 if (stream.bad()) {
1243 return absl::DataLossError(
1244 "Could not finish reading project Save As file");
1245 }
1246 return std::optional<std::string>(std::move(bytes));
1247 };
1248
1249 auto candidate = *this;
1250 // Preserve the original meaning of every relative asset before relocating.
1251 candidate.NormalizePathsToAbsolute();
1252 std::error_code ec;
1253 const fs::path source_project = fs::absolute(candidate.filepath, ec);
1254 if (ec) {
1255 return absl::InvalidArgumentError("Cannot resolve the source project path");
1256 }
1257 const fs::path source_terrain = candidate.lyra_terrain_file;
1258 const fs::path destination = fs::absolute(new_path, ec).lexically_normal();
1259 if (ec) {
1260 return absl::InvalidArgumentError(
1261 "Cannot resolve the project Save As path");
1262 }
1263 fs::path destination_terrain = destination;
1264 destination_terrain.replace_extension(".lyra-terrain.json");
1265 candidate.filepath = destination.string();
1266 candidate.lyra_terrain_file = destination_terrain.string();
1267
1268 const std::vector<fs::path> targets = {destination, destination_terrain};
1269 std::vector<fs::path> read_only_targets = {source_project, source_terrain};
1270 if (!candidate.rom_filename.empty()) {
1271 read_only_targets.push_back(candidate.rom_filename);
1272 }
1273 if (!candidate.lyra_build_request_file.empty()) {
1274 read_only_targets.push_back(candidate.lyra_build_request_file);
1275 }
1276 for (const auto& reference : {candidate.native_save_producer_file,
1277 candidate.native_save_transfer_proof_file,
1278 candidate.native_save_receipt_file}) {
1279 if (!reference.empty()) {
1280 read_only_targets.push_back(reference);
1281 }
1282 }
1284 auto lock,
1286 targets, {"Lyra project", "Lyra project files"}, read_only_targets));
1287 ASSIGN_OR_RETURN(auto original_project, read(source_project));
1288 if (!original_project) {
1289 return absl::NotFoundError("The original project file is missing");
1290 }
1291 ASSIGN_OR_RETURN(auto original_terrain, read(source_terrain));
1292 if (original_terrain != expected_source_bytes) {
1293 return absl::FailedPreconditionError(
1294 "Lyra terrain changed on disk; refusing stale Save As");
1295 }
1296 ASSIGN_OR_RETURN(auto previous_destination, read(destination));
1297 ASSIGN_OR_RETURN(auto previous_terrain, read(destination_terrain));
1298 if (previous_terrain) {
1299 return absl::AlreadyExistsError(
1300 "The destination project already owns Lyra terrain; choose another "
1301 "project name");
1302 }
1303 auto now =
1304 std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
1305 std::stringstream timestamp;
1306 timestamp << std::put_time(std::localtime(&now), "%Y-%m-%d %H:%M:%S");
1307 candidate.metadata.last_modified = timestamp.str();
1308 if (candidate.music_persistence.storage_key.empty()) {
1309 candidate.music_persistence.storage_key = candidate.MakeStorageKey("music");
1310 }
1311 ASSIGN_OR_RETURN(auto descriptor, candidate.SerializeToString());
1312 const std::string terrain_bytes = terrain.Serialize();
1314 *lock,
1315 {{source_project, original_project, *original_project},
1316 {destination, previous_destination, descriptor},
1317 {destination_terrain, std::nullopt, terrain_bytes}},
1318 core::ComputeSourceArtifactSha256(*original_project),
1319 [&]() -> absl::Status {
1320 ASSIGN_OR_RETURN(auto source_after, read(source_terrain));
1321 if (source_after != expected_source_bytes) {
1322 return absl::FailedPreconditionError(
1323 "Lyra terrain changed during Save As");
1324 }
1325 ASSIGN_OR_RETURN(auto saved, read(destination_terrain));
1326 if (!saved) {
1327 return absl::DataLossError("Saved Lyra terrain is missing");
1328 }
1329 return core::LyraTerrainDocument::Parse(*saved, terrain.profile())
1330 .status();
1331 }));
1332 *this = std::move(candidate);
1333 return absl::OkStatus();
1334#endif
1335}
1336
1337std::string YazeProject::MakeStorageKey(absl::string_view suffix) const {
1338 std::string base;
1339 if (!metadata.project_id.empty()) {
1340 base = metadata.project_id;
1341 } else if (!name.empty()) {
1342 base = name;
1343 } else if (!filepath.empty()) {
1344 base = std::filesystem::path(filepath).stem().string();
1345 }
1346 base = SanitizeStorageKey(base);
1347 if (suffix.empty()) {
1348 return base;
1349 }
1350 return absl::StrFormat("%s_%s", base, suffix);
1351}
1352
1353namespace {
1354// A '\r' not followed by '\n' is a classic-Mac line ending. The parser can
1355// normalize CRLF, but a lone CR would split a line somewhere it does not
1356// expect, so both directions refuse it.
1357bool ContainsLoneCarriageReturn(const std::string& content) {
1358 for (size_t i = 0; i < content.size(); ++i) {
1359 if (content[i] == '\r' &&
1360 (i + 1 >= content.size() || content[i + 1] != '\n')) {
1361 return true;
1362 }
1363 }
1364 return false;
1365}
1366
1367constexpr absl::string_view kProjectFileFooter = "# End of YAZE Project File";
1368
1369// Sections ParseFromString models. Everything else is passed through.
1370bool IsKnownProjectSection(absl::string_view name) {
1371 static constexpr absl::string_view kKnown[] = {"project",
1372 "files",
1373 "sprite_assets",
1374 "rom",
1375 "feature_flags",
1376 "workspace",
1377 "dungeon_overlay",
1378 "graphics_sheets",
1379 "atlas",
1380 "planning",
1381 "rom_addresses",
1382 "custom_objects",
1383 "agent_settings",
1384 "build",
1385 "keybindings",
1386 "editor_visibility",
1387 "zscream_compatibility",
1388 "music"};
1389 if (absl::StartsWith(name, "labels_")) {
1390 return true;
1391 }
1392 return std::find(std::begin(kKnown), std::end(kKnown), name) !=
1393 std::end(kKnown);
1394}
1395
1396// Comments SerializeToString writes itself; keeping them would duplicate
1397// them on every save.
1398bool IsGeneratedProjectComment(absl::string_view line, bool before_sections) {
1399 if (line == kProjectFileFooter) {
1400 return true;
1401 }
1402 if (!before_sections) {
1403 return false;
1404 }
1405 return absl::StartsWith(line, "# yaze Project File") ||
1406 absl::StartsWith(line, "# Format Version:") ||
1407 absl::StartsWith(line, "# Generated by YAZE") ||
1408 absl::StartsWith(line, "# Last Modified:");
1409}
1410
1412 ProjectFilePassthrough& passthrough, const std::string& name) {
1413 for (auto& section : passthrough.sections) {
1414 if (section.name == name) {
1415 return section;
1416 }
1417 }
1418 passthrough.sections.push_back({name, false, {}});
1419 return passthrough.sections.back();
1420}
1421
1422bool IsKeyLine(absl::string_view line) {
1423 return !line.empty() && line.front() != '#' &&
1424 line.find('=') != absl::string_view::npos;
1425}
1426
1427// Writes `canonical` (SerializeToString's own output) with the passthrough
1428// lines put back. See ProjectFilePassthrough for the placement rules.
1430 const std::string& canonical, const ProjectFilePassthrough& passthrough) {
1431 if (passthrough.empty()) {
1432 return canonical;
1433 }
1435
1436 struct CanonicalSection {
1437 std::string header;
1438 std::string name;
1439 std::vector<std::string> lines;
1440 };
1441 std::vector<std::string> preamble;
1442 std::vector<CanonicalSection> canonical_sections;
1443 bool has_footer = false;
1444 for (absl::string_view line : absl::StrSplit(canonical, '\n')) {
1445 if (line == kProjectFileFooter) {
1446 has_footer = true;
1447 continue;
1448 }
1449 if (line.size() >= 2 && line.front() == '[' && line.back() == ']') {
1450 canonical_sections.push_back(
1451 {std::string(line),
1452 std::string(line.substr(1, line.size() - 2)),
1453 {}});
1454 continue;
1455 }
1456 if (line.empty()) {
1457 continue;
1458 }
1459 (canonical_sections.empty() ? preamble : canonical_sections.back().lines)
1460 .push_back(std::string(line));
1461 }
1462
1463 auto find_section =
1464 [&](absl::string_view name) -> const ProjectFilePassthrough::Section* {
1465 for (const auto& section : passthrough.sections) {
1466 if (section.name == name) {
1467 return &section;
1468 }
1469 }
1470 return nullptr;
1471 };
1472
1473 std::ostringstream out;
1474 // Every comment and unknown line of `section`, in file order.
1475 auto write_all = [&](const ProjectFilePassthrough::Section& section) {
1476 for (const auto& line : section.lines) {
1477 if (line.kind != Kind::kKnownKey) {
1478 out << line.text << "\n";
1479 }
1480 }
1481 };
1482
1483 for (const auto& line : preamble) {
1484 out << line << "\n";
1485 }
1486 out << "\n";
1487 if (const auto* before = find_section(""); before != nullptr) {
1488 write_all(*before);
1489 if (!before->lines.empty()) {
1490 out << "\n";
1491 }
1492 }
1493
1494 std::vector<const ProjectFilePassthrough::Section*> merged;
1495 for (const auto& section : canonical_sections) {
1496 out << section.header << "\n";
1497 const auto* kept = find_section(section.name);
1498 if (kept == nullptr) {
1499 for (const auto& line : section.lines) {
1500 out << line << "\n";
1501 }
1502 out << "\n";
1503 continue;
1504 }
1505 merged.push_back(kept);
1506
1507 // Comments belong to the next key line after them in the file.
1508 std::vector<std::pair<std::string, std::vector<std::string>>> anchored;
1509 std::vector<std::string> pending;
1510 std::vector<std::pair<std::string, std::vector<std::string>>> unknown;
1511 for (const auto& line : kept->lines) {
1512 if (line.kind == Kind::kComment) {
1513 pending.push_back(line.text);
1514 } else if (line.kind == Kind::kUnknownLine) {
1515 unknown.push_back({line.text, std::move(pending)});
1516 pending.clear();
1517 } else if (!pending.empty()) {
1518 anchored.push_back({line.key, std::move(pending)});
1519 pending.clear();
1520 }
1521 }
1522 std::vector<bool> anchor_written(anchored.size(), false);
1523 for (const auto& line : section.lines) {
1524 if (IsKeyLine(line)) {
1525 const std::string key = ParseKeyValue(line).first;
1526 for (size_t i = 0; i < anchored.size(); ++i) {
1527 if (!anchor_written[i] && anchored[i].first == key) {
1528 for (const auto& comment : anchored[i].second) {
1529 out << comment << "\n";
1530 }
1531 anchor_written[i] = true;
1532 }
1533 }
1534 }
1535 out << line << "\n";
1536 }
1537 for (const auto& [text, comments] : unknown) {
1538 for (const auto& comment : comments) {
1539 out << comment << "\n";
1540 }
1541 out << text << "\n";
1542 }
1543 // Comments whose key this build no longer writes, then trailing ones.
1544 for (size_t i = 0; i < anchored.size(); ++i) {
1545 if (!anchor_written[i]) {
1546 for (const auto& comment : anchored[i].second) {
1547 out << comment << "\n";
1548 }
1549 }
1550 }
1551 for (const auto& comment : pending) {
1552 out << comment << "\n";
1553 }
1554 out << "\n";
1555 }
1556
1557 // Unknown sections, and known sections this build left out (for example
1558 // an emptied [graphics_sheets]) that still carry comments or unknown keys.
1559 for (const auto& section : passthrough.sections) {
1560 if (section.name.empty() ||
1561 std::find(merged.begin(), merged.end(), &section) != merged.end()) {
1562 continue;
1563 }
1564 const bool has_content = std::any_of(
1565 section.lines.begin(), section.lines.end(),
1566 [](const auto& line) { return line.kind != Kind::kKnownKey; });
1567 if (!has_content) {
1568 continue;
1569 }
1570 out << "[" << section.name << "]\n";
1571 write_all(section);
1572 out << "\n";
1573 }
1574
1575 if (has_footer) {
1576 out << kProjectFileFooter << "\n";
1577 }
1578 return out.str();
1579}
1580} // namespace
1581
1582bool IsKnownProjectFileSection(absl::string_view name) {
1583 return IsKnownProjectSection(name);
1584}
1585
1586absl::StatusOr<std::string> YazeProject::SerializeToString() const {
1587 std::ostringstream file;
1588
1589 // Write header comment
1590 file << "# yaze Project File\n";
1591 file << "# Format Version: 2.0\n";
1592 file << "# Generated by YAZE " << metadata.yaze_version << "\n";
1593 file << "# Last Modified: " << metadata.last_modified << "\n\n";
1594
1595 // Project section
1596 file << "[project]\n";
1597 file << "name=" << name << "\n";
1598 file << "description=" << metadata.description << "\n";
1599 file << "author=" << metadata.author << "\n";
1600 file << "license=" << metadata.license << "\n";
1601 file << "version=" << metadata.version << "\n";
1602 file << "created_date=" << metadata.created_date << "\n";
1603 file << "last_modified=" << metadata.last_modified << "\n";
1604 file << "yaze_version=" << metadata.yaze_version << "\n";
1605 file << "created_by=" << metadata.created_by << "\n";
1606 file << "project_id=" << metadata.project_id << "\n";
1607 file << "tags=" << absl::StrJoin(metadata.tags, ",") << "\n\n";
1608
1609 // Files section
1610 file << "[files]\n";
1611 file << "rom_filename=" << GetRelativePath(rom_filename) << "\n";
1612 file << "rom_backup_folder=" << GetRelativePath(rom_backup_folder) << "\n";
1613 file << "code_folder=" << GetRelativePath(code_folder) << "\n";
1614 file << "assets_folder=" << GetRelativePath(assets_folder) << "\n";
1615 file << "patches_folder=" << GetRelativePath(patches_folder) << "\n";
1616 file << "labels_filename=" << GetRelativePath(labels_filename) << "\n";
1617 file << "symbols_filename=" << GetRelativePath(symbols_filename) << "\n";
1618 if (!overworld_graphics_preview.empty()) {
1619 file << "overworld_graphics_preview="
1621 }
1622 if (!dungeon_tile_patterns.empty()) {
1623 file << "dungeon_tile_patterns="
1625 }
1626 if (!cutscene_shots.empty()) {
1627 file << "cutscene_shots="
1629 }
1630 if (!custom_collision_json.empty()) {
1631 file << "custom_collision_json="
1633 }
1634 file << "output_folder=" << GetRelativePath(output_folder) << "\n";
1635 file << "custom_objects_folder=" << GetRelativePath(custom_objects_folder)
1636 << "\n";
1637 file << "palette_catalog_file="
1639 file << "terrain_catalog_file="
1641 file << "lyra_terrain_file="
1643 file << "lyra_build_request_file="
1645 if (!native_save_producer_file.empty()) {
1646 file << "native_save_producer_file="
1648 }
1649 if (!native_save_transfer_proof_file.empty()) {
1650 file << "native_save_transfer_proof_file="
1652 << "\n";
1653 }
1654 if (!native_save_receipt_file.empty()) {
1655 file << "native_save_receipt_file="
1657 }
1658 file << "hack_manifest_file=" << GetRelativePath(hack_manifest_file) << "\n";
1659 file << "sprite_catalog_file="
1661 file << "sprite_source_root="
1663 file << "additional_roms=" << absl::StrJoin(additional_roms, ",") << "\n\n";
1664
1665 file << "[sprite_assets]\n";
1666 for (size_t i = 0; i < sprite_assets.size(); ++i) {
1667 auto asset = sprite_assets[i];
1668 asset.zsm_path = GetRelativePath(GetAbsolutePath(asset.zsm_path));
1669 for (auto& source : asset.graphics_sources) {
1670 if (source.kind == "bin") {
1671 source.path = GetRelativePath(GetAbsolutePath(source.path));
1672 }
1673 }
1674 file << "asset_" << i << "=" << SpriteAssetToJson(asset).dump() << "\n";
1675 }
1676 file << "\n";
1677
1678 // ROM metadata section
1679 file << "[rom]\n";
1680 file << "role=" << RomRoleToString(rom_metadata.role) << "\n";
1681 file << "expected_hash=" << rom_metadata.expected_hash << "\n";
1682 file << "write_policy=" << RomWritePolicyToString(rom_metadata.write_policy)
1683 << "\n\n";
1684
1685 // Feature flags section
1686 file << "[feature_flags]\n";
1687 file << "load_custom_overworld="
1688 << (feature_flags.overworld.kLoadCustomOverworld ? "true" : "false")
1689 << "\n";
1690 file << "apply_zs_custom_overworld_asm="
1692 : "false")
1693 << "\n";
1694 file << "enable_special_world_expansion="
1696 : "false")
1697 << "\n";
1698 file << "save_with_change_queue="
1699 << (feature_flags.kSaveWithChangeQueue ? "true" : "false") << "\n";
1700 file << "save_dungeon_maps="
1701 << (feature_flags.kSaveDungeonMaps ? "true" : "false") << "\n";
1702 file << "save_overworld_maps="
1703 << (feature_flags.overworld.kSaveOverworldMaps ? "true" : "false")
1704 << "\n";
1705 file << "save_overworld_entrances="
1706 << (feature_flags.overworld.kSaveOverworldEntrances ? "true" : "false")
1707 << "\n";
1708 file << "save_overworld_exits="
1709 << (feature_flags.overworld.kSaveOverworldExits ? "true" : "false")
1710 << "\n";
1711 file << "save_overworld_items="
1712 << (feature_flags.overworld.kSaveOverworldItems ? "true" : "false")
1713 << "\n";
1714 file << "save_overworld_sprites="
1715 << (feature_flags.overworld.kSaveOverworldSprites ? "true" : "false")
1716 << "\n";
1717 file << "save_overworld_properties="
1718 << (feature_flags.overworld.kSaveOverworldProperties ? "true" : "false")
1719 << "\n";
1720 file << "save_dungeon_objects="
1721 << (feature_flags.dungeon.kSaveObjects ? "true" : "false") << "\n";
1722 file << "save_dungeon_sprites="
1723 << (feature_flags.dungeon.kSaveSprites ? "true" : "false") << "\n";
1724 file << "save_dungeon_room_headers="
1725 << (feature_flags.dungeon.kSaveRoomHeaders ? "true" : "false") << "\n";
1726 file << "save_dungeon_torches="
1727 << (feature_flags.dungeon.kSaveTorches ? "true" : "false") << "\n";
1728 file << "save_dungeon_pits="
1729 << (feature_flags.dungeon.kSavePits ? "true" : "false") << "\n";
1730 file << "save_dungeon_blocks="
1731 << (feature_flags.dungeon.kSaveBlocks ? "true" : "false") << "\n";
1732 file << "save_dungeon_collision="
1733 << (feature_flags.dungeon.kSaveCollision ? "true" : "false") << "\n";
1734 file << "save_dungeon_water_fill_zones="
1735 << (feature_flags.dungeon.kSaveWaterFillZones ? "true" : "false")
1736 << "\n";
1737 file << "save_dungeon_chests="
1738 << (feature_flags.dungeon.kSaveChests ? "true" : "false") << "\n";
1739 file << "save_dungeon_pot_items="
1740 << (feature_flags.dungeon.kSavePotItems ? "true" : "false") << "\n";
1741 file << "save_dungeon_entrances="
1742 << (feature_flags.dungeon.kSaveEntrances ? "true" : "false") << "\n";
1743 file << "save_dungeon_palettes="
1744 << (feature_flags.dungeon.kSavePalettes ? "true" : "false") << "\n";
1745 file << "save_graphics_sheet="
1746 << (feature_flags.kSaveGraphicsSheet ? "true" : "false") << "\n";
1747 file << "save_all_palettes="
1748 << (feature_flags.kSaveAllPalettes ? "true" : "false") << "\n";
1749 file << "save_gfx_groups="
1750 << (feature_flags.kSaveGfxGroups ? "true" : "false") << "\n";
1751 file << "save_messages=" << (feature_flags.kSaveMessages ? "true" : "false")
1752 << "\n";
1753 file << "enable_custom_objects="
1754 << (feature_flags.kEnableCustomObjects ? "true" : "false") << "\n\n";
1755
1756 // Workspace settings section
1757 file << "[workspace]\n";
1758 file << "font_global_scale=" << workspace_settings.font_global_scale << "\n";
1759 file << "dark_mode=" << (workspace_settings.dark_mode ? "true" : "false")
1760 << "\n";
1761 file << "ui_theme=" << workspace_settings.ui_theme << "\n";
1762 file << "autosave_enabled="
1763 << (workspace_settings.autosave_enabled ? "true" : "false") << "\n";
1764 file << "autosave_interval_secs=" << workspace_settings.autosave_interval_secs
1765 << "\n";
1766 file << "backup_on_save="
1767 << (workspace_settings.backup_on_save ? "true" : "false") << "\n";
1768 file << "backup_retention_count=" << workspace_settings.backup_retention_count
1769 << "\n";
1770 file << "backup_keep_daily="
1771 << (workspace_settings.backup_keep_daily ? "true" : "false") << "\n";
1772 file << "backup_keep_daily_days=" << workspace_settings.backup_keep_daily_days
1773 << "\n";
1774 file << "show_grid=" << (workspace_settings.show_grid ? "true" : "false")
1775 << "\n";
1776 file << "show_collision="
1777 << (workspace_settings.show_collision ? "true" : "false") << "\n";
1778 file << "prefer_hmagic_names="
1779 << (workspace_settings.prefer_hmagic_names ? "true" : "false") << "\n";
1780 file << "last_layout_preset=" << workspace_settings.last_layout_preset
1781 << "\n";
1782 file << "saved_layouts="
1783 << absl::StrJoin(workspace_settings.saved_layouts, ",") << "\n";
1784 file << "recent_files=" << absl::StrJoin(workspace_settings.recent_files, ",")
1785 << "\n\n";
1786
1787 // Dungeon overlay settings section
1788 auto track_tiles = dungeon_overlay.track_tiles;
1789 if (track_tiles.empty()) {
1790 for (uint16_t tile = 0xB0; tile <= 0xBE; ++tile) {
1791 track_tiles.push_back(tile);
1792 }
1793 }
1794 auto track_stop_tiles = dungeon_overlay.track_stop_tiles;
1795 if (track_stop_tiles.empty()) {
1796 track_stop_tiles = {0xB7, 0xB8, 0xB9, 0xBA};
1797 }
1798 auto track_switch_tiles = dungeon_overlay.track_switch_tiles;
1799 if (track_switch_tiles.empty()) {
1800 track_switch_tiles = {0xD0, 0xD1, 0xD2, 0xD3};
1801 }
1802 auto track_object_ids = dungeon_overlay.track_object_ids;
1803 if (track_object_ids.empty()) {
1804 track_object_ids = {0x31};
1805 }
1806 auto minecart_sprite_ids = dungeon_overlay.minecart_sprite_ids;
1807 if (minecart_sprite_ids.empty()) {
1808 minecart_sprite_ids = {0xA3};
1809 }
1810 file << "[dungeon_overlay]\n";
1811 file << "track_tiles=" << FormatHexUintList(track_tiles) << "\n";
1812 file << "track_stop_tiles=" << FormatHexUintList(track_stop_tiles) << "\n";
1813 file << "track_switch_tiles=" << FormatHexUintList(track_switch_tiles)
1814 << "\n";
1815 file << "track_object_ids=" << FormatHexUintList(track_object_ids) << "\n";
1816 file << "minecart_sprite_ids=" << FormatHexUintList(minecart_sprite_ids)
1817 << "\n\n";
1818
1819 if (!graphics_sheets.empty()) {
1820 file << "[graphics_sheets]\n";
1821 file << "reserved_sheets="
1822 << FormatHexUintList(graphics_sheets.reserved_sheets) << "\n";
1823 file << "flagged_sheets="
1824 << FormatHexUintList(graphics_sheets.flagged_sheets) << "\n";
1825 file << "reserved_blocks="
1826 << FormatHexUintListMap(graphics_sheets.reserved_blocks) << "\n\n";
1827 }
1828
1830 file << "[atlas]\n";
1831 file << "annotations=" << absl::StrJoin(atlas.annotations, ",") << "\n";
1832 file << "defaults=" << atlas.defaults << "\n";
1833 file << "symbols=" << atlas.symbols << "\n";
1834 file << "export_folder=" << atlas.export_folder << "\n\n";
1835 }
1836
1838 file << "[planning]\n";
1839 file << "file=" << planning.file << "\n\n";
1840 }
1841
1842 if (!rom_address_overrides.addresses.empty()) {
1843 file << "[rom_addresses]\n";
1844 for (const auto& [key, value] : rom_address_overrides.addresses) {
1845 file << key << "=" << FormatHexUint32(value) << "\n";
1846 }
1847 file << "\n";
1848 }
1849
1850 if (!custom_object_files.empty()) {
1851 file << "[custom_objects]\n";
1852 for (const auto& [object_id, files] : custom_object_files) {
1853 file << absl::StrFormat("object_0x%X", object_id) << "="
1854 << absl::StrJoin(files, ",") << "\n";
1855 }
1856 file << "\n";
1857 }
1858
1859 // AI Agent settings section
1860 file << "[agent_settings]\n";
1861 file << "ai_provider=" << agent_settings.ai_provider << "\n";
1862 file << "ai_model=" << agent_settings.ai_model << "\n";
1863 file << "ollama_host=" << agent_settings.ollama_host << "\n";
1864 file << "gemini_api_key=" << agent_settings.gemini_api_key << "\n";
1865 file << "custom_system_prompt="
1867 << "\n";
1868 file << "use_custom_prompt="
1869 << (agent_settings.use_custom_prompt ? "true" : "false") << "\n";
1870 file << "show_reasoning="
1871 << (agent_settings.show_reasoning ? "true" : "false") << "\n";
1872 file << "verbose=" << (agent_settings.verbose ? "true" : "false") << "\n";
1873 file << "max_tool_iterations=" << agent_settings.max_tool_iterations << "\n";
1874 file << "max_retry_attempts=" << agent_settings.max_retry_attempts << "\n";
1875 file << "temperature=" << agent_settings.temperature << "\n";
1876 file << "top_p=" << agent_settings.top_p << "\n";
1877 file << "max_output_tokens=" << agent_settings.max_output_tokens << "\n";
1878 file << "stream_responses="
1879 << (agent_settings.stream_responses ? "true" : "false") << "\n";
1880 file << "favorite_models="
1881 << absl::StrJoin(agent_settings.favorite_models, ",") << "\n";
1882 file << "model_chain=" << absl::StrJoin(agent_settings.model_chain, ",")
1883 << "\n";
1884 file << "chain_mode=" << agent_settings.chain_mode << "\n";
1885 file << "enable_tool_resources="
1886 << (agent_settings.enable_tool_resources ? "true" : "false") << "\n";
1887 file << "enable_tool_dungeon="
1888 << (agent_settings.enable_tool_dungeon ? "true" : "false") << "\n";
1889 file << "enable_tool_overworld="
1890 << (agent_settings.enable_tool_overworld ? "true" : "false") << "\n";
1891 file << "enable_tool_messages="
1892 << (agent_settings.enable_tool_messages ? "true" : "false") << "\n";
1893 file << "enable_tool_dialogue="
1894 << (agent_settings.enable_tool_dialogue ? "true" : "false") << "\n";
1895 file << "enable_tool_gui="
1896 << (agent_settings.enable_tool_gui ? "true" : "false") << "\n";
1897 file << "enable_tool_music="
1898 << (agent_settings.enable_tool_music ? "true" : "false") << "\n";
1899 file << "enable_tool_sprite="
1900 << (agent_settings.enable_tool_sprite ? "true" : "false") << "\n";
1901 file << "enable_tool_emulator="
1902 << (agent_settings.enable_tool_emulator ? "true" : "false") << "\n";
1903 file << "enable_tool_memory_inspector="
1904 << (agent_settings.enable_tool_memory_inspector ? "true" : "false")
1905 << "\n";
1906 file << "builder_blueprint_path=" << agent_settings.builder_blueprint_path
1907 << "\n\n";
1908
1909 // Custom keybindings section
1911 file << "[keybindings]\n";
1912 for (const auto& [key, value] : workspace_settings.custom_keybindings) {
1913 file << key << "=" << value << "\n";
1914 }
1915 file << "\n";
1916 }
1917
1918 // Editor visibility section
1920 file << "[editor_visibility]\n";
1921 for (const auto& [key, value] : workspace_settings.editor_visibility) {
1922 file << key << "=" << (value ? "true" : "false") << "\n";
1923 }
1924 file << "\n";
1925 }
1926
1927 // Resource labels sections
1928 for (const auto& [type, labels] : resource_labels) {
1929 if (!labels.empty()) {
1930 file << "[labels_" << type << "]\n";
1931 for (const auto& [key, value] : labels) {
1932 file << key << "=" << value << "\n";
1933 }
1934 file << "\n";
1935 }
1936 }
1937
1938 // Build settings section
1939 file << "[build]\n";
1940 file << "build_script=" << build_script << "\n";
1941 file << "output_folder=" << GetRelativePath(output_folder) << "\n";
1942 file << "git_repository=" << git_repository << "\n";
1943 file << "track_changes=" << (track_changes ? "true" : "false") << "\n";
1944 file << "build_configurations=" << absl::StrJoin(build_configurations, ",")
1945 << "\n";
1946 file << "build_target=" << build_target << "\n";
1947 file << "asm_entry_point=" << asm_entry_point << "\n";
1948 file << "asm_sources=" << absl::StrJoin(asm_sources, ",") << "\n";
1949 file << "last_build_hash=" << last_build_hash << "\n";
1950 file << "build_number=" << build_number << "\n\n";
1951
1952 // Music persistence section (for WASM/offline state)
1953 file << "[music]\n";
1954 file << "persist_custom_music="
1955 << (music_persistence.persist_custom_music ? "true" : "false") << "\n";
1956 file << "storage_key=" << music_persistence.storage_key << "\n";
1957 file << "last_saved_at=" << music_persistence.last_saved_at << "\n\n";
1958
1959 // ZScream compatibility section
1960 if (!zscream_project_file.empty()) {
1961 file << "[zscream_compatibility]\n";
1962 file << "original_project_file=" << zscream_project_file << "\n";
1963 for (const auto& [key, value] : zscream_mappings) {
1964 file << key << "=" << value << "\n";
1965 }
1966 file << "\n";
1967 }
1968
1969 file << "# End of YAZE Project File\n";
1970
1971 // Refuse to WRITE what ParseFromString refuses to READ. Values are streamed
1972 // verbatim — metadata.description and resource labels injected from
1973 // hack_manifest.json among them — so a lone CR pasted from an old-Mac
1974 // source would round-trip through Save() and leave the project permanently
1975 // unopenable, with the previous good file already overwritten. Failing here
1976 // keeps that file intact.
1977 std::string serialized =
1978 MergeProjectFilePassthrough(file.str(), file_passthrough);
1979 if (ContainsLoneCarriageReturn(serialized)) {
1980 return absl::InvalidArgumentError(
1981 "Project contains a lone carriage return in a value; refusing to "
1982 "write a descriptor that could not be read back");
1983 }
1984 return serialized;
1985}
1986
1987absl::Status YazeProject::ParseFromString(const std::string& content) {
1988 if (ContainsLoneCarriageReturn(content)) {
1989 return absl::InvalidArgumentError(
1990 "Project file contains unsupported lone carriage returns");
1991 }
1992
1997 sprite_catalog_file.clear();
1998 sprite_source_root.clear();
1999 sprite_assets.clear();
2000 terrain_catalog_file.clear();
2001 lyra_terrain_file.clear();
2006 palette_catalog_file.clear();
2007 dungeon_tile_patterns.clear();
2009 atlas = AtlasSettings{};
2011 std::istringstream stream(content);
2012 std::string line;
2013 std::string current_section;
2014 ProjectFilePassthrough::Section* passthrough_section =
2015 &PassthroughSection(file_passthrough, "");
2016
2017 while (std::getline(stream, line)) {
2018 // std::getline() consumes '\n' but preserves the '\r' in CRLF input.
2019 // Normalize that line ending before comment, section, or value parsing so
2020 // project safety flags have identical behavior across platforms.
2021 if (!line.empty() && line.back() == '\r') {
2022 line.pop_back();
2023 }
2024
2025 if (line.find_first_not_of(" \t") == std::string::npos)
2026 continue;
2027
2028 if (line[0] == '#') {
2029 if (!IsGeneratedProjectComment(line, current_section.empty())) {
2030 passthrough_section->lines.push_back(
2032 }
2033 continue;
2034 }
2035
2036 if (line.front() == '[' && line.back() == ']') {
2037 current_section = line.substr(1, line.length() - 2);
2038 passthrough_section =
2039 &PassthroughSection(file_passthrough, current_section);
2040 passthrough_section->known = IsKnownProjectSection(current_section);
2041 if (current_section == "atlas") {
2042 atlas.section_present = true;
2043 } else if (current_section == "planning") {
2045 }
2046 continue;
2047 }
2048
2049 auto [key, value] = ParseKeyValue(line);
2050 // False when this build does not model the line; it is then kept verbatim
2051 // and written back on save.
2052 bool understood = !key.empty();
2053 if (key.empty()) {
2054 // No "key=value": nothing to parse.
2055 } else if (current_section == "project") {
2056 if (key == "name")
2057 name = value;
2058 else if (key == "description")
2059 metadata.description = value;
2060 else if (key == "author")
2061 metadata.author = value;
2062 else if (key == "license")
2063 metadata.license = value;
2064 else if (key == "version")
2065 metadata.version = value;
2066 else if (key == "created_date")
2067 metadata.created_date = value;
2068 else if (key == "last_modified")
2069 metadata.last_modified = value;
2070 else if (key == "yaze_version")
2071 metadata.yaze_version = value;
2072 else if (key == "created_by")
2073 metadata.created_by = value;
2074 else if (key == "tags")
2075 metadata.tags = ParseStringList(value);
2076 else if (key == "project_id")
2077 metadata.project_id = value;
2078 else
2079 understood = false;
2080 } else if (current_section == "files") {
2081 if (key == "rom_filename")
2082 rom_filename = value;
2083 else if (key == "rom_backup_folder")
2084 rom_backup_folder = value;
2085 else if (key == "code_folder")
2086 code_folder = value;
2087 else if (key == "assets_folder")
2088 assets_folder = value;
2089 else if (key == "patches_folder")
2090 patches_folder = value;
2091 else if (key == "labels_filename")
2092 labels_filename = value;
2093 else if (key == "symbols_filename")
2094 symbols_filename = value;
2095 else if (key == "overworld_graphics_preview")
2097 else if (key == "dungeon_tile_patterns")
2098 dungeon_tile_patterns = value;
2099 else if (key == "cutscene_shots")
2100 cutscene_shots = value;
2101 else if (key == "custom_collision_json")
2102 custom_collision_json = value;
2103 else if (key == "output_folder")
2104 output_folder = value;
2105 else if (key == "custom_objects_folder")
2106 custom_objects_folder = value;
2107 else if (key == "palette_catalog_file")
2108 palette_catalog_file = value;
2109 else if (key == "terrain_catalog_file")
2110 terrain_catalog_file = value;
2111 else if (key == "lyra_terrain_file")
2112 lyra_terrain_file = value;
2113 else if (key == "lyra_build_request_file")
2115 else if (key == "native_save_producer_file")
2117 else if (key == "native_save_transfer_proof_file")
2119 else if (key == "native_save_receipt_file")
2121 else if (key == "hack_manifest_file")
2122 hack_manifest_file = value;
2123 else if (key == "sprite_catalog_file")
2124 sprite_catalog_file = value;
2125 else if (key == "sprite_source_root")
2126 sprite_source_root = value;
2127 else if (key == "additional_roms")
2128 additional_roms = ParseStringList(value);
2129 else
2130 understood = false;
2131 } else if (current_section == "sprite_assets") {
2132 if (key.rfind("asset_", 0) != 0 || value.size() > 65536 ||
2133 sprite_assets.size() >= 4096)
2134 return absl::InvalidArgumentError("Invalid sprite_assets section");
2135 auto asset = ParseSpriteAssetBinding(value);
2136 if (!asset.ok())
2137 return asset.status();
2138 for (const auto& prior : sprite_assets)
2139 if (prior.zsm_path == asset->zsm_path)
2140 return absl::InvalidArgumentError("Duplicate sprite asset path");
2141 sprite_assets.push_back(std::move(*asset));
2142 } else if (current_section == "rom") {
2143 if (key == "role")
2145 else if (key == "expected_hash")
2147 else if (key == "write_policy")
2149 else
2150 understood = false;
2151 } else if (current_section == "feature_flags") {
2152 if (key == "load_custom_overworld")
2153 feature_flags.overworld.kLoadCustomOverworld = ParseBool(value);
2154 else if (key == "apply_zs_custom_overworld_asm")
2156 else if (key == "enable_special_world_expansion")
2158 else if (key == "save_with_change_queue")
2159 feature_flags.kSaveWithChangeQueue = ParseBool(value);
2160 else if (key == "save_dungeon_maps")
2161 feature_flags.kSaveDungeonMaps = ParseBool(value);
2162 else if (key == "save_overworld_maps")
2163 feature_flags.overworld.kSaveOverworldMaps = ParseBool(value);
2164 else if (key == "save_overworld_entrances")
2166 else if (key == "save_overworld_exits")
2167 feature_flags.overworld.kSaveOverworldExits = ParseBool(value);
2168 else if (key == "save_overworld_items")
2169 feature_flags.overworld.kSaveOverworldItems = ParseBool(value);
2170 else if (key == "save_overworld_sprites")
2172 else if (key == "save_overworld_properties")
2174 else if (key == "save_dungeon_objects")
2175 feature_flags.dungeon.kSaveObjects = ParseBool(value);
2176 else if (key == "save_dungeon_sprites")
2177 feature_flags.dungeon.kSaveSprites = ParseBool(value);
2178 else if (key == "save_dungeon_room_headers")
2179 feature_flags.dungeon.kSaveRoomHeaders = ParseBool(value);
2180 else if (key == "save_dungeon_torches")
2181 feature_flags.dungeon.kSaveTorches = ParseBool(value);
2182 else if (key == "save_dungeon_pits")
2183 feature_flags.dungeon.kSavePits = ParseBool(value);
2184 else if (key == "save_dungeon_blocks")
2185 feature_flags.dungeon.kSaveBlocks = ParseBool(value);
2186 else if (key == "save_dungeon_collision")
2187 feature_flags.dungeon.kSaveCollision = ParseBool(value);
2188 else if (key == "save_dungeon_water_fill_zones")
2189 feature_flags.dungeon.kSaveWaterFillZones = ParseBool(value);
2190 else if (key == "save_dungeon_chests")
2191 feature_flags.dungeon.kSaveChests = ParseBool(value);
2192 else if (key == "save_dungeon_pot_items")
2193 feature_flags.dungeon.kSavePotItems = ParseBool(value);
2194 else if (key == "save_dungeon_entrances")
2195 feature_flags.dungeon.kSaveEntrances = ParseBool(value);
2196 else if (key == "save_dungeon_palettes")
2197 feature_flags.dungeon.kSavePalettes = ParseBool(value);
2198 else if (key == "save_graphics_sheet")
2199 feature_flags.kSaveGraphicsSheet = ParseBool(value);
2200 else if (key == "save_all_palettes")
2201 feature_flags.kSaveAllPalettes = ParseBool(value);
2202 else if (key == "save_gfx_groups")
2203 feature_flags.kSaveGfxGroups = ParseBool(value);
2204 else if (key == "save_messages")
2205 feature_flags.kSaveMessages = ParseBool(value);
2206 else if (key == "enable_custom_objects")
2207 feature_flags.kEnableCustomObjects = ParseBool(value);
2208 else
2209 understood = false;
2210 } else if (current_section == "workspace") {
2211 if (key == "font_global_scale")
2212 workspace_settings.font_global_scale = ParseFloat(value);
2213 else if (key == "dark_mode")
2214 workspace_settings.dark_mode = ParseBool(value);
2215 else if (key == "ui_theme")
2217 else if (key == "autosave_enabled")
2218 workspace_settings.autosave_enabled = ParseBool(value);
2219 else if (key == "autosave_interval_secs")
2220 workspace_settings.autosave_interval_secs = ParseFloat(value);
2221 else if (key == "backup_on_save")
2222 workspace_settings.backup_on_save = ParseBool(value);
2223 else if (key == "backup_retention_count")
2224 workspace_settings.backup_retention_count = std::stoi(value);
2225 else if (key == "backup_keep_daily")
2226 workspace_settings.backup_keep_daily = ParseBool(value);
2227 else if (key == "backup_keep_daily_days")
2228 workspace_settings.backup_keep_daily_days = std::stoi(value);
2229 else if (key == "show_grid")
2230 workspace_settings.show_grid = ParseBool(value);
2231 else if (key == "show_collision")
2232 workspace_settings.show_collision = ParseBool(value);
2233 else if (key == "prefer_hmagic_names")
2234 workspace_settings.prefer_hmagic_names = ParseBool(value);
2235 else if (key == "last_layout_preset")
2237 else if (key == "saved_layouts")
2238 workspace_settings.saved_layouts = ParseStringList(value);
2239 else if (key == "recent_files")
2240 workspace_settings.recent_files = ParseStringList(value);
2241 else
2242 understood = false;
2243 } else if (current_section == "dungeon_overlay") {
2244 if (key == "track_tiles")
2245 dungeon_overlay.track_tiles = ParseHexUintList(value);
2246 else if (key == "track_stop_tiles")
2247 dungeon_overlay.track_stop_tiles = ParseHexUintList(value);
2248 else if (key == "track_switch_tiles")
2249 dungeon_overlay.track_switch_tiles = ParseHexUintList(value);
2250 else if (key == "track_object_ids")
2251 dungeon_overlay.track_object_ids = ParseHexUintList(value);
2252 else if (key == "minecart_sprite_ids")
2253 dungeon_overlay.minecart_sprite_ids = ParseHexUintList(value);
2254 else
2255 understood = false;
2256 } else if (current_section == "graphics_sheets") {
2257 if (key == "reserved_sheets")
2258 graphics_sheets.reserved_sheets = ParseHexUintList(value);
2259 else if (key == "flagged_sheets")
2260 graphics_sheets.flagged_sheets = ParseHexUintList(value);
2261 else if (key == "reserved_blocks")
2262 graphics_sheets.reserved_blocks = ParseHexUintListMap(value);
2263 else
2264 understood = false;
2265 } else if (current_section == "atlas") {
2266 if (key == "annotations")
2267 atlas.annotations = ParseStringList(value);
2268 else if (key == "defaults")
2269 atlas.defaults = value.empty() ? "vanilla" : value;
2270 else if (key == "symbols")
2271 atlas.symbols = value;
2272 else if (key == "export_folder")
2273 atlas.export_folder = value;
2274 else
2275 understood = false;
2276 } else if (current_section == "planning") {
2277 if (key == "file")
2278 planning.file = value;
2279 else
2280 understood = false;
2281 } else if (current_section == "rom_addresses") {
2282 auto parsed = ParseHexUint32(value);
2283 if (parsed.has_value()) {
2284 rom_address_overrides.addresses[key] = *parsed;
2285 } else {
2286 understood = false;
2287 }
2288 } else if (current_section == "custom_objects") {
2289 std::string id_token = key;
2290 if (absl::StartsWith(id_token, "object_")) {
2291 id_token = id_token.substr(7);
2292 }
2293 auto parsed = ParseHexUint32(id_token);
2294 if (parsed.has_value()) {
2295 custom_object_files[static_cast<int>(*parsed)] =
2296 ParsePositionalStringList(value);
2297 } else {
2298 understood = false;
2299 }
2300 } else if (current_section == "agent_settings") {
2301 if (key == "ai_provider")
2303 else if (key == "ai_model")
2304 agent_settings.ai_model = value;
2305 else if (key == "ollama_host")
2307 else if (key == "gemini_api_key")
2309 else if (key == "custom_system_prompt")
2311 else if (key == "use_custom_prompt")
2312 agent_settings.use_custom_prompt = ParseBool(value);
2313 else if (key == "show_reasoning")
2314 agent_settings.show_reasoning = ParseBool(value);
2315 else if (key == "verbose")
2316 agent_settings.verbose = ParseBool(value);
2317 else if (key == "max_tool_iterations")
2318 agent_settings.max_tool_iterations = std::stoi(value);
2319 else if (key == "max_retry_attempts")
2320 agent_settings.max_retry_attempts = std::stoi(value);
2321 else if (key == "temperature")
2322 agent_settings.temperature = ParseFloat(value);
2323 else if (key == "top_p")
2324 agent_settings.top_p = ParseFloat(value);
2325 else if (key == "max_output_tokens")
2326 agent_settings.max_output_tokens = std::stoi(value);
2327 else if (key == "stream_responses")
2328 agent_settings.stream_responses = ParseBool(value);
2329 else if (key == "favorite_models")
2330 agent_settings.favorite_models = ParseStringList(value);
2331 else if (key == "model_chain")
2332 agent_settings.model_chain = ParseStringList(value);
2333 else if (key == "chain_mode")
2334 agent_settings.chain_mode = std::stoi(value);
2335 else if (key == "enable_tool_resources")
2336 agent_settings.enable_tool_resources = ParseBool(value);
2337 else if (key == "enable_tool_dungeon")
2338 agent_settings.enable_tool_dungeon = ParseBool(value);
2339 else if (key == "enable_tool_overworld")
2340 agent_settings.enable_tool_overworld = ParseBool(value);
2341 else if (key == "enable_tool_messages")
2342 agent_settings.enable_tool_messages = ParseBool(value);
2343 else if (key == "enable_tool_dialogue")
2344 agent_settings.enable_tool_dialogue = ParseBool(value);
2345 else if (key == "enable_tool_gui")
2346 agent_settings.enable_tool_gui = ParseBool(value);
2347 else if (key == "enable_tool_music")
2348 agent_settings.enable_tool_music = ParseBool(value);
2349 else if (key == "enable_tool_sprite")
2350 agent_settings.enable_tool_sprite = ParseBool(value);
2351 else if (key == "enable_tool_emulator")
2352 agent_settings.enable_tool_emulator = ParseBool(value);
2353 else if (key == "enable_tool_memory_inspector")
2355 else if (key == "builder_blueprint_path")
2357 else
2358 understood = false;
2359 } else if (current_section == "build") {
2360 if (key == "build_script")
2361 build_script = value;
2362 else if (key == "output_folder")
2363 output_folder = value;
2364 else if (key == "git_repository")
2365 git_repository = value;
2366 else if (key == "track_changes")
2367 track_changes = ParseBool(value);
2368 else if (key == "build_configurations")
2369 build_configurations = ParseStringList(value);
2370 else if (key == "build_target")
2371 build_target = value;
2372 else if (key == "asm_entry_point")
2373 asm_entry_point = value;
2374 else if (key == "asm_sources")
2375 asm_sources = ParseStringList(value);
2376 else if (key == "last_build_hash")
2377 last_build_hash = value;
2378 else if (key == "build_number")
2379 build_number = std::stoi(value);
2380 else
2381 understood = false;
2382 } else if (current_section.rfind("labels_", 0) == 0) {
2383 std::string label_type = current_section.substr(7);
2384 resource_labels[label_type][key] = value;
2385 } else if (current_section == "keybindings") {
2387 } else if (current_section == "editor_visibility") {
2388 workspace_settings.editor_visibility[key] = ParseBool(value);
2389 } else if (current_section == "zscream_compatibility") {
2390 if (key == "original_project_file")
2391 zscream_project_file = value;
2392 else
2393 zscream_mappings[key] = value;
2394 } else if (current_section == "music") {
2395 if (key == "persist_custom_music")
2396 music_persistence.persist_custom_music = ParseBool(value);
2397 else if (key == "storage_key")
2399 else if (key == "last_saved_at")
2401 else
2402 understood = false;
2403 } else {
2404 understood = false; // A section this build does not know.
2405 }
2406 passthrough_section->lines.push_back(
2407 understood
2412 }
2413
2414 MigrateGfxGroupLabelKeys(resource_labels);
2415
2416 if (metadata.project_id.empty()) {
2418 }
2419 if (metadata.created_by.empty()) {
2420 metadata.created_by = "YAZE";
2421 }
2422 if (music_persistence.storage_key.empty()) {
2424 }
2425
2426 return absl::OkStatus();
2427}
2428
2429absl::Status YazeProject::LoadFromYazeFormat(const std::string& project_path) {
2430#ifdef __EMSCRIPTEN__
2431 auto storage_key = MakeStorageKey("project");
2432 auto storage_or = platform::WasmStorage::LoadProject(storage_key);
2433 if (storage_or.ok()) {
2434 return ParseFromString(storage_or.value());
2435 }
2436#endif // __EMSCRIPTEN__
2437
2438 std::ifstream file(project_path);
2439 if (!file.is_open()) {
2440 return absl::InvalidArgumentError(
2441 absl::StrFormat("Cannot open project file: %s", project_path));
2442 }
2443
2444 std::stringstream buffer;
2445 buffer << file.rdbuf();
2446 file.close();
2447 return ParseFromString(buffer.str());
2448}
2449
2450absl::Status YazeProject::SaveToYazeFormat(bool replace_existing) {
2451 // Update last modified timestamp
2452 auto now = std::chrono::system_clock::now();
2453 auto time_t = std::chrono::system_clock::to_time_t(now);
2454 std::stringstream ss;
2455 ss << std::put_time(std::localtime(&time_t), "%Y-%m-%d %H:%M:%S");
2456 metadata.last_modified = ss.str();
2457 if (music_persistence.storage_key.empty()) {
2459 }
2460
2461 // Ensure we serialize clean relative paths even if the user edited fields
2462 // into relative form (and avoid relying on cwd when opening later).
2464
2465 ASSIGN_OR_RETURN(auto serialized, SerializeToString());
2466
2467#ifdef __EMSCRIPTEN__
2468 auto storage_status = platform::WasmStorage::SaveProject(
2469 MakeStorageKey("project"), serialized, replace_existing);
2470 if (!storage_status.ok()) {
2471 return storage_status;
2472 }
2473#else
2474 if (!filepath.empty()) {
2476 WriteProjectFileAtomically(filepath, serialized, replace_existing));
2477 }
2478#endif
2479
2480 return absl::OkStatus();
2481}
2482
2484 const std::string& zscream_project_path) {
2485 // Basic ZScream project import (to be expanded based on ZScream format)
2486 zscream_project_file = zscream_project_path;
2488
2489 // Extract project name from path
2490 std::filesystem::path zs_path(zscream_project_path);
2491 name = zs_path.stem().string() + "_imported";
2492
2493 // Set up basic mapping for common fields
2494 zscream_mappings["rom_file"] = "rom_filename";
2495 zscream_mappings["source_code"] = "code_folder";
2496 zscream_mappings["project_name"] = "name";
2497
2499
2500 // TODO: Implement actual ZScream format parsing when format is known
2501 // For now, just create a project structure that can be manually configured
2502
2503 return absl::OkStatus();
2504}
2505
2506absl::Status YazeProject::ExportForZScream(const std::string& target_path) {
2507 // Create a simplified project file that ZScream might understand
2508 std::ofstream file(target_path);
2509 if (!file.is_open()) {
2510 return absl::InvalidArgumentError(
2511 absl::StrFormat("Cannot create ZScream project file: %s", target_path));
2512 }
2513
2514 // Write in a simple format that ZScream might understand
2515 file << "# ZScream Compatible Project File\n";
2516 file << "# Exported from YAZE " << metadata.yaze_version << "\n\n";
2517 file << "name=" << name << "\n";
2518 file << "rom_file=" << rom_filename << "\n";
2519 file << "source_code=" << code_folder << "\n";
2520 file << "description=" << metadata.description << "\n";
2521 file << "author=" << metadata.author << "\n";
2522 file << "created_with=YAZE " << metadata.yaze_version << "\n";
2523
2524 file.close();
2525 return absl::OkStatus();
2526}
2527
2529 // Consolidated loading of all settings from project file
2530 // This replaces scattered config loading throughout the application
2532}
2533
2535 // Consolidated saving of all settings to project file
2536 return SaveToYazeFormat();
2537}
2538
2541 return Save();
2542}
2543
2544absl::Status YazeProject::Validate() const {
2545 std::vector<std::string> errors;
2546
2547 if (name.empty())
2548 errors.push_back("Project name is required");
2549 if (filepath.empty())
2550 errors.push_back("Project file path is required");
2551 if (rom_filename.empty())
2552 errors.push_back("ROM file is required");
2553
2554#ifndef __EMSCRIPTEN__
2555 // Check if files exist
2556 if (!rom_filename.empty() &&
2557 !std::filesystem::exists(GetAbsolutePath(rom_filename))) {
2558 errors.push_back("ROM file does not exist: " + rom_filename);
2559 }
2560
2561 if (!code_folder.empty() &&
2562 !std::filesystem::exists(GetAbsolutePath(code_folder))) {
2563 errors.push_back("Code folder does not exist: " + code_folder);
2564 }
2565
2566 if (!labels_filename.empty() &&
2567 !std::filesystem::exists(GetAbsolutePath(labels_filename))) {
2568 errors.push_back("Labels file does not exist: " + labels_filename);
2569 }
2570
2571 if (!hack_manifest_file.empty()) {
2572 if (!std::filesystem::exists(GetAbsolutePath(hack_manifest_file))) {
2573 errors.push_back("Hack manifest file does not exist: " +
2575 } else if (!hack_manifest.loaded()) {
2576 errors.push_back("Hack manifest file failed to load: " +
2578 }
2579 }
2580#endif // __EMSCRIPTEN__
2581
2582 if (!errors.empty()) {
2583 return absl::InvalidArgumentError(absl::StrJoin(errors, "; "));
2584 }
2585
2586 return absl::OkStatus();
2587}
2588
2589std::vector<std::string> YazeProject::GetMissingFiles() const {
2590 std::vector<std::string> missing;
2591
2592#ifndef __EMSCRIPTEN__
2593 if (!rom_filename.empty() &&
2594 !std::filesystem::exists(GetAbsolutePath(rom_filename))) {
2595 missing.push_back(rom_filename);
2596 }
2597 if (!labels_filename.empty() &&
2598 !std::filesystem::exists(GetAbsolutePath(labels_filename))) {
2599 missing.push_back(labels_filename);
2600 }
2601 if (!symbols_filename.empty() &&
2602 !std::filesystem::exists(GetAbsolutePath(symbols_filename))) {
2603 missing.push_back(symbols_filename);
2604 }
2605 if (!hack_manifest_file.empty() &&
2606 !std::filesystem::exists(GetAbsolutePath(hack_manifest_file))) {
2607 missing.push_back(hack_manifest_file);
2608 }
2609#endif // __EMSCRIPTEN__
2610
2611 return missing;
2612}
2613
2615#ifdef __EMSCRIPTEN__
2616 // In the web build, filesystem layout is virtual; nothing to repair eagerly.
2617 return absl::OkStatus();
2618#else
2619 // Create missing directories
2620 std::vector<std::string> folders = {code_folder, assets_folder,
2623
2624 for (const auto& folder : folders) {
2625 if (!folder.empty()) {
2626 std::filesystem::path abs_path = GetAbsolutePath(folder);
2627 if (!std::filesystem::exists(abs_path)) {
2628 std::filesystem::create_directories(abs_path);
2629 }
2630 }
2631 }
2632
2633 // Create missing files with defaults
2634 if (!labels_filename.empty()) {
2635 std::filesystem::path abs_labels = GetAbsolutePath(labels_filename);
2636 if (!std::filesystem::exists(abs_labels)) {
2637 std::ofstream labels_file(abs_labels);
2638 labels_file << "# yaze Resource Labels\n";
2639 labels_file << "# Format: [type] key=value\n\n";
2640 labels_file.close();
2641 }
2642 }
2643
2644 return absl::OkStatus();
2645#endif
2646}
2647
2648std::string YazeProject::GetDisplayName() const {
2649 if (!metadata.description.empty()) {
2650 return metadata.description;
2651 }
2652 return name.empty() ? "Untitled Project" : name;
2653}
2654
2656 const std::string& absolute_path) const {
2657 if (absolute_path.empty() || filepath.empty())
2658 return absolute_path;
2659
2660 std::filesystem::path project_dir =
2661 std::filesystem::path(filepath).parent_path();
2662 std::filesystem::path abs_path(absolute_path);
2663
2664 try {
2665 std::filesystem::path relative =
2666 std::filesystem::relative(abs_path.lexically_normal(), project_dir);
2667 // Persist relative paths in a platform-neutral format for project files.
2668 return relative.generic_string();
2669 } catch (...) {
2670 // Return normalized absolute path if relative conversion fails.
2671 return abs_path.lexically_normal().generic_string();
2672 }
2673}
2674
2676 const std::string& relative_path) const {
2677 if (relative_path.empty() || filepath.empty())
2678 return relative_path;
2679
2680 std::filesystem::path project_dir =
2681 std::filesystem::path(filepath).parent_path();
2682 std::filesystem::path abs_path(relative_path);
2683 if (abs_path.is_absolute()) {
2684 abs_path = abs_path.lexically_normal();
2685 abs_path.make_preferred();
2686 return abs_path.string();
2687 }
2688 abs_path = (project_dir / abs_path).lexically_normal();
2689 abs_path.make_preferred();
2690
2691 return abs_path.string();
2692}
2693
2695#ifdef __EMSCRIPTEN__
2696 // Web builds rely on a virtual filesystem and often use relative paths.
2697 return;
2698#endif
2699 if (filepath.empty()) {
2700 return;
2701 }
2702
2703 auto normalize = [this](std::string* path) {
2704 if (!path || path->empty()) {
2705 return;
2706 }
2707 *path = GetAbsolutePath(*path);
2708 };
2709
2710 normalize(&rom_filename);
2711 normalize(&rom_backup_folder);
2712 normalize(&code_folder);
2713 normalize(&assets_folder);
2714 normalize(&patches_folder);
2715 normalize(&labels_filename);
2716 normalize(&symbols_filename);
2717 normalize(&custom_objects_folder);
2718 normalize(&hack_manifest_file);
2719 normalize(&sprite_catalog_file);
2720 normalize(&sprite_source_root);
2721 normalize(&overworld_graphics_preview);
2722 normalize(&dungeon_tile_patterns);
2723 normalize(&terrain_catalog_file);
2724 normalize(&lyra_terrain_file);
2725 normalize(&lyra_build_request_file);
2726 normalize(&native_save_producer_file);
2728 normalize(&native_save_receipt_file);
2729 normalize(&palette_catalog_file);
2730 for (auto& asset : sprite_assets) {
2731 normalize(&asset.zsm_path);
2732 for (auto& source : asset.graphics_sources) {
2733 if (source.kind == "bin") {
2734 normalize(&source.path);
2735 }
2736 }
2737 }
2738 normalize(&output_folder);
2739
2740 for (auto& rom_path : additional_roms) {
2741 if (!rom_path.empty()) {
2742 rom_path = GetAbsolutePath(rom_path);
2743 }
2744 }
2745}
2746
2747std::vector<std::string> YazeProject::AtlasAnnotationPaths() const {
2748 std::vector<std::string> paths;
2749 paths.reserve(atlas.annotations.size());
2750 for (const auto& path : atlas.annotations) {
2751 paths.push_back(GetAbsolutePath(path));
2752 }
2753 return paths;
2754}
2755
2758}
2759
2762}
2763
2765 return name.empty() && rom_filename.empty() && code_folder.empty();
2766}
2767
2769 const std::string& project_path) {
2770 // TODO: Implement ZScream format parsing when format specification is
2771 // available For now, create a basic project that can be manually configured
2772
2773 std::filesystem::path zs_path(project_path);
2774 name = zs_path.stem().string() + "_imported";
2775 zscream_project_file = project_path;
2776
2778
2779 return absl::OkStatus();
2780}
2781
2785
2789
2791 absl::string_view artifact_name) const {
2792 std::filesystem::path base_dir;
2793 if (!output_folder.empty()) {
2794 base_dir = output_folder;
2795 } else if (!z3dk_settings.config_path.empty()) {
2796 base_dir = std::filesystem::path(z3dk_settings.config_path).parent_path();
2797 } else if (!code_folder.empty()) {
2798 base_dir = code_folder;
2799 } else if (!filepath.empty()) {
2800 base_dir = std::filesystem::path(filepath).parent_path();
2801 }
2802
2803 if (base_dir.empty()) {
2804 return std::string(artifact_name);
2805 }
2806 return (base_dir / std::string(artifact_name)).lexically_normal().string();
2807}
2808
2811
2812#if defined(YAZE_WITH_Z3DK) && __has_include("z3dk_core/config.h")
2813 std::vector<std::filesystem::path> candidates;
2814 auto add_candidate = [&candidates](const std::filesystem::path& candidate) {
2815 if (candidate.empty()) {
2816 return;
2817 }
2818 auto normalized = candidate.lexically_normal();
2819 if (std::find(candidates.begin(), candidates.end(), normalized) ==
2820 candidates.end()) {
2821 candidates.push_back(normalized);
2822 }
2823 };
2824
2825 if (!code_folder.empty()) {
2826 std::filesystem::path code_path(code_folder);
2827 if (!std::filesystem::is_directory(code_path)) {
2828 code_path = code_path.parent_path();
2829 }
2830 add_candidate(code_path / "z3dk.toml");
2831 }
2832
2833 if (!hack_manifest_file.empty()) {
2834 add_candidate(std::filesystem::path(hack_manifest_file).parent_path() /
2835 "z3dk.toml");
2836 }
2837
2838 if (!filepath.empty()) {
2839 add_candidate(std::filesystem::path(filepath).parent_path() / "z3dk.toml");
2840 }
2841
2842 for (const auto& candidate : candidates) {
2843 if (!std::filesystem::exists(candidate)) {
2844 continue;
2845 }
2846
2847 std::string error;
2848 z3dk::Config config = z3dk::LoadConfigFile(candidate.string(), &error);
2849 if (!error.empty()) {
2850 LOG_WARN("Project", "Failed to parse z3dk config '%s': %s",
2851 candidate.string().c_str(), error.c_str());
2852 continue;
2853 }
2854
2855 const std::filesystem::path base_dir = candidate.parent_path();
2856 z3dk_settings.loaded = true;
2857 z3dk_settings.config_path = candidate.string();
2858 if (config.preset.has_value()) {
2859 z3dk_settings.preset = *config.preset;
2860 }
2861
2862 z3dk_settings.include_paths.reserve(config.include_paths.size());
2863 for (const auto& include_path : config.include_paths) {
2864 z3dk_settings.include_paths.push_back(
2865 ResolveOptionalPath(base_dir, include_path));
2866 }
2867
2868 z3dk_settings.defines.reserve(config.defines.size());
2869 for (const auto& define : config.defines) {
2870 z3dk_settings.defines.push_back(ParseDefineToken(define));
2871 }
2872
2873 z3dk_settings.main_files.reserve(config.main_files.size());
2874 for (const auto& main_file : config.main_files) {
2875 z3dk_settings.main_files.push_back(
2876 ResolveOptionalPath(base_dir, main_file));
2877 }
2878
2879 if (config.std_includes_path.has_value()) {
2881 ResolveOptionalPath(base_dir, *config.std_includes_path);
2882 }
2883 if (config.std_defines_path.has_value()) {
2885 ResolveOptionalPath(base_dir, *config.std_defines_path);
2886 }
2887 if (config.mapper.has_value()) {
2888 z3dk_settings.mapper = *config.mapper;
2889 }
2890 if (config.rom_size.has_value()) {
2891 z3dk_settings.rom_size = *config.rom_size;
2892 }
2893 if (config.symbols_format.has_value()) {
2894 z3dk_settings.symbols_format = *config.symbols_format;
2895 }
2896 z3dk_settings.lsp_log_enabled = config.lsp_log_enabled;
2897 if (config.lsp_log_path.has_value()) {
2899 ResolveOptionalPath(base_dir, *config.lsp_log_path);
2900 }
2901
2902 z3dk_settings.emits.reserve(config.emits.size());
2903 for (const auto& emit_path : config.emits) {
2904 z3dk_settings.emits.push_back(ResolveOptionalPath(base_dir, emit_path));
2905 }
2906
2907 for (const auto& range : config.prohibited_memory_ranges) {
2909 {.start = range.start, .end = range.end, .reason = range.reason});
2910 }
2911
2913 config.warn_unused_symbols.value_or(true);
2915 config.warn_branch_outside_bank.value_or(true);
2916 z3dk_settings.warn_unknown_width = config.warn_unknown_width.value_or(true);
2917 z3dk_settings.warn_org_collision = config.warn_org_collision.value_or(true);
2919 config.warn_unauthorized_hook.value_or(true);
2920 z3dk_settings.warn_stack_balance = config.warn_stack_balance.value_or(true);
2921 z3dk_settings.warn_hook_return = config.warn_hook_return.value_or(true);
2922
2923 if (config.rom_path.has_value()) {
2924 z3dk_settings.rom_path = ResolveOptionalPath(base_dir, *config.rom_path);
2925 }
2926 if (config.symbols_path.has_value()) {
2928 ResolveOptionalPath(base_dir, *config.symbols_path);
2929 }
2930
2931 for (const auto& emit_path : z3dk_settings.emits) {
2932 const std::string basename = BasenameLower(emit_path);
2933 if (basename.ends_with(".mlb") &&
2936 } else if (basename == "sourcemap.json") {
2938 } else if (basename == "annotations.json") {
2940 } else if (basename == "hooks.json") {
2942 } else if (basename == "lint.json") {
2944 }
2945 }
2946
2948 if (!z3dk_settings.symbols_path.empty() &&
2949 BasenameLower(z3dk_settings.symbols_path).ends_with(".mlb")) {
2951 } else {
2953 GetZ3dkArtifactPath("symbols.mlb");
2954 }
2955 }
2958 GetZ3dkArtifactPath("sourcemap.json");
2959 }
2962 GetZ3dkArtifactPath("annotations.json");
2963 }
2966 GetZ3dkArtifactPath("hooks.json");
2967 }
2970 }
2971
2972 LOG_INFO("Project",
2973 "Loaded z3dk config from %s (%zu include paths, %zu defines)",
2974 z3dk_settings.config_path.c_str(),
2976 return;
2977 }
2978#endif
2979}
2980
2982#ifdef __EMSCRIPTEN__
2985 return; // Hack manifests not supported in web builds
2986#endif
2987
2988 // Clear previous state so we never keep a stale manifest across project loads.
2991
2992 std::filesystem::path loaded_manifest_path;
2993 auto load_manifest = [&](const std::filesystem::path& candidate,
2994 bool update_project_setting) -> bool {
2995 if (candidate.empty() || !std::filesystem::exists(candidate)) {
2996 return false;
2997 }
2998 auto status = hack_manifest.LoadFromFile(candidate.string());
2999 if (!status.ok()) {
3000 LOG_WARN("Project", "Failed to load hack manifest %s: %s",
3001 candidate.string().c_str(),
3002 std::string(status.message()).c_str());
3003 return false;
3004 }
3005 loaded_manifest_path = candidate;
3006 if (update_project_setting) {
3007 hack_manifest_file = GetRelativePath(candidate.string());
3008 }
3009 LOG_DEBUG("Project", "Loaded hack manifest: %s",
3010 candidate.string().c_str());
3012 return true;
3013 };
3014
3015 // Priority 1: An explicit hack_manifest_file setting is authoritative. Do
3016 // not silently replace a missing or malformed configured manifest with an
3017 // auto-discovered file; project validation must surface the bad reference.
3018 const bool has_explicit_manifest = !hack_manifest_file.empty();
3019 if (has_explicit_manifest) {
3020 (void)load_manifest(GetAbsolutePath(hack_manifest_file), false);
3021 }
3022
3023 // Priority 2: Auto-discover hack_manifest.json in code_folder.
3024 if (!has_explicit_manifest && !hack_manifest.loaded() &&
3025 !code_folder.empty()) {
3026 auto code_path = GetAbsolutePath(code_folder);
3027 auto candidate = std::filesystem::path(code_path) / "hack_manifest.json";
3028 (void)load_manifest(candidate, true);
3029 }
3030
3031 // Priority 3: Fallback to the project file directory (or its parent).
3032 if (!has_explicit_manifest && !hack_manifest.loaded() && !filepath.empty()) {
3033 const std::filesystem::path project_dir =
3034 std::filesystem::path(filepath).parent_path();
3035 (void)load_manifest(project_dir / "hack_manifest.json", true);
3036 if (!hack_manifest.loaded() && project_dir.has_parent_path()) {
3037 (void)load_manifest(project_dir.parent_path() / "hack_manifest.json",
3038 true);
3039 }
3040 }
3041
3042 if (hack_manifest.loaded()) {
3044 }
3045
3046 auto try_load_registry = [&](const std::filesystem::path& base) -> bool {
3047 if (base.empty()) {
3048 return false;
3049 }
3050 const auto planning = base / "Docs" / "Dev" / "Planning";
3051 if (!std::filesystem::exists(planning)) {
3052 return false;
3053 }
3054 auto status = hack_manifest.LoadProjectRegistry(base.string());
3055 if (!status.ok()) {
3056 LOG_WARN("Project", "Failed to load project registry from %s: %s",
3057 base.string().c_str(), std::string(status.message()).c_str());
3058 return false;
3059 }
3061 };
3062
3063 bool registry_loaded = false;
3064
3065 // Prefer configured code_folder when valid.
3066 if (!code_folder.empty()) {
3067 registry_loaded =
3068 try_load_registry(std::filesystem::path(GetAbsolutePath(code_folder)));
3069 }
3070
3071 // Fallback to the manifest directory if code_folder is stale/misconfigured.
3072 if (!registry_loaded && !loaded_manifest_path.empty()) {
3073 registry_loaded = try_load_registry(loaded_manifest_path.parent_path());
3074 }
3075
3076 // Last fallback: project directory.
3077 if (!registry_loaded && !filepath.empty()) {
3078 registry_loaded =
3079 try_load_registry(std::filesystem::path(filepath).parent_path());
3080 }
3081
3082 if (!registry_loaded) {
3083 if (!hack_manifest.loaded()) {
3084 return;
3085 }
3086 LOG_WARN("Project",
3087 "Hack manifest loaded but project registry was not found "
3088 "(code_folder='%s', manifest='%s')",
3089 code_folder.c_str(), loaded_manifest_path.string().c_str());
3090 return;
3091 }
3092
3093 // Inject all Oracle resource labels into project resource_labels.
3094 size_t injected = 0;
3095 for (const auto& [type_key, labels] :
3097 for (const auto& [id_str, label] : labels) {
3098 resource_labels[type_key][id_str] = label;
3099 ++injected;
3100 }
3101 }
3102 LOG_DEBUG("Project", "Loaded project registry: %zu resource labels injected",
3103 injected);
3104}
3105
3107 sprite_catalog_file.clear();
3108 sprite_source_root.clear();
3109 sprite_assets.clear();
3110 terrain_catalog_file.clear();
3111 lyra_terrain_file.clear();
3116 palette_catalog_file.clear();
3117 dungeon_tile_patterns.clear();
3118 if (metadata.project_id.empty()) {
3120 }
3121
3122 // Initialize default feature flags
3138 // REMOVED: kLogInstructions (deprecated)
3139
3140 // Initialize default workspace settings
3143 workspace_settings.ui_theme = "default";
3145 workspace_settings.autosave_interval_secs = 300.0f; // 5 minutes
3152
3153 // Initialize default dungeon overlay settings (minecart tracks)
3155 for (uint16_t tile = 0xB0; tile <= 0xBE; ++tile) {
3156 dungeon_overlay.track_tiles.push_back(tile);
3157 }
3158 dungeon_overlay.track_stop_tiles = {0xB7, 0xB8, 0xB9, 0xBA};
3159 dungeon_overlay.track_switch_tiles = {0xD0, 0xD1, 0xD2, 0xD3};
3162
3166
3167 // Initialize default build configurations
3168 build_configurations = {"Debug", "Release", "Distribution"};
3169 build_target.clear();
3170 asm_entry_point = "asm/main.asm";
3171 asm_sources = {"asm"};
3172 last_build_hash.clear();
3173 build_number = 0;
3174
3175 track_changes = true;
3176
3180
3181 if (metadata.created_by.empty()) {
3182 metadata.created_by = "YAZE";
3183 }
3184}
3185
3187 auto now = std::chrono::system_clock::now().time_since_epoch();
3188 auto timestamp =
3189 std::chrono::duration_cast<std::chrono::milliseconds>(now).count();
3190 return absl::StrFormat("yaze_project_%lld", timestamp);
3191}
3192
3193// ProjectManager Implementation
3194std::vector<ProjectManager::ProjectTemplate>
3196 std::vector<ProjectTemplate> templates;
3197
3198 // ==========================================================================
3199 // ZSCustomOverworld Templates (Recommended)
3200 // ==========================================================================
3201
3202 // Vanilla ROM Hack - no ZSO
3203 {
3205 t.name = "Vanilla ROM Hack";
3206 t.description =
3207 "Standard ROM editing without custom ASM. Limited to vanilla features.";
3211 false;
3216 templates.push_back(t);
3217 }
3218
3219 // ZSCustomOverworld v2 - Basic expansion
3220 {
3222 t.name = "ZSCustomOverworld v2";
3223 t.description =
3224 "Basic overworld expansion: custom BG colors, main palettes, parent "
3225 "system.";
3226 t.icon = ICON_MD_MAP;
3229 true;
3236 t.template_project.metadata.tags = {"zso_v2", "overworld", "expansion"};
3237 templates.push_back(t);
3238 }
3239
3240 // ZSCustomOverworld v3 - Full features (Recommended)
3241 {
3243 t.name = "ZSCustomOverworld v3 (Recommended)";
3244 t.description =
3245 "Full overworld expansion: wide/tall areas, animated GFX, overlays, "
3246 "all features.";
3250 true;
3260 t.template_project.metadata.tags = {"zso_v3", "overworld", "full",
3261 "recommended"};
3262 templates.push_back(t);
3263 }
3264
3265 // Randomizer Compatible
3266 {
3268 t.name = "Randomizer Compatible";
3269 t.description =
3270 "Compatible with ALttP Randomizer. Minimal custom features to avoid "
3271 "conflicts.";
3275 false;
3279 t.template_project.metadata.tags = {"randomizer", "compatible", "minimal"};
3280 templates.push_back(t);
3281 }
3282
3283 // ==========================================================================
3284 // Editor-Focused Templates
3285 // ==========================================================================
3286
3287 // Dungeon Designer
3288 {
3290 t.name = "Dungeon Designer";
3291 t.description = "Focused on dungeon creation and modification.";
3292 t.icon = ICON_MD_DOMAIN;
3296 "dungeon_default";
3297 t.template_project.metadata.tags = {"dungeons", "rooms", "design"};
3298 templates.push_back(t);
3299 }
3300
3301 // Graphics Pack
3302 {
3304 t.name = "Graphics Pack";
3305 t.description =
3306 "Project focused on graphics, sprites, and visual modifications.";
3313 "graphics_default";
3314 t.template_project.metadata.tags = {"graphics", "sprites", "palettes"};
3315 templates.push_back(t);
3316 }
3317
3318 // Complete Overhaul
3319 {
3321 t.name = "Complete Overhaul";
3322 t.description = "Full-scale ROM hack with all features enabled.";
3323 t.icon = ICON_MD_BUILD;
3326 true;
3336 t.template_project.metadata.tags = {"complete", "overhaul", "full-mod"};
3337 templates.push_back(t);
3338 }
3339
3340 return templates;
3341}
3342
3343absl::StatusOr<YazeProject> ProjectManager::CreateFromTemplate(
3344 const std::string& template_name, const std::string& project_name,
3345 const std::string& base_path) {
3346 YazeProject project;
3347 auto status = project.Create(project_name, base_path);
3348 if (!status.ok()) {
3349 return status;
3350 }
3351
3352 // Customize based on template
3353 if (template_name == "Full Overworld Mod") {
3356 project.metadata.description = "Overworld modification project";
3357 project.metadata.tags = {"overworld", "maps", "graphics"};
3358 } else if (template_name == "Dungeon Designer") {
3359 project.feature_flags.kSaveDungeonMaps = true;
3360 project.workspace_settings.show_grid = true;
3361 project.metadata.description = "Dungeon design and modification project";
3362 project.metadata.tags = {"dungeons", "rooms", "design"};
3363 } else if (template_name == "Graphics Pack") {
3364 project.feature_flags.kSaveGraphicsSheet = true;
3365 project.workspace_settings.show_grid = true;
3366 project.metadata.description = "Graphics and sprite modification project";
3367 project.metadata.tags = {"graphics", "sprites", "palettes"};
3368 } else if (template_name == "Complete Overhaul") {
3371 project.feature_flags.kSaveDungeonMaps = true;
3372 project.feature_flags.kSaveGraphicsSheet = true;
3373 project.metadata.description = "Complete ROM overhaul project";
3374 project.metadata.tags = {"complete", "overhaul", "full-mod"};
3375 }
3376
3377 status = project.Save();
3378 if (!status.ok()) {
3379 return status;
3380 }
3381
3382 return project;
3383}
3384
3386 const std::string& directory) {
3387#ifdef __EMSCRIPTEN__
3388 (void)directory;
3389 return {};
3390#else
3391 std::vector<std::string> projects;
3392
3393 try {
3394 for (const auto& entry : std::filesystem::directory_iterator(directory)) {
3395 if (entry.is_regular_file()) {
3396 std::string filename = entry.path().filename().string();
3397 if (filename.ends_with(".yaze") || filename.ends_with(".zsproj")) {
3398 projects.push_back(entry.path().string());
3399 }
3400 } else if (entry.is_directory()) {
3401 std::string filename = entry.path().filename().string();
3402 if (filename.ends_with(".yazeproj")) {
3403 projects.push_back(entry.path().string());
3404 }
3405 }
3406 }
3407 } catch (const std::filesystem::filesystem_error& e) {
3408 // Directory doesn't exist or can't be accessed
3409 }
3410
3411 return projects;
3412#endif // __EMSCRIPTEN__
3413}
3414
3415absl::Status ProjectManager::BackupProject(const YazeProject& project) {
3416#ifdef __EMSCRIPTEN__
3417 (void)project;
3418 return absl::UnimplementedError(
3419 "Project backups are not supported in the web build");
3420#else
3421 if (project.filepath.empty()) {
3422 return absl::InvalidArgumentError("Project has no file path");
3423 }
3424
3425 std::filesystem::path project_path(project.filepath);
3426 std::filesystem::path backup_dir = project_path.parent_path() / "backups";
3427 std::filesystem::create_directories(backup_dir);
3428
3429 auto now = std::chrono::system_clock::now();
3430 auto time_t = std::chrono::system_clock::to_time_t(now);
3431 std::stringstream ss;
3432 ss << std::put_time(std::localtime(&time_t), "%Y%m%d_%H%M%S");
3433
3434 std::string backup_filename = project.name + "_backup_" + ss.str() + ".yaze";
3435 std::filesystem::path backup_path = backup_dir / backup_filename;
3436
3437 try {
3438 std::filesystem::copy_file(project.filepath, backup_path);
3439 } catch (const std::filesystem::filesystem_error& e) {
3440 return absl::InternalError(
3441 absl::StrFormat("Failed to backup project: %s", e.what()));
3442 }
3443
3444 return absl::OkStatus();
3445#endif
3446}
3447
3449 const YazeProject& project) {
3450 return project.Validate();
3451}
3452
3454 const YazeProject& project) {
3455 std::vector<std::string> recommendations;
3456
3457 if (project.rom_filename.empty()) {
3458 recommendations.push_back("Add a ROM file to begin editing");
3459 }
3460
3461 if (project.code_folder.empty()) {
3462 recommendations.push_back("Set up a code folder for assembly patches");
3463 }
3464
3465 if (project.labels_filename.empty()) {
3466 recommendations.push_back("Create a labels file for better organization");
3467 }
3468
3469 if (project.metadata.description.empty()) {
3470 recommendations.push_back("Add a project description for documentation");
3471 }
3472
3473 if (project.git_repository.empty() && project.track_changes) {
3474 recommendations.push_back(
3475 "Consider setting up version control for your project");
3476 }
3477
3478 auto missing_files = project.GetMissingFiles();
3479 if (!missing_files.empty()) {
3480 recommendations.push_back(
3481 "Some project files are missing - use Project > Repair to fix");
3482 }
3483
3484 return recommendations;
3485}
3486
3487// Compatibility implementations for ResourceLabelManager and related classes
3488bool ResourceLabelManager::LoadLabels(const std::string& filename) {
3489 filename_ = filename;
3490 std::ifstream file(filename);
3491 if (!file.is_open()) {
3492 labels_loaded_ = false;
3493 return false;
3494 }
3495
3496 labels_.clear();
3497 std::string line;
3498 std::string current_type = "";
3499
3500 while (std::getline(file, line)) {
3501 if (line.empty() || line[0] == '#')
3502 continue;
3503
3504 // Check for type headers [type_name]
3505 if (line[0] == '[' && line.back() == ']') {
3506 current_type = line.substr(1, line.length() - 2);
3507 continue;
3508 }
3509
3510 // Parse key=value pairs
3511 size_t eq_pos = line.find('=');
3512 if (eq_pos != std::string::npos && !current_type.empty()) {
3513 std::string key = line.substr(0, eq_pos);
3514 std::string value = line.substr(eq_pos + 1);
3515 labels_[current_type][key] = value;
3516 }
3517 }
3518
3519 file.close();
3520 MigrateGfxGroupLabelKeys(labels_);
3521 labels_loaded_ = true;
3522 return true;
3523}
3524
3526 if (filename_.empty())
3527 return false;
3528
3529 std::ofstream file(filename_);
3530 if (!file.is_open())
3531 return false;
3532
3533 file << "# yaze Resource Labels\n";
3534 file << "# Format: [type] followed by key=value pairs\n\n";
3535
3536 for (const auto& [type, type_labels] : labels_) {
3537 if (!type_labels.empty()) {
3538 file << "[" << type << "]\n";
3539 for (const auto& [key, value] : type_labels) {
3540 file << key << "=" << value << "\n";
3541 }
3542 file << "\n";
3543 }
3544 }
3545
3546 file.close();
3547 return true;
3548}
3549
3551 if (!p_open || !*p_open)
3552 return;
3553
3554 // Basic implementation - can be enhanced later
3555 if (ImGui::Begin("Resource Labels", p_open)) {
3556 ImGui::Text("Resource Labels Manager");
3557 ImGui::Text("Labels loaded: %s", labels_loaded_ ? "Yes" : "No");
3558 ImGui::Text("Total types: %zu", labels_.size());
3559
3560 for (const auto& [type, type_labels] : labels_) {
3561 if (ImGui::TreeNode(type.c_str())) {
3562 ImGui::Text("Labels: %zu", type_labels.size());
3563 for (const auto& [key, value] : type_labels) {
3564 ImGui::Text("%s = %s", key.c_str(), value.c_str());
3565 }
3566 ImGui::TreePop();
3567 }
3568 }
3569 }
3570 ImGui::End();
3571}
3572
3573void ResourceLabelManager::EditLabel(const std::string& type,
3574 const std::string& key,
3575 const std::string& newValue) {
3576 labels_[type][key] = newValue;
3577}
3578
3580 bool selected, const std::string& type, const std::string& key,
3581 const std::string& defaultValue) {
3582 const auto custom_label = GetLabel(type, key);
3583 const auto& label = custom_label.empty() ? defaultValue : custom_label;
3584 if (ImGui::Selectable(
3585 absl::StrFormat("%s: %s", key.c_str(), label.c_str()).c_str(),
3586 selected)) {
3587 // Handle selection
3588 }
3589}
3590
3591std::string ResourceLabelManager::GetLabel(const std::string& type,
3592 const std::string& key) {
3593 auto type_it = labels_.find(type);
3594 if (type_it == labels_.end())
3595 return "";
3596
3597 auto label_it = type_it->second.find(key);
3598 if (label_it == type_it->second.end())
3599 return "";
3600
3601 return label_it->second;
3602}
3603
3605 const std::string& type, const std::string& key,
3606 const std::string& defaultValue) {
3607 auto existing = GetLabel(type, key);
3608 if (!existing.empty())
3609 return existing;
3610
3611 labels_[type][key] = defaultValue;
3612 return defaultValue;
3613}
3614
3615// ============================================================================
3616// Embedded Labels Support
3617// ============================================================================
3618
3620 const std::unordered_map<
3621 std::string, std::unordered_map<std::string, std::string>>& labels) {
3622 try {
3623 // Load all default Zelda3 resource names into resource_labels
3624 // We merge them with existing labels, prioritizing existing overrides?
3625 // Or just overwrite? The previous code was:
3626 // resource_labels = zelda3::Zelda3Labels::ToResourceLabels();
3627 // which implies overwriting. But we want to keep overrides if possible.
3628 // However, this is usually called on load.
3629
3630 // Let's overwrite for now to match previous behavior, assuming overrides
3631 // are loaded afterwards or this is initial setup.
3632 // Actually, if we load project then init embedded labels, we might lose overrides.
3633 // But typically overrides are loaded from the project file *into* resource_labels.
3634 // If we call this, we might clobber them.
3635 // The previous implementation clobbered resource_labels.
3636
3637 // However, if we want to support overrides + embedded, we should merge.
3638 // But `resource_labels` was treated as "overrides" in the old code?
3639 // No, `resource_labels` was the container for loaded labels.
3640
3641 // If I look at `LoadFromYazeFormat`:
3642 // It parses `[labels_type]` into `resource_labels`.
3643
3644 // If `use_embedded_labels` is true, `InitializeEmbeddedLabels` is called?
3645 // I need to check when `InitializeEmbeddedLabels` is called.
3646
3647 resource_labels = labels;
3648 use_embedded_labels = true;
3649
3650 LOG_DEBUG("Project", "Initialized embedded labels:");
3651 LOG_DEBUG("Project", " - %d room names", resource_labels["room"].size());
3652 LOG_DEBUG("Project", " - %d entrance names",
3653 resource_labels["entrance"].size());
3654 LOG_DEBUG("Project", " - %d sprite names",
3655 resource_labels["sprite"].size());
3656 LOG_DEBUG("Project", " - %d overlord names",
3657 resource_labels["overlord"].size());
3658 LOG_DEBUG("Project", " - %d item names", resource_labels["item"].size());
3659 LOG_DEBUG("Project", " - %d music names",
3660 resource_labels["music"].size());
3661 LOG_DEBUG("Project", " - %d graphics names",
3662 resource_labels["graphics"].size());
3663 LOG_DEBUG("Project", " - %d room effect names",
3664 resource_labels["room_effect"].size());
3665 LOG_DEBUG("Project", " - %d room tag names",
3666 resource_labels["room_tag"].size());
3667 LOG_DEBUG("Project", " - %d tile type names",
3668 resource_labels["tile_type"].size());
3669
3670 return absl::OkStatus();
3671 } catch (const std::exception& e) {
3672 return absl::InternalError(
3673 absl::StrCat("Failed to initialize embedded labels: ", e.what()));
3674 }
3675}
3676
3677std::string YazeProject::GetLabel(const std::string& resource_type, int id,
3678 const std::string& default_value) const {
3679 // First check if we have a custom label override
3680 auto type_it = resource_labels.find(resource_type);
3681 if (type_it != resource_labels.end()) {
3682 auto label_it = type_it->second.find(std::to_string(id));
3683 if (label_it != type_it->second.end()) {
3684 return label_it->second;
3685 }
3686 }
3687
3688 return default_value.empty() ? resource_type + "_" + std::to_string(id)
3689 : default_value;
3690}
3691
3692absl::Status YazeProject::ImportLabelsFromZScream(const std::string& filepath) {
3693#ifdef __EMSCRIPTEN__
3694 (void)filepath;
3695 return absl::UnimplementedError(
3696 "File-based label import is not supported in the web build");
3697#else
3698 std::ifstream file(filepath);
3699 if (!file.is_open()) {
3700 return absl::InvalidArgumentError(
3701 absl::StrFormat("Cannot open labels file: %s", filepath));
3702 }
3703
3704 std::stringstream buffer;
3705 buffer << file.rdbuf();
3706 file.close();
3707
3708 return ImportLabelsFromZScreamContent(buffer.str());
3709#endif
3710}
3711
3713 const std::string& content) {
3714 // Initialize the global provider with our labels
3715 auto& provider = zelda3::GetResourceLabels();
3716 provider.SetProjectLabels(&resource_labels);
3717 provider.SetPreferHMagicNames(workspace_settings.prefer_hmagic_names);
3718
3719 // Use the provider to parse ZScream format
3720 auto status = provider.ImportFromZScreamFormat(content);
3721 if (!status.ok()) {
3722 return status;
3723 }
3724
3725 LOG_DEBUG("Project", "Imported ZScream labels:");
3726 LOG_DEBUG("Project", " - %d sprite labels",
3727 resource_labels["sprite"].size());
3728 LOG_DEBUG("Project", " - %d room labels", resource_labels["room"].size());
3729 LOG_DEBUG("Project", " - %d item labels", resource_labels["item"].size());
3730 LOG_DEBUG("Project", " - %d room tag labels",
3731 resource_labels["room_tag"].size());
3732
3733 return absl::OkStatus();
3734}
3735
3737 auto& provider = zelda3::GetResourceLabels();
3738 provider.SetProjectLabels(&resource_labels);
3739 provider.SetPreferHMagicNames(workspace_settings.prefer_hmagic_names);
3740 provider.SetHackManifest(hack_manifest.loaded() ? &hack_manifest : nullptr);
3741
3742 LOG_DEBUG("Project", "Initialized ResourceLabelProvider with project labels");
3743 LOG_DEBUG("Project", " - prefer_hmagic_names: %s",
3744 workspace_settings.prefer_hmagic_names ? "true" : "false");
3745 LOG_DEBUG("Project", " - hack_manifest: %s",
3746 hack_manifest.loaded() ? "loaded" : "not loaded");
3747}
3748
3749// ============================================================================
3750// JSON Format Support (Optional)
3751// ============================================================================
3752
3753#ifdef YAZE_ENABLE_JSON_PROJECT_FORMAT
3754
3755absl::Status YazeProject::LoadFromJsonFormat(const std::string& project_path) {
3760 terrain_catalog_file.clear();
3761 lyra_terrain_file.clear();
3766 palette_catalog_file.clear();
3767 dungeon_tile_patterns.clear();
3768#ifdef __EMSCRIPTEN__
3769 return absl::UnimplementedError(
3770 "JSON project format loading is not supported in the web build");
3771#endif
3772 std::ifstream file(project_path);
3773 if (!file.is_open()) {
3774 return absl::InvalidArgumentError(
3775 absl::StrFormat("Cannot open JSON project file: %s", project_path));
3776 }
3777
3778 try {
3779 Json j;
3780 file >> j;
3781
3782 sprite_catalog_file.clear();
3783 sprite_source_root.clear();
3784 sprite_assets.clear();
3785
3786 // Parse project metadata
3787 if (j.contains("yaze_project")) {
3788 auto& proj = j["yaze_project"];
3789
3790 if (proj.contains("name"))
3791 name = proj["name"].get<std::string>();
3792 if (proj.contains("description"))
3793 metadata.description = proj["description"].get<std::string>();
3794 if (proj.contains("author"))
3795 metadata.author = proj["author"].get<std::string>();
3796 if (proj.contains("version"))
3797 metadata.version = proj["version"].get<std::string>();
3798 if (proj.contains("created"))
3799 metadata.created_date = proj["created"].get<std::string>();
3800 if (proj.contains("modified"))
3801 metadata.last_modified = proj["modified"].get<std::string>();
3802 if (proj.contains("created_by"))
3803 metadata.created_by = proj["created_by"].get<std::string>();
3804
3805 // Files
3806 if (proj.contains("rom_filename"))
3807 rom_filename = proj["rom_filename"].get<std::string>();
3808 if (proj.contains("rom_backup_folder"))
3809 rom_backup_folder = proj["rom_backup_folder"].get<std::string>();
3810 if (proj.contains("code_folder"))
3811 code_folder = proj["code_folder"].get<std::string>();
3812 if (proj.contains("assets_folder"))
3813 assets_folder = proj["assets_folder"].get<std::string>();
3814 if (proj.contains("patches_folder"))
3815 patches_folder = proj["patches_folder"].get<std::string>();
3816 if (proj.contains("labels_filename"))
3817 labels_filename = proj["labels_filename"].get<std::string>();
3818 if (proj.contains("symbols_filename"))
3819 symbols_filename = proj["symbols_filename"].get<std::string>();
3820 if (proj.contains("dungeon_tile_patterns"))
3822 proj["dungeon_tile_patterns"].get<std::string>();
3823 if (proj.contains("palette_catalog_file"))
3824 palette_catalog_file = proj["palette_catalog_file"].get<std::string>();
3825 if (proj.contains("terrain_catalog_file"))
3826 terrain_catalog_file = proj["terrain_catalog_file"].get<std::string>();
3827 if (proj.contains("lyra_terrain_file"))
3828 lyra_terrain_file = proj["lyra_terrain_file"].get<std::string>();
3829 if (proj.contains("lyra_build_request_file"))
3831 proj["lyra_build_request_file"].get<std::string>();
3832 if (proj.contains("native_save_producer_file"))
3834 proj["native_save_producer_file"].get<std::string>();
3835 if (proj.contains("native_save_transfer_proof_file"))
3837 proj["native_save_transfer_proof_file"].get<std::string>();
3838 if (proj.contains("native_save_receipt_file"))
3840 proj["native_save_receipt_file"].get<std::string>();
3841 if (proj.contains("hack_manifest_file"))
3842 hack_manifest_file = proj["hack_manifest_file"].get<std::string>();
3843 if (proj.contains("sprite_catalog_file"))
3844 sprite_catalog_file = proj["sprite_catalog_file"].get<std::string>();
3845 if (proj.contains("sprite_source_root"))
3846 sprite_source_root = proj["sprite_source_root"].get<std::string>();
3847 if (proj.contains("sprite_assets")) {
3848 if (!proj["sprite_assets"].is_array() ||
3849 proj["sprite_assets"].size() > 4096)
3850 return absl::InvalidArgumentError("Invalid sprite_assets array");
3851 for (const auto& value : proj["sprite_assets"]) {
3852 auto asset = SpriteAssetFromJson(value);
3853 if (!asset.ok())
3854 return asset.status();
3855 for (const auto& prior : sprite_assets)
3856 if (prior.zsm_path == asset->zsm_path)
3857 return absl::InvalidArgumentError("Duplicate sprite asset path");
3858 sprite_assets.push_back(std::move(*asset));
3859 }
3860 }
3861
3862 if (proj.contains("rom") && proj["rom"].is_object()) {
3863 auto& rom = proj["rom"];
3864 if (rom.contains("role"))
3865 rom_metadata.role = ParseRomRole(rom["role"].get<std::string>());
3866 if (rom.contains("expected_hash"))
3867 rom_metadata.expected_hash = rom["expected_hash"].get<std::string>();
3868 if (rom.contains("write_policy"))
3870 ParseRomWritePolicy(rom["write_policy"].get<std::string>());
3871 }
3872
3873 // Embedded labels flag
3874 if (proj.contains("use_embedded_labels")) {
3875 use_embedded_labels = proj["use_embedded_labels"].get<bool>();
3876 }
3877
3878 // Feature flags
3879 if (proj.contains("feature_flags")) {
3880 auto& flags = proj["feature_flags"];
3881 // REMOVED: kLogInstructions (deprecated - DisassemblyViewer always
3882 // active)
3883 if (flags.contains("kSaveDungeonMaps"))
3885 flags["kSaveDungeonMaps"].get<bool>();
3886 if (flags.contains("kSaveWithChangeQueue"))
3888 flags["kSaveWithChangeQueue"].get<bool>();
3889 if (flags.contains("kEnableSpecialWorldExpansion"))
3891 flags["kEnableSpecialWorldExpansion"].get<bool>();
3892 if (flags.contains("kSaveOverworldMaps"))
3894 flags["kSaveOverworldMaps"].get<bool>();
3895 if (flags.contains("kSaveOverworldEntrances"))
3897 flags["kSaveOverworldEntrances"].get<bool>();
3898 if (flags.contains("kSaveOverworldExits"))
3900 flags["kSaveOverworldExits"].get<bool>();
3901 if (flags.contains("kSaveOverworldItems"))
3903 flags["kSaveOverworldItems"].get<bool>();
3904 if (flags.contains("kSaveOverworldSprites"))
3906 flags["kSaveOverworldSprites"].get<bool>();
3907 if (flags.contains("kSaveOverworldProperties"))
3909 flags["kSaveOverworldProperties"].get<bool>();
3910 if (flags.contains("kSaveDungeonObjects"))
3912 flags["kSaveDungeonObjects"].get<bool>();
3913 if (flags.contains("kSaveDungeonSprites"))
3915 flags["kSaveDungeonSprites"].get<bool>();
3916 if (flags.contains("kSaveDungeonRoomHeaders"))
3918 flags["kSaveDungeonRoomHeaders"].get<bool>();
3919 if (flags.contains("kSaveDungeonTorches"))
3921 flags["kSaveDungeonTorches"].get<bool>();
3922 if (flags.contains("kSaveDungeonPits"))
3924 flags["kSaveDungeonPits"].get<bool>();
3925 if (flags.contains("kSaveDungeonBlocks"))
3927 flags["kSaveDungeonBlocks"].get<bool>();
3928 if (flags.contains("kSaveDungeonCollision"))
3930 flags["kSaveDungeonCollision"].get<bool>();
3931 if (flags.contains("kSaveDungeonWaterFillZones"))
3933 flags["kSaveDungeonWaterFillZones"].get<bool>();
3934 if (flags.contains("kSaveDungeonChests"))
3936 flags["kSaveDungeonChests"].get<bool>();
3937 if (flags.contains("kSaveDungeonPotItems"))
3939 flags["kSaveDungeonPotItems"].get<bool>();
3940 if (flags.contains("kSaveDungeonEntrances"))
3942 flags["kSaveDungeonEntrances"].get<bool>();
3943 if (flags.contains("kSaveDungeonPalettes"))
3945 flags["kSaveDungeonPalettes"].get<bool>();
3946 if (flags.contains("kSaveGraphicsSheet"))
3948 flags["kSaveGraphicsSheet"].get<bool>();
3949 if (flags.contains("kSaveAllPalettes"))
3951 flags["kSaveAllPalettes"].get<bool>();
3952 if (flags.contains("kSaveGfxGroups"))
3953 feature_flags.kSaveGfxGroups = flags["kSaveGfxGroups"].get<bool>();
3954 if (flags.contains("kSaveMessages"))
3955 feature_flags.kSaveMessages = flags["kSaveMessages"].get<bool>();
3956 }
3957
3958 // Workspace settings
3959 if (proj.contains("workspace_settings")) {
3960 auto& ws = proj["workspace_settings"];
3961 if (ws.contains("auto_save_enabled"))
3963 ws["auto_save_enabled"].get<bool>();
3964 if (ws.contains("auto_save_interval"))
3966 ws["auto_save_interval"].get<float>();
3967 if (ws.contains("backup_on_save"))
3968 workspace_settings.backup_on_save = ws["backup_on_save"].get<bool>();
3969 if (ws.contains("backup_retention_count"))
3971 ws["backup_retention_count"].get<int>();
3972 if (ws.contains("backup_keep_daily"))
3974 ws["backup_keep_daily"].get<bool>();
3975 if (ws.contains("backup_keep_daily_days"))
3977 ws["backup_keep_daily_days"].get<int>();
3978 }
3979
3980 if (proj.contains("rom_addresses") && proj["rom_addresses"].is_object()) {
3982 for (auto it = proj["rom_addresses"].begin();
3983 it != proj["rom_addresses"].end(); ++it) {
3984 if (it.value().is_number_unsigned()) {
3986 it.value().get<uint32_t>();
3987 } else if (it.value().is_string()) {
3988 auto parsed = ParseHexUint32(it.value().get<std::string>());
3989 if (parsed.has_value()) {
3990 rom_address_overrides.addresses[it.key()] = *parsed;
3991 }
3992 }
3993 }
3994 }
3995
3996 if (proj.contains("custom_objects") &&
3997 proj["custom_objects"].is_object()) {
3998 custom_object_files.clear();
3999 for (auto it = proj["custom_objects"].begin();
4000 it != proj["custom_objects"].end(); ++it) {
4001 if (!it.value().is_array())
4002 continue;
4003 auto parsed = ParseHexUint32(it.key());
4004 if (!parsed.has_value()) {
4005 continue;
4006 }
4007 std::vector<std::string> files;
4008 for (const auto& entry : it.value()) {
4009 if (entry.is_string()) {
4010 files.push_back(entry.get<std::string>());
4011 }
4012 }
4013 if (!files.empty()) {
4014 custom_object_files[static_cast<int>(*parsed)] = std::move(files);
4015 }
4016 }
4017 }
4018
4019 if (proj.contains("agent_settings") &&
4020 proj["agent_settings"].is_object()) {
4021 auto& agent = proj["agent_settings"];
4023 agent.value("ai_provider", agent_settings.ai_provider);
4025 agent.value("ai_model", agent_settings.ai_model);
4027 agent.value("ollama_host", agent_settings.ollama_host);
4029 agent.value("gemini_api_key", agent_settings.gemini_api_key);
4031 agent.value("use_custom_prompt", agent_settings.use_custom_prompt);
4033 "custom_system_prompt", agent_settings.custom_system_prompt);
4035 agent.value("show_reasoning", agent_settings.show_reasoning);
4036 agent_settings.verbose = agent.value("verbose", agent_settings.verbose);
4037 agent_settings.max_tool_iterations = agent.value(
4038 "max_tool_iterations", agent_settings.max_tool_iterations);
4039 agent_settings.max_retry_attempts = agent.value(
4040 "max_retry_attempts", agent_settings.max_retry_attempts);
4042 agent.value("temperature", agent_settings.temperature);
4043 agent_settings.top_p = agent.value("top_p", agent_settings.top_p);
4045 agent.value("max_output_tokens", agent_settings.max_output_tokens);
4047 agent.value("stream_responses", agent_settings.stream_responses);
4048 if (agent.contains("favorite_models") &&
4049 agent["favorite_models"].is_array()) {
4051 for (const auto& model : agent["favorite_models"]) {
4052 if (model.is_string())
4054 model.get<std::string>());
4055 }
4056 }
4057 if (agent.contains("model_chain") && agent["model_chain"].is_array()) {
4059 for (const auto& model : agent["model_chain"]) {
4060 if (model.is_string())
4061 agent_settings.model_chain.push_back(model.get<std::string>());
4062 }
4063 }
4065 agent.value("chain_mode", agent_settings.chain_mode);
4067 "enable_tool_resources", agent_settings.enable_tool_resources);
4068 agent_settings.enable_tool_dungeon = agent.value(
4069 "enable_tool_dungeon", agent_settings.enable_tool_dungeon);
4071 "enable_tool_overworld", agent_settings.enable_tool_overworld);
4073 "enable_tool_messages", agent_settings.enable_tool_messages);
4075 "enable_tool_dialogue", agent_settings.enable_tool_dialogue);
4077 agent.value("enable_tool_gui", agent_settings.enable_tool_gui);
4079 agent.value("enable_tool_music", agent_settings.enable_tool_music);
4080 agent_settings.enable_tool_sprite = agent.value(
4081 "enable_tool_sprite", agent_settings.enable_tool_sprite);
4083 "enable_tool_emulator", agent_settings.enable_tool_emulator);
4085 agent.value("enable_tool_memory_inspector",
4088 "builder_blueprint_path", agent_settings.builder_blueprint_path);
4089 }
4090
4091 // Build settings
4092 if (proj.contains("build_script"))
4093 build_script = proj["build_script"].get<std::string>();
4094 if (proj.contains("output_folder"))
4095 output_folder = proj["output_folder"].get<std::string>();
4096 if (proj.contains("git_repository"))
4097 git_repository = proj["git_repository"].get<std::string>();
4098 if (proj.contains("track_changes"))
4099 track_changes = proj["track_changes"].get<bool>();
4100 }
4101
4102 return absl::OkStatus();
4103 } catch (const Json::exception& e) {
4104 return absl::InvalidArgumentError(
4105 absl::StrFormat("JSON parse error: %s", e.what()));
4106 }
4107}
4108
4109absl::Status YazeProject::SaveToJsonFormat() {
4110#ifdef __EMSCRIPTEN__
4111 return absl::UnimplementedError(
4112 "JSON project format saving is not supported in the web build");
4113#endif
4114 Json j;
4115 auto& proj = j["yaze_project"];
4116
4117 // Metadata
4118 proj["version"] = metadata.version;
4119 proj["name"] = name;
4120 proj["author"] = metadata.author;
4121 proj["created_by"] = metadata.created_by;
4122 proj["description"] = metadata.description;
4123 proj["created"] = metadata.created_date;
4124 proj["modified"] = metadata.last_modified;
4125
4126 // Files
4127 proj["rom_filename"] = rom_filename;
4128 proj["rom_backup_folder"] = rom_backup_folder;
4129 proj["code_folder"] = code_folder;
4130 proj["assets_folder"] = assets_folder;
4131 proj["patches_folder"] = patches_folder;
4132 proj["labels_filename"] = labels_filename;
4133 proj["symbols_filename"] = symbols_filename;
4134 proj["dungeon_tile_patterns"] =
4136 proj["palette_catalog_file"] =
4138 proj["terrain_catalog_file"] =
4140 proj["lyra_terrain_file"] =
4142 proj["lyra_build_request_file"] =
4144 if (!native_save_producer_file.empty()) {
4145 proj["native_save_producer_file"] =
4147 }
4148 if (!native_save_transfer_proof_file.empty()) {
4149 proj["native_save_transfer_proof_file"] =
4151 }
4152 if (!native_save_receipt_file.empty()) {
4153 proj["native_save_receipt_file"] =
4155 }
4156 proj["hack_manifest_file"] = hack_manifest_file;
4157 proj["sprite_catalog_file"] =
4159 proj["sprite_source_root"] =
4161 proj["sprite_assets"] = nlohmann::json::array();
4162 for (auto asset : sprite_assets) {
4163 asset.zsm_path = GetRelativePath(GetAbsolutePath(asset.zsm_path));
4164 for (auto& source : asset.graphics_sources) {
4165 if (source.kind == "bin") {
4166 source.path = GetRelativePath(GetAbsolutePath(source.path));
4167 }
4168 }
4169 proj["sprite_assets"].push_back(SpriteAssetToJson(asset));
4170 }
4171 proj["output_folder"] = output_folder;
4172
4173 proj["rom"]["role"] = RomRoleToString(rom_metadata.role);
4174 proj["rom"]["expected_hash"] = rom_metadata.expected_hash;
4175 proj["rom"]["write_policy"] =
4177
4178 // Embedded labels
4179 proj["use_embedded_labels"] = use_embedded_labels;
4180
4181 // Feature flags
4182 // REMOVED: kLogInstructions (deprecated)
4183 proj["feature_flags"]["kSaveDungeonMaps"] = feature_flags.kSaveDungeonMaps;
4184 proj["feature_flags"]["kSaveWithChangeQueue"] =
4186 proj["feature_flags"]["kEnableSpecialWorldExpansion"] =
4188 proj["feature_flags"]["kSaveOverworldMaps"] =
4190 proj["feature_flags"]["kSaveOverworldEntrances"] =
4192 proj["feature_flags"]["kSaveOverworldExits"] =
4194 proj["feature_flags"]["kSaveOverworldItems"] =
4196 proj["feature_flags"]["kSaveOverworldSprites"] =
4198 proj["feature_flags"]["kSaveOverworldProperties"] =
4200 proj["feature_flags"]["kSaveDungeonObjects"] =
4202 proj["feature_flags"]["kSaveDungeonSprites"] =
4204 proj["feature_flags"]["kSaveDungeonRoomHeaders"] =
4206 proj["feature_flags"]["kSaveDungeonTorches"] =
4208 proj["feature_flags"]["kSaveDungeonPits"] = feature_flags.dungeon.kSavePits;
4209 proj["feature_flags"]["kSaveDungeonBlocks"] =
4211 proj["feature_flags"]["kSaveDungeonCollision"] =
4213 proj["feature_flags"]["kSaveDungeonWaterFillZones"] =
4215 proj["feature_flags"]["kSaveDungeonChests"] =
4217 proj["feature_flags"]["kSaveDungeonPotItems"] =
4219 proj["feature_flags"]["kSaveDungeonEntrances"] =
4221 proj["feature_flags"]["kSaveDungeonPalettes"] =
4223 proj["feature_flags"]["kSaveGraphicsSheet"] =
4225 proj["feature_flags"]["kSaveAllPalettes"] = feature_flags.kSaveAllPalettes;
4226 proj["feature_flags"]["kSaveGfxGroups"] = feature_flags.kSaveGfxGroups;
4227 proj["feature_flags"]["kSaveMessages"] = feature_flags.kSaveMessages;
4228
4229 // Workspace settings
4230 proj["workspace_settings"]["auto_save_enabled"] =
4232 proj["workspace_settings"]["auto_save_interval"] =
4234 proj["workspace_settings"]["backup_on_save"] =
4236 proj["workspace_settings"]["backup_retention_count"] =
4238 proj["workspace_settings"]["backup_keep_daily"] =
4240 proj["workspace_settings"]["backup_keep_daily_days"] =
4242
4243 auto& agent = proj["agent_settings"];
4244 agent["ai_provider"] = agent_settings.ai_provider;
4245 agent["ai_model"] = agent_settings.ai_model;
4246 agent["ollama_host"] = agent_settings.ollama_host;
4247 agent["gemini_api_key"] = agent_settings.gemini_api_key;
4248 agent["use_custom_prompt"] = agent_settings.use_custom_prompt;
4249 agent["custom_system_prompt"] = agent_settings.custom_system_prompt;
4250 agent["show_reasoning"] = agent_settings.show_reasoning;
4251 agent["verbose"] = agent_settings.verbose;
4252 agent["max_tool_iterations"] = agent_settings.max_tool_iterations;
4253 agent["max_retry_attempts"] = agent_settings.max_retry_attempts;
4254 agent["temperature"] = agent_settings.temperature;
4255 agent["top_p"] = agent_settings.top_p;
4256 agent["max_output_tokens"] = agent_settings.max_output_tokens;
4257 agent["stream_responses"] = agent_settings.stream_responses;
4258 agent["favorite_models"] = agent_settings.favorite_models;
4259 agent["model_chain"] = agent_settings.model_chain;
4260 agent["chain_mode"] = agent_settings.chain_mode;
4261 agent["enable_tool_resources"] = agent_settings.enable_tool_resources;
4262 agent["enable_tool_dungeon"] = agent_settings.enable_tool_dungeon;
4263 agent["enable_tool_overworld"] = agent_settings.enable_tool_overworld;
4264 agent["enable_tool_messages"] = agent_settings.enable_tool_messages;
4265 agent["enable_tool_dialogue"] = agent_settings.enable_tool_dialogue;
4266 agent["enable_tool_gui"] = agent_settings.enable_tool_gui;
4267 agent["enable_tool_music"] = agent_settings.enable_tool_music;
4268 agent["enable_tool_sprite"] = agent_settings.enable_tool_sprite;
4269 agent["enable_tool_emulator"] = agent_settings.enable_tool_emulator;
4270 agent["enable_tool_memory_inspector"] =
4272 agent["builder_blueprint_path"] = agent_settings.builder_blueprint_path;
4273
4274 if (!rom_address_overrides.addresses.empty()) {
4275 auto& addrs = proj["rom_addresses"];
4276 for (const auto& [key, value] : rom_address_overrides.addresses) {
4277 addrs[key] = value;
4278 }
4279 }
4280
4281 if (!custom_object_files.empty()) {
4282 auto& objs = proj["custom_objects"];
4283 for (const auto& [object_id, files] : custom_object_files) {
4284 objs[absl::StrFormat("0x%X", object_id)] = files;
4285 }
4286 }
4287
4288 // Build settings
4289 proj["build_script"] = build_script;
4290 proj["git_repository"] = git_repository;
4291 proj["track_changes"] = track_changes;
4292
4293 // Write to file
4294 std::ofstream file(filepath);
4295 if (!file.is_open()) {
4296 return absl::InvalidArgumentError(
4297 absl::StrFormat("Cannot write JSON project file: %s", filepath));
4298 }
4299
4300 file << j.dump(2); // Pretty print with 2-space indent
4301 return absl::OkStatus();
4302}
4303
4304#endif // YAZE_ENABLE_JSON_PROJECT_FORMAT
4305
4306// RecentFilesManager implementation
4308 auto config_dir = util::PlatformPaths::GetConfigDirectory();
4309 if (!config_dir.ok()) {
4310 return ""; // Or handle error appropriately
4311 }
4312 return (*config_dir / kRecentFilesFilename).string();
4313}
4314
4316#ifdef __EMSCRIPTEN__
4317 auto status = platform::WasmStorage::SaveProject(
4318 kRecentFilesFilename, absl::StrJoin(recent_files_, "\n"));
4319 if (!status.ok()) {
4320 LOG_WARN("RecentFilesManager", "Could not persist recent files: %s",
4321 status.ToString().c_str());
4322 }
4323 return;
4324#endif
4325 // Ensure config directory exists
4326 auto config_dir_status = util::PlatformPaths::GetConfigDirectory();
4327 if (!config_dir_status.ok()) {
4328 LOG_ERROR("Project", "Failed to get or create config directory: %s",
4329 config_dir_status.status().ToString().c_str());
4330 return;
4331 }
4332
4333 std::string filepath = GetFilePath();
4334 std::ofstream file(filepath);
4335 if (!file.is_open()) {
4336 LOG_WARN("RecentFilesManager", "Could not save recent files to %s",
4337 filepath.c_str());
4338 return;
4339 }
4340
4341 for (const auto& file_path : recent_files_) {
4342 file << file_path << std::endl;
4343 }
4344}
4345
4347#ifdef __EMSCRIPTEN__
4348 auto storage_or = platform::WasmStorage::LoadProject(kRecentFilesFilename);
4349 if (!storage_or.ok()) {
4350 return;
4351 }
4352 recent_files_.clear();
4353 std::istringstream stream(storage_or.value());
4354 std::string line;
4355 while (std::getline(stream, line)) {
4356 if (!line.empty()) {
4357 recent_files_.push_back(line);
4358 }
4359 }
4361 return;
4362#else
4363 std::string filepath = GetFilePath();
4364 std::ifstream file(filepath);
4365 if (!file.is_open()) {
4366 // File doesn't exist yet, which is fine
4367 return;
4368 }
4369
4370 recent_files_.clear();
4371 std::string line;
4372 while (std::getline(file, line)) {
4373 if (!line.empty()) {
4374 recent_files_.push_back(line);
4375 }
4376 }
4378#endif
4379}
4380
4381} // namespace project
4382} // namespace yaze
static Json parse(const std::string &)
Definition json.h:36
static Json array()
Definition json.h:35
T get() const
Definition json.h:49
bool contains(const std::string &) const
Definition json.h:53
void Clear()
Clear any loaded manifest state.
const std::string & hack_name() const
const ProjectRegistry & project_registry() const
bool HasProjectRegistry() const
absl::Status LoadProjectRegistry(const std::string &code_folder)
Load project registry data from the code folder.
std::optional< OracleProgressionState > oracle_progression_state() const
absl::Status LoadFromFile(const std::string &filepath)
Load manifest from a JSON file path.
bool loaded() const
Check if the manifest has been loaded.
const LyraTerrainProfile & profile() const
static absl::StatusOr< LyraTerrainDocument > Parse(std::string_view text, const LyraTerrainProfile &expected_profile)
static std::vector< std::string > FindProjectsInDirectory(const std::string &directory)
Definition project.cc:3385
static absl::Status ValidateProjectStructure(const YazeProject &project)
Definition project.cc:3448
static absl::StatusOr< YazeProject > CreateFromTemplate(const std::string &template_name, const std::string &project_name, const std::string &base_path)
Definition project.cc:3343
static std::vector< std::string > GetRecommendedFixesForProject(const YazeProject &project)
Definition project.cc:3453
static std::vector< ProjectTemplate > GetProjectTemplates()
Definition project.cc:3195
static absl::Status BackupProject(const YazeProject &project)
Definition project.cc:3415
std::string GetFilePath() const
Definition project.cc:4307
std::vector< std::string > recent_files_
Definition project.h:690
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.
struct destination destination
Room transition destination.
#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
absl::Status PublishSourceArtifacts(const SourceArtifactPublicationLock &lock, std::vector< SourceArtifactUpdate > updates, std::string_view expected_primary_sha256, SourceArtifactReadbackValidator readback_validator)
std::string ComputeSourceArtifactSha256(std::string_view content)
absl::StatusOr< std::string > ComputeSourceArtifactFileSha256(const fs::path &path)
absl::StatusOr< NativeSaveContext > GetNativeSaveContext(const std::string &, const std::string &)
absl::StatusOr< std::unique_ptr< SourceArtifactPublicationLock > > AcquireSourceArtifactPublicationLock(const std::vector< fs::path > &targets, const SourceArtifactPublisherLabels &labels, const std::vector< fs::path > &read_only_targets)
Rom * rom()
Get the current ROM instance.
float ParseFloat(const std::string &value)
Definition project.cc:87
std::string ResolveOptionalPath(const std::filesystem::path &base_dir, const std::string &value)
Definition project.cc:286
void RemoveProjectSaveTempFile(const std::filesystem::path &temp_path)
Definition project.cc:322
bool ContainsLoneCarriageReturn(const std::string &content)
Definition project.cc:1357
std::vector< std::string > ParsePositionalStringList(const std::string &value)
Definition project.cc:112
absl::Status WriteProjectFileAtomicallyImpl(const std::filesystem::path &target_path, absl::string_view contents, bool replace_existing)
Definition project.cc:327
std::vector< uint16_t > ParseHexUintList(const std::string &value)
Definition project.cc:128
std::string ToLowerCopy(std::string value)
Definition project.cc:58
constexpr absl::string_view kProjectFileFooter
Definition project.cc:1367
bool ParseBool(const std::string &value)
Definition project.cc:83
std::pair< std::string, std::string > ParseDefineToken(const std::string &value)
Definition project.cc:278
std::optional< uint32_t > ParseHexUint32(const std::string &value)
Definition project.cc:156
bool IsKnownProjectSection(absl::string_view name)
Definition project.cc:1370
std::string FormatHexUint32(uint32_t value)
Definition project.cc:261
ProjectFilePassthrough::Section & PassthroughSection(ProjectFilePassthrough &passthrough, const std::string &name)
Definition project.cc:1411
std::map< uint16_t, std::vector< uint16_t > > ParseHexUintListMap(const std::string &value)
Definition project.cc:235
std::string FormatHexUintListMap(const std::map< uint16_t, std::vector< uint16_t > > &values)
Definition project.cc:253
bool IsGeneratedProjectComment(absl::string_view line, bool before_sections)
Definition project.cc:1398
std::string SanitizeStorageKey(absl::string_view input)
Definition project.cc:265
std::filesystem::path MakeProjectSaveTempPath(const std::filesystem::path &target_path)
Definition project.cc:312
std::string MergeProjectFilePassthrough(const std::string &canonical, const ProjectFilePassthrough &passthrough)
Definition project.cc:1429
std::string FormatHexUintList(const std::vector< uint16_t > &values)
Definition project.cc:176
void MigrateGfxGroupLabelKeys(std::unordered_map< std::string, std::unordered_map< std::string, std::string > > &labels)
Definition project.cc:188
bool IsKeyLine(absl::string_view line)
Definition project.cc:1422
std::string BasenameLower(const std::string &path)
Definition project.cc:298
std::vector< std::string > ParseStringList(const std::string &value)
Definition project.cc:95
std::string RomRoleToString(RomRole role)
Definition project.cc:461
absl::Status WriteProjectFileAtomically(absl::string_view target_path, absl::string_view contents, bool replace_existing)
Definition project.cc:453
RomRole ParseRomRole(absl::string_view value)
Definition project.cc:475
const std::string kRecentFilesFilename
Definition project.h:627
RomWritePolicy ParseRomWritePolicy(absl::string_view value)
Definition project.cc:503
std::string RomWritePolicyToString(RomWritePolicy policy)
Definition project.cc:489
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:1582
ResourceLabelProvider & GetResourceLabels()
Get the global ResourceLabelProvider instance.
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
Room transition destination.
Definition zelda.h:448
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:137
std::vector< std::string > annotations
Definition project.h:140
std::vector< uint16_t > track_object_ids
Definition project.h:104
std::vector< uint16_t > minecart_sprite_ids
Definition project.h:105
std::vector< uint16_t > track_stop_tiles
Definition project.h:100
std::vector< uint16_t > track_tiles
Definition project.h:99
std::vector< uint16_t > track_switch_tiles
Definition project.h:101
std::vector< uint16_t > reserved_sheets
Definition project.h:118
std::map< uint16_t, std::vector< uint16_t > > reserved_blocks
Definition project.h:120
std::vector< uint16_t > flagged_sheets
Definition project.h:119
Planning editor sidecar ([planning]).
Definition project.h:170
Lines of a .yaze file that this build does not model.
Definition project.h:196
std::vector< Section > sections
Definition project.h:208
std::vector< std::string > tags
Definition project.h:47
std::string CreateOrGetLabel(const std::string &type, const std::string &key, const std::string &defaultValue)
Definition project.cc:3604
std::string GetLabel(const std::string &type, const std::string &key)
Definition project.cc:3591
void EditLabel(const std::string &type, const std::string &key, const std::string &newValue)
Definition project.cc:3573
bool LoadLabels(const std::string &filename)
Definition project.cc:3488
void SelectableLabelWithNameEdit(bool selected, const std::string &type, const std::string &key, const std::string &defaultValue)
Definition project.cc:3579
std::unordered_map< std::string, std::unordered_map< std::string, std::string > > labels_
Definition project.h:623
std::string expected_hash
Definition project.h:228
RomWritePolicy write_policy
Definition project.h:229
std::map< std::string, std::string > custom_keybindings
Definition project.h:88
std::vector< std::string > saved_layouts
Definition project.h:69
std::map< std::string, bool > editor_visibility
Definition project.h:90
std::vector< std::string > recent_files
Definition project.h:89
std::vector< std::string > favorite_models
Definition project.h:413
std::vector< std::string > model_chain
Definition project.h:414
Modern project structure with comprehensive settings consolidation.
Definition project.h:290
std::string rom_backup_folder
Definition project.h:309
std::unordered_map< int, std::vector< std::string > > custom_object_files
Definition project.h:358
absl::Status ResetToDefaults()
Definition project.cc:2539
std::string custom_objects_folder
Definition project.h:330
absl::Status RepairProject()
Definition project.cc:2614
std::string MakeStorageKey(absl::string_view suffix) const
Definition project.cc:1337
static std::string ResolveBundleRoot(const std::string &path)
Definition project.cc:581
absl::Status ValidateSavedSourceBindings(const std::string &content) const
Definition project.cc:804
struct yaze::project::YazeProject::MusicPersistence music_persistence
bool project_opened() const
Definition project.h:538
std::string sprite_catalog_file
Definition project.h:334
absl::StatusOr< std::string > SerializeToString() const
Definition project.cc:1586
std::string zscream_project_file
Definition project.h:430
absl::Status ExportForZScream(const std::string &target_path)
Definition project.cc:2506
ProjectMetadata metadata
Definition project.h:302
absl::Status SaveToYazeFormat(bool replace_existing=true)
Definition project.cc:2450
std::string native_save_transfer_proof_file
Definition project.h:351
absl::Status ImportZScreamProject(const std::string &zscream_project_path)
Definition project.cc:2483
absl::Status SaveAllSettings()
Definition project.cc:2534
absl::Status LoadFromString(const std::string &content, const std::string &project_path)
Definition project.cc:773
absl::Status ImportLabelsFromZScreamContent(const std::string &content)
Import labels from ZScream format content directly.
Definition project.cc:3712
std::string git_repository
Definition project.h:390
core::HackManifest hack_manifest
Definition project.h:376
void InitializeResourceLabelProvider()
Initialize the global ResourceLabelProvider with this project's labels.
Definition project.cc:3736
absl::Status ParseFromString(const std::string &content)
Definition project.cc:1987
std::string cutscene_shots
Definition project.h:321
std::vector< std::string > additional_roms
Definition project.h:310
std::string native_save_receipt_file
Definition project.h:352
std::string lyra_terrain_file
Definition project.h:342
std::string terrain_catalog_file
Definition project.h:339
std::string patches_folder
Definition project.h:316
absl::Status LoadFromYazeFormat(const std::string &project_path)
Definition project.cc:2429
std::unordered_map< std::string, std::unordered_map< std::string, std::string > > resource_labels
Definition project.h:369
std::vector< std::string > AtlasAnnotationPaths() const
Definition project.cc:2747
std::string GenerateProjectId() const
Definition project.cc:3186
absl::Status Create(const std::string &project_name, const std::string &base_path)
Definition project.cc:528
std::string assets_folder
Definition project.h:315
GraphicsSheetSettings graphics_sheets
Definition project.h:364
std::string AtlasSymbolsPath() const
Definition project.cc:2756
absl::Status LoadAllSettings()
Definition project.cc:2528
std::string labels_filename
Definition project.h:317
std::vector< std::string > asm_sources
Definition project.h:387
std::string hack_manifest_file
Definition project.h:332
std::string GetDisplayName() const
Definition project.cc:2648
PlanningSettings planning
Definition project.h:366
std::string PlanningFilePath() const
Definition project.cc:2760
std::string native_save_producer_file
Definition project.h:350
std::vector< std::string > GetMissingFiles() const
Definition project.cc:2589
std::string lyra_build_request_file
Definition project.h:345
WorkspaceSettings workspace_settings
Definition project.h:362
std::string GetZ3dkArtifactPath(absl::string_view artifact_name) const
Definition project.cc:2790
std::string output_folder
Definition project.h:383
std::string asm_entry_point
Definition project.h:386
std::string GetRelativePath(const std::string &absolute_path) const
Definition project.cc:2655
std::vector< SpriteAssetBinding > sprite_assets
Definition project.h:336
absl::Status InitializeEmbeddedLabels(const std::unordered_map< std::string, std::unordered_map< std::string, std::string > > &labels)
Definition project.cc:3619
ProjectFilePassthrough file_passthrough
Definition project.h:442
absl::StatusOr< std::optional< std::string > > ResolveExpandedBuildRunTarget(const std::string &editing_source_path) const
Definition project.cc:889
absl::Status SaveAs(const std::string &new_path)
Definition project.cc:1195
absl::Status SaveNew()
Definition project.cc:769
bool IsOracleCompatible() const
Definition project.cc:1207
struct yaze::project::YazeProject::AgentSettings agent_settings
std::string custom_collision_json
Definition project.h:328
DungeonOverlaySettings dungeon_overlay
Definition project.h:363
absl::Status SaveAsWithLyraTerrain(const std::string &new_path, const core::LyraTerrainDocument &terrain, const std::optional< std::string > &expected_source_bytes)
Definition project.cc:1214
std::string palette_catalog_file
Definition project.h:355
absl::Status ImportFromZScreamFormat(const std::string &project_path)
Definition project.cc:2768
std::string GetAbsolutePath(const std::string &relative_path) const
Definition project.cc:2675
std::string overworld_graphics_preview
Definition project.h:323
std::string GetLabel(const std::string &resource_type, int id, const std::string &default_value="") const
Definition project.cc:3677
std::string sprite_source_root
Definition project.h:335
absl::Status Open(const std::string &project_path)
Definition project.cc:607
absl::Status ImportLabelsFromZScream(const std::string &filepath)
Import labels from a ZScream DefaultNames.txt file.
Definition project.cc:3692
std::string last_build_hash
Definition project.h:392
std::map< std::string, std::string > zscream_mappings
Definition project.h:431
std::string dungeon_tile_patterns
Definition project.h:325
absl::Status Validate() const
Definition project.cc:2544
core::FeatureFlags::Flags feature_flags
Definition project.h:361
std::vector< std::string > build_configurations
Definition project.h:384
core::RomAddressOverrides rom_address_overrides
Definition project.h:367
std::string symbols_filename
Definition project.h:318
Z3dkSettings z3dk_settings
Definition project.h:379
std::vector< std::string > include_paths
Definition project.h:250
std::string std_includes_path
Definition project.h:254
std::vector< std::string > main_files
Definition project.h:253
std::vector< std::string > emits
Definition project.h:252
std::vector< std::pair< std::string, std::string > > defines
Definition project.h:251
Z3dkArtifactPaths artifact_paths
Definition project.h:271
std::optional< bool > lsp_log_enabled
Definition project.h:261
std::vector< Z3dkMemoryRange > prohibited_memory_ranges
Definition project.h:260
std::string std_defines_path
Definition project.h:255