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"
28#include "imgui/imgui.h"
34#include "yaze_config.h"
37#if defined(YAZE_WITH_Z3DK) && __has_include("z3dk_core/config.h")
38#include "z3dk_core/config.h"
60 value.begin(), value.end(), value.begin(),
61 [](
unsigned char c) { return static_cast<char>(std::tolower(c)); });
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)
71 std::string key = line.substr(0, eq_pos);
72 std::string value = line.substr(eq_pos + 1);
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);
84 return value ==
"true" || value ==
"1" || value ==
"yes";
89 return std::stof(value);
96 std::vector<std::string> result;
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);
113 std::vector<std::string> result;
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));
129 std::vector<uint16_t> result;
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);
141 result.push_back(
static_cast<uint16_t
>(std::stoul(token,
nullptr, 16)));
147 result.push_back(
static_cast<uint16_t
>(std::stoul(token,
nullptr, 10)));
160 std::string token = value;
161 if (token.rfind(
"0x", 0) == 0 || token.rfind(
"0X", 0) == 0) {
162 token = token.substr(2);
164 return static_cast<uint32_t
>(std::stoul(token,
nullptr, 16));
170 return static_cast<uint32_t
>(std::stoul(token,
nullptr, 10));
177 return absl::StrJoin(values,
",", [](std::string* out, uint16_t value) {
178 out->append(absl::StrFormat(
"0x%02X", value));
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);
194 auto is_decimal = [](
char c) {
195 return c >=
'0' && c <=
'9';
197 auto is_hex = [](
char c) {
198 return std::isxdigit(
static_cast<unsigned char>(c)) != 0;
200 for (
const char* type : {
"blockset",
"roomset",
"spriteset",
"paletteset"}) {
201 auto found = labels.find(type);
202 if (found == labels.end()) {
205 std::unordered_map<std::string, std::string> migrated;
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)) {
217 std::to_string(std::stoul(std::string(digits),
nullptr, 10));
218 }
else if (pass == 2 && !all_of(digits, is_decimal)) {
221 prefixed && all_of(digits, is_hex)
222 ? std::to_string(std::stoul(std::string(digits),
nullptr, 16))
225 if (!canonical.empty()) {
226 migrated.emplace(canonical, value);
230 found->second = std::move(migrated);
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) {
244 if (keys.size() != 1) {
247 result[keys.front()] =
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,
","));
262 return absl::StrFormat(
"0x%06X", value);
266 std::string key(input);
267 for (
char& c : key) {
268 if (!std::isalnum(
static_cast<unsigned char>(c))) {
279 auto [key, parsed_value] = ParseKeyValue(value);
283 return {key, parsed_value.empty() ?
"1" : parsed_value};
287 const std::string& value) {
291 std::filesystem::path path(value);
292 if (path.is_absolute()) {
293 return path.lexically_normal().string();
295 return (base_dir / path).lexically_normal().string();
299 return ToLowerCopy(std::filesystem::path(path).filename().
string());
304 return static_cast<uint64_t
>(::GetCurrentProcessId());
305#elif defined(__EMSCRIPTEN__)
308 return static_cast<uint64_t
>(::getpid());
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;
318 std::to_string(next_temp_id.fetch_add(1, std::memory_order_relaxed));
323 std::error_code remove_error;
324 std::filesystem::remove(temp_path, remove_error);
328 const std::filesystem::path& target_path, absl::string_view contents,
329 bool replace_existing) {
331 if (!replace_existing) {
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()));
340 return absl::InternalError(absl::StrFormat(
341 "Cannot create project file: %s", target_path.string()));
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) {
348 return absl::InternalError(absl::StrFormat(
349 "Failed to write project file: %s", target_path.string()));
351 return absl::OkStatus();
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()));
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));
368 if (!WriteFile(file, contents.data() + written_total, chunk, &written,
373 return absl::InternalError(absl::StrFormat(
374 "Failed to write temporary project file: %s", temp_path.string()));
376 written_total += written;
378 if (!FlushFileBuffers(file)) {
381 return absl::InternalError(absl::StrFormat(
382 "Failed to write temporary project file: %s", temp_path.string()));
384 if (!CloseHandle(file)) {
386 return absl::InternalError(absl::StrFormat(
387 "Failed to close temporary project file: %s", temp_path.string()));
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()));
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) {
402 return absl::InternalError(absl::StrFormat(
403 "Failed to write temporary project file: %s", temp_path.string()));
407 return absl::InternalError(absl::StrFormat(
408 "Failed to close temporary project file: %s", temp_path.string()));
412 std::error_code rename_error;
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());
421#elif defined(__EMSCRIPTEN__)
422 std::filesystem::rename(temp_path, target_path, rename_error);
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());
434 bool target_already_exists = rename_error == std::errc::file_exists;
436 target_already_exists = target_already_exists ||
437 rename_error.value() == ERROR_FILE_EXISTS ||
438 rename_error.value() == ERROR_ALREADY_EXISTS;
440 if (!replace_existing && target_already_exists) {
441 return absl::AlreadyExistsError(absl::StrFormat(
442 "Project file already exists: %s", target_path.string()));
444 return absl::InternalError(
445 absl::StrFormat(
"Failed to replace project file %s: %s",
446 target_path.string(), rename_error.message()));
449 return absl::OkStatus();
454 absl::string_view contents,
455 bool replace_existing) {
456 return WriteProjectFileAtomicallyImpl(
457 std::filesystem::path(std::string(target_path)), contents,
476 std::string lower = ToLowerCopy(std::string(value));
477 if (lower ==
"base") {
480 if (lower ==
"patched") {
483 if (lower ==
"release") {
504 std::string lower = ToLowerCopy(std::string(value));
505 if (lower ==
"allow") {
508 if (lower ==
"block") {
511 if (lower ==
"read_only") {
521 provider.SetHackManifest(
nullptr);
524 provider.SetProjectLabels(
nullptr);
529 const std::string& base_path) {
531 filepath = base_path +
"/" + project_name +
".yaze";
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");
548#ifndef __EMSCRIPTEN__
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");
589 auto current = std::filesystem::path(path).lexically_normal();
591 for (; !current.empty(); current = current.parent_path()) {
592 if (current.extension() ==
".yazeproj") {
594 if (std::filesystem::is_directory(current, ec) && !ec) {
595 return current.string();
599 if (current == current.parent_path()) {
611 std::string resolved_path = project_path;
613 if (!bundle_root.empty() && bundle_root != project_path) {
615 resolved_path = bundle_root;
618#ifndef __EMSCRIPTEN__
623 std::error_code absolute_ec;
624 const auto absolute_path =
625 std::filesystem::absolute(resolved_path, absolute_ec).lexically_normal();
627 resolved_path = absolute_path.string();
636 auto storage_or = platform::WasmStorage::LoadProject(storage_key);
637 if (storage_or.ok()) {
643 absl::Status load_status;
644 if (resolved_path.ends_with(
".yazeproj")) {
647 const std::filesystem::path bundle_path(resolved_path);
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));
657 const std::filesystem::path project_file = bundle_path /
"project.yaze";
660 if (!std::filesystem::exists(project_file, ec) || ec) {
663 name = bundle_path.stem().string();
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");
689 const std::filesystem::path rom_candidate = bundle_path /
"rom";
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) {
701 }
else if (std::filesystem::exists(code_dir, ec) &&
702 std::filesystem::is_directory(code_dir, ec) && !ec) {
717 }
else if (resolved_path.ends_with(
".yaze")) {
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();
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);
738 return absl::InvalidArgumentError(
739 absl::StrFormat(
"Cannot open project file: %s", resolved_path));
741 }
else if (resolved_path.ends_with(
".zsproj")) {
745 return absl::InvalidArgumentError(
"Unsupported project file format");
748 if (!load_status.ok()) {
762 return absl::OkStatus();
774 const std::string& project_path) {
775 if (project_path.empty()) {
776 return absl::InvalidArgumentError(
"Project file path cannot be empty");
781#ifndef __EMSCRIPTEN__
783 const auto absolute_path =
784 std::filesystem::absolute(project_path, ec).lexically_normal();
786 ec ? std::filesystem::path(project_path).lexically_normal().string()
787 : absolute_path.string();
794 }
catch (
const std::exception& error) {
795 return absl::InvalidArgumentError(
796 absl::StrFormat(
"Invalid project file value: %s", error.what()));
801 return absl::OkStatus();
805 const std::string& content)
const {
811 }
catch (
const std::exception& error) {
812 return absl::InvalidArgumentError(
813 std::string(
"Invalid saved project bindings: ") + error.what());
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() ==
'#')
825 if (trimmed.front() ==
'[' && trimmed.back() ==
']') {
826 section = std::string(trimmed.substr(1, trimmed.size() - 2));
830 if (section ==
"workspace" || section ==
"keybindings" ||
831 section ==
"editor_visibility" || section ==
"agent_settings")
833 const auto [key, value] = ParseKeyValue(std::string(trimmed));
834 if (key.empty() || (section ==
"project" &&
835 (key ==
"last_modified" || key ==
"created_date")))
837 values[{section, key}] = value;
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);
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);
853 for (
const auto& [key, value] : saved_bindings) {
854 if (live_bindings.find(key) == live_bindings.end())
855 return mismatch(key);
857 return absl::OkStatus();
861#if defined(YAZE_WITH_JSON) && !defined(__EMSCRIPTEN__)
862absl::Status ExpandedRunError(
const std::string& message) {
863 return absl::FailedPreconditionError(
"Expanded run target unavailable: " +
866absl::Status ExpandedRunRequire(
bool valid,
const std::string& message) {
867 return valid ? absl::OkStatus() : ExpandedRunError(message);
869std::string ExpandedRunPathText(
const std::filesystem::path& path) {
870 const auto text = path.generic_u8string();
871 return std::string(text.begin(), text.end());
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");
878 std::error_code error;
879 const auto canonical = std::filesystem::weakly_canonical(path, error);
881 return ExpandedRunError(
"cannot resolve evidence path: " + text);
883 return ExpandedRunPathText(canonical);
888absl::StatusOr<std::optional<std::string>>
890 const std::string& editing_source_path)
const {
892 return std::optional<std::string>{};
894#if !defined(YAZE_WITH_JSON) || defined(__EMSCRIPTEN__)
895 return absl::UnimplementedError(
896 "Expanded run target verification requires desktop JSON support");
898 namespace fs = std::filesystem;
902 editing_source_path.empty()) {
903 return ExpandedRunError(
"complete saved expanded references are required");
907 const auto resolve = [&](
const std::string& ref) {
920 const auto authority,
922 const fs::path target = fs::u8path(candidate);
923 const auto relative =
924 target.lexically_relative(fs::u8path(engine) /
"Roms");
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"));
933 uint64_t remaining = 8ULL * 1024 * 1024 * 1024;
934 std::vector<Json> observed;
935 const auto check_file = [&](
const Json& row) -> absl::Status {
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);
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 &&
947 "an evidence file is missing, changed, or exceeds its bounds"));
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"));
959 return ExpandedRunRequire(actual == expected,
960 "evidence changed: " +
name);
962 const auto check = [&](
const Json& row) -> absl::Status {
964 "too many evidence bindings"));
966 observed.push_back(row);
967 return absl::OkStatus();
969 const auto rows = [&](
const Json& values) -> absl::Status {
971 (values.is_array() || values.is_object()) && values.size() <= 4096,
972 "an evidence binding collection is malformed"));
973 for (
const auto& row : values)
975 return absl::OkStatus();
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()));
990 stream.gcount() ==
static_cast<std::streamsize
>(raw.size()) &&
991 !stream.get(extra) && !stream.bad(),
992 "metadata changed while reading"));
997 const auto load = [&](
const std::string&
name) -> absl::StatusOr<Json> {
1002 throw std::runtime_error(
"evidence nesting is too deep");
1006 "evidence JSON must be an object"));
1009 const auto same_path = [&](
const Json& value,
1010 const std::string& expected) -> absl::Status {
1012 ExpandedRunCanonical(value.
get<std::string>()));
1013 return ExpandedRunRequire(path == expected,
1014 "evidence belongs to another path");
1016 const auto sibling = [&](
const char*
name) {
1017 return ExpandedRunPathText(target.parent_path() /
name);
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"));
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"));
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>()));
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"}) {
1071 prepared.at(flag).is_boolean() && prepared.at(flag).get<
bool>(),
1072 "expanded build admission is incomplete"));
1075 ExpandedRunRequire(!prepared.contains(
"runtime_scope"),
1076 "a private preview is not an ordinary run target"));
1081 const Json* request_binding =
nullptr;
1082 for (
const auto& row : prepared.at(
"inputs")) {
1084 ExpandedRunCanonical(row.at(
"path").get<std::string>()));
1085 if (path == request_path)
1086 request_binding = &row;
1089 ExpandedRunRequire(request_binding !=
nullptr,
1090 "the preparation omits its request binding"));
1093 const auto& live = request.at(
"live");
1094 const auto& plan = prepared.at(
"terrain_plan");
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));
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));
1114 for (
const char* key : {
"producer",
"save_receipt",
"save_context"})
1116 for (
const char* key : {
"observation",
"save_snapshot",
"producer_snapshot",
1117 "context_snapshot"})
1119 const auto& producer = live.at(
"producer_identity");
1124 if (request.contains(
"native_verification"))
1126 if (plan.at(
"enforcement_owner") ==
"oracle.current_builder") {
1128 plan.at(
"enforcement_receipt") == request.at(
"native_verification"),
1129 "the builder enforcement binding differs"));
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));
1138 const auto receipt_path = descriptor +
".native-save.json";
1140 same_path(live.at(
"save_receipt").at(
"path"), receipt_path));
1142 ExpandedRunPathText(authority.path)));
1145 load(ExpandedRunPathText(authority.path)));
1147 receipt.at(
"kind") ==
"yaze.native_save.v1" &&
1148 receipt.at(
"bindings_complete") ==
true &&
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"));
1164 RETURN_IF_ERROR(same_path(context.at(
"native_commit").at(
"receipt_path"),
1167 authority.editing_source_path));
1170 const Json* saved_producer =
nullptr;
1171 for (
const auto& row : receipt.at(
"artifacts")) {
1172 if (row.at(
"role") ==
"producer")
1173 saved_producer = &row;
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"));
1181 ExpandedRunCanonical(
1182 producer.at(
"editor").at(
"path").get<std::string>()));
1186 for (
const auto& binding : observed)
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());
1196 std::string old_filepath =
filepath;
1199 auto status =
Save();
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");
1221 namespace fs = std::filesystem;
1223 return absl::FailedPreconditionError(
1224 "An open project and Lyra terrain are required for Save As");
1227 [](
const fs::path& path) -> absl::StatusOr<std::optional<std::string>> {
1229 if (!fs::exists(path, ec) && !ec) {
1230 return std::nullopt;
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");
1237 std::ifstream stream(path, std::ios::binary);
1239 return absl::PermissionDeniedError(
"Cannot read project Save As file");
1241 std::string bytes{std::istreambuf_iterator<char>(stream), {}};
1243 return absl::DataLossError(
1244 "Could not finish reading project Save As file");
1246 return std::optional<std::string>(std::move(bytes));
1249 auto candidate = *
this;
1251 candidate.NormalizePathsToAbsolute();
1253 const fs::path source_project = fs::absolute(candidate.filepath, ec);
1255 return absl::InvalidArgumentError(
"Cannot resolve the source project path");
1257 const fs::path source_terrain = candidate.lyra_terrain_file;
1258 const fs::path
destination = fs::absolute(new_path, ec).lexically_normal();
1260 return absl::InvalidArgumentError(
1261 "Cannot resolve the project Save As path");
1264 destination_terrain.replace_extension(
".lyra-terrain.json");
1266 candidate.lyra_terrain_file = destination_terrain.string();
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);
1273 if (!candidate.lyra_build_request_file.empty()) {
1274 read_only_targets.push_back(candidate.lyra_build_request_file);
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);
1286 targets, {
"Lyra project",
"Lyra project files"}, read_only_targets));
1288 if (!original_project) {
1289 return absl::NotFoundError(
"The original project file is missing");
1292 if (original_terrain != expected_source_bytes) {
1293 return absl::FailedPreconditionError(
1294 "Lyra terrain changed on disk; refusing stale Save As");
1298 if (previous_terrain) {
1299 return absl::AlreadyExistsError(
1300 "The destination project already owns Lyra terrain; choose another "
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");
1312 const std::string terrain_bytes = terrain.
Serialize();
1315 {{source_project, original_project, *original_project},
1317 {destination_terrain, std::nullopt, terrain_bytes}},
1319 [&]() -> absl::Status {
1321 if (source_after != expected_source_bytes) {
1322 return absl::FailedPreconditionError(
1323 "Lyra terrain changed during Save As");
1327 return absl::DataLossError(
"Saved Lyra terrain is missing");
1332 *
this = std::move(candidate);
1333 return absl::OkStatus();
1341 }
else if (!
name.empty()) {
1344 base = std::filesystem::path(
filepath).stem().string();
1346 base = SanitizeStorageKey(base);
1347 if (suffix.empty()) {
1350 return absl::StrFormat(
"%s_%s", base, suffix);
1358 for (
size_t i = 0; i < content.size(); ++i) {
1359 if (content[i] ==
'\r' &&
1360 (i + 1 >= content.size() || content[i + 1] !=
'\n')) {
1371 static constexpr absl::string_view kKnown[] = {
"project",
1386 "editor_visibility",
1387 "zscream_compatibility",
1389 if (absl::StartsWith(name,
"labels_")) {
1392 return std::find(std::begin(kKnown), std::end(kKnown), name) !=
1402 if (!before_sections) {
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:");
1413 for (
auto& section : passthrough.
sections) {
1414 if (section.name == name) {
1418 passthrough.
sections.push_back({name,
false, {}});
1419 return passthrough.
sections.back();
1423 return !line.empty() && line.front() !=
'#' &&
1424 line.find(
'=') != absl::string_view::npos;
1431 if (passthrough.
empty()) {
1436 struct CanonicalSection {
1439 std::vector<std::string> lines;
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')) {
1449 if (line.size() >= 2 && line.front() ==
'[' && line.back() ==
']') {
1450 canonical_sections.push_back(
1452 std::string(line.substr(1, line.size() - 2)),
1459 (canonical_sections.empty() ? preamble : canonical_sections.back().lines)
1460 .push_back(std::string(line));
1465 for (
const auto& section : passthrough.
sections) {
1466 if (section.name == name) {
1473 std::ostringstream out;
1476 for (
const auto& line : section.lines) {
1477 if (line.kind != Kind::kKnownKey) {
1478 out << line.text <<
"\n";
1483 for (
const auto& line : preamble) {
1484 out << line <<
"\n";
1487 if (
const auto* before = find_section(
""); before !=
nullptr) {
1489 if (!before->lines.empty()) {
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";
1505 merged.push_back(kept);
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)});
1517 }
else if (!pending.empty()) {
1518 anchored.push_back({line.key, std::move(pending)});
1522 std::vector<bool> anchor_written(anchored.size(),
false);
1523 for (
const auto& line : section.lines) {
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";
1531 anchor_written[i] =
true;
1535 out << line <<
"\n";
1537 for (
const auto& [text, comments] : unknown) {
1538 for (
const auto& comment : comments) {
1539 out << comment <<
"\n";
1541 out << text <<
"\n";
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";
1551 for (
const auto& comment : pending) {
1552 out << comment <<
"\n";
1559 for (
const auto& section : passthrough.
sections) {
1560 if (section.name.empty() ||
1561 std::find(merged.begin(), merged.end(), §ion) != merged.end()) {
1564 const bool has_content = std::any_of(
1565 section.lines.begin(), section.lines.end(),
1566 [](
const auto& line) { return line.kind != Kind::kKnownKey; });
1570 out <<
"[" << section.name <<
"]\n";
1583 return IsKnownProjectSection(name);
1587 std::ostringstream file;
1590 file <<
"# yaze Project File\n";
1591 file <<
"# Format Version: 2.0\n";
1596 file <<
"[project]\n";
1597 file <<
"name=" <<
name <<
"\n";
1607 file <<
"tags=" << absl::StrJoin(
metadata.
tags,
",") <<
"\n\n";
1610 file <<
"[files]\n";
1619 file <<
"overworld_graphics_preview="
1623 file <<
"dungeon_tile_patterns="
1627 file <<
"cutscene_shots="
1631 file <<
"custom_collision_json="
1637 file <<
"palette_catalog_file="
1639 file <<
"terrain_catalog_file="
1641 file <<
"lyra_terrain_file="
1643 file <<
"lyra_build_request_file="
1646 file <<
"native_save_producer_file="
1650 file <<
"native_save_transfer_proof_file="
1655 file <<
"native_save_receipt_file="
1659 file <<
"sprite_catalog_file="
1661 file <<
"sprite_source_root="
1663 file <<
"additional_roms=" << absl::StrJoin(
additional_roms,
",") <<
"\n\n";
1665 file <<
"[sprite_assets]\n";
1669 for (
auto& source : asset.graphics_sources) {
1670 if (source.kind ==
"bin") {
1686 file <<
"[feature_flags]\n";
1687 file <<
"load_custom_overworld="
1690 file <<
"apply_zs_custom_overworld_asm="
1694 file <<
"enable_special_world_expansion="
1698 file <<
"save_with_change_queue="
1700 file <<
"save_dungeon_maps="
1702 file <<
"save_overworld_maps="
1705 file <<
"save_overworld_entrances="
1708 file <<
"save_overworld_exits="
1711 file <<
"save_overworld_items="
1714 file <<
"save_overworld_sprites="
1717 file <<
"save_overworld_properties="
1720 file <<
"save_dungeon_objects="
1722 file <<
"save_dungeon_sprites="
1724 file <<
"save_dungeon_room_headers="
1726 file <<
"save_dungeon_torches="
1728 file <<
"save_dungeon_pits="
1730 file <<
"save_dungeon_blocks="
1732 file <<
"save_dungeon_collision="
1734 file <<
"save_dungeon_water_fill_zones="
1737 file <<
"save_dungeon_chests="
1739 file <<
"save_dungeon_pot_items="
1741 file <<
"save_dungeon_entrances="
1743 file <<
"save_dungeon_palettes="
1745 file <<
"save_graphics_sheet="
1747 file <<
"save_all_palettes="
1749 file <<
"save_gfx_groups="
1753 file <<
"enable_custom_objects="
1757 file <<
"[workspace]\n";
1762 file <<
"autosave_enabled="
1766 file <<
"backup_on_save="
1770 file <<
"backup_keep_daily="
1776 file <<
"show_collision="
1778 file <<
"prefer_hmagic_names="
1782 file <<
"saved_layouts="
1789 if (track_tiles.empty()) {
1790 for (uint16_t tile = 0xB0; tile <= 0xBE; ++tile) {
1791 track_tiles.push_back(tile);
1795 if (track_stop_tiles.empty()) {
1796 track_stop_tiles = {0xB7, 0xB8, 0xB9, 0xBA};
1799 if (track_switch_tiles.empty()) {
1800 track_switch_tiles = {0xD0, 0xD1, 0xD2, 0xD3};
1803 if (track_object_ids.empty()) {
1804 track_object_ids = {0x31};
1807 if (minecart_sprite_ids.empty()) {
1808 minecart_sprite_ids = {0xA3};
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)
1815 file <<
"track_object_ids=" << FormatHexUintList(track_object_ids) <<
"\n";
1816 file <<
"minecart_sprite_ids=" << FormatHexUintList(minecart_sprite_ids)
1820 file <<
"[graphics_sheets]\n";
1821 file <<
"reserved_sheets="
1823 file <<
"flagged_sheets="
1825 file <<
"reserved_blocks="
1830 file <<
"[atlas]\n";
1838 file <<
"[planning]\n";
1843 file <<
"[rom_addresses]\n";
1845 file << key <<
"=" << FormatHexUint32(value) <<
"\n";
1851 file <<
"[custom_objects]\n";
1853 file << absl::StrFormat(
"object_0x%X", object_id) <<
"="
1854 << absl::StrJoin(files,
",") <<
"\n";
1860 file <<
"[agent_settings]\n";
1865 file <<
"custom_system_prompt="
1868 file <<
"use_custom_prompt="
1870 file <<
"show_reasoning="
1878 file <<
"stream_responses="
1880 file <<
"favorite_models="
1885 file <<
"enable_tool_resources="
1887 file <<
"enable_tool_dungeon="
1889 file <<
"enable_tool_overworld="
1891 file <<
"enable_tool_messages="
1893 file <<
"enable_tool_dialogue="
1895 file <<
"enable_tool_gui="
1897 file <<
"enable_tool_music="
1899 file <<
"enable_tool_sprite="
1901 file <<
"enable_tool_emulator="
1903 file <<
"enable_tool_memory_inspector="
1911 file <<
"[keybindings]\n";
1913 file << key <<
"=" << value <<
"\n";
1920 file <<
"[editor_visibility]\n";
1922 file << key <<
"=" << (value ?
"true" :
"false") <<
"\n";
1929 if (!labels.empty()) {
1930 file <<
"[labels_" << type <<
"]\n";
1931 for (
const auto& [key, value] : labels) {
1932 file << key <<
"=" << value <<
"\n";
1939 file <<
"[build]\n";
1943 file <<
"track_changes=" << (
track_changes ?
"true" :
"false") <<
"\n";
1948 file <<
"asm_sources=" << absl::StrJoin(
asm_sources,
",") <<
"\n";
1953 file <<
"[music]\n";
1954 file <<
"persist_custom_music="
1961 file <<
"[zscream_compatibility]\n";
1964 file << key <<
"=" << value <<
"\n";
1969 file <<
"# End of YAZE Project File\n";
1977 std::string serialized =
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");
1988 if (ContainsLoneCarriageReturn(content)) {
1989 return absl::InvalidArgumentError(
1990 "Project file contains unsupported lone carriage returns");
2011 std::istringstream stream(content);
2013 std::string current_section;
2017 while (std::getline(stream, line)) {
2021 if (!line.empty() && line.back() ==
'\r') {
2025 if (line.find_first_not_of(
" \t") == std::string::npos)
2028 if (line[0] ==
'#') {
2029 if (!IsGeneratedProjectComment(line, current_section.empty())) {
2030 passthrough_section->
lines.push_back(
2036 if (line.front() ==
'[' && line.back() ==
']') {
2037 current_section = line.substr(1, line.length() - 2);
2038 passthrough_section =
2040 passthrough_section->
known = IsKnownProjectSection(current_section);
2041 if (current_section ==
"atlas") {
2043 }
else if (current_section ==
"planning") {
2049 auto [key, value] = ParseKeyValue(line);
2052 bool understood = !key.empty();
2055 }
else if (current_section ==
"project") {
2058 else if (key ==
"description")
2060 else if (key ==
"author")
2062 else if (key ==
"license")
2064 else if (key ==
"version")
2066 else if (key ==
"created_date")
2068 else if (key ==
"last_modified")
2070 else if (key ==
"yaze_version")
2072 else if (key ==
"created_by")
2074 else if (key ==
"tags")
2076 else if (key ==
"project_id")
2080 }
else if (current_section ==
"files") {
2081 if (key ==
"rom_filename")
2083 else if (key ==
"rom_backup_folder")
2085 else if (key ==
"code_folder")
2087 else if (key ==
"assets_folder")
2089 else if (key ==
"patches_folder")
2091 else if (key ==
"labels_filename")
2093 else if (key ==
"symbols_filename")
2095 else if (key ==
"overworld_graphics_preview")
2097 else if (key ==
"dungeon_tile_patterns")
2099 else if (key ==
"cutscene_shots")
2101 else if (key ==
"custom_collision_json")
2103 else if (key ==
"output_folder")
2105 else if (key ==
"custom_objects_folder")
2107 else if (key ==
"palette_catalog_file")
2109 else if (key ==
"terrain_catalog_file")
2111 else if (key ==
"lyra_terrain_file")
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")
2123 else if (key ==
"sprite_catalog_file")
2125 else if (key ==
"sprite_source_root")
2127 else if (key ==
"additional_roms")
2131 }
else if (current_section ==
"sprite_assets") {
2132 if (key.rfind(
"asset_", 0) != 0 || value.size() > 65536 ||
2134 return absl::InvalidArgumentError(
"Invalid sprite_assets section");
2137 return asset.status();
2139 if (prior.zsm_path == asset->zsm_path)
2140 return absl::InvalidArgumentError(
"Duplicate sprite asset path");
2142 }
else if (current_section ==
"rom") {
2145 else if (key ==
"expected_hash")
2147 else if (key ==
"write_policy")
2151 }
else if (current_section ==
"feature_flags") {
2152 if (key ==
"load_custom_overworld")
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")
2160 else if (key ==
"save_dungeon_maps")
2162 else if (key ==
"save_overworld_maps")
2164 else if (key ==
"save_overworld_entrances")
2166 else if (key ==
"save_overworld_exits")
2168 else if (key ==
"save_overworld_items")
2170 else if (key ==
"save_overworld_sprites")
2172 else if (key ==
"save_overworld_properties")
2174 else if (key ==
"save_dungeon_objects")
2176 else if (key ==
"save_dungeon_sprites")
2178 else if (key ==
"save_dungeon_room_headers")
2180 else if (key ==
"save_dungeon_torches")
2182 else if (key ==
"save_dungeon_pits")
2184 else if (key ==
"save_dungeon_blocks")
2186 else if (key ==
"save_dungeon_collision")
2188 else if (key ==
"save_dungeon_water_fill_zones")
2190 else if (key ==
"save_dungeon_chests")
2192 else if (key ==
"save_dungeon_pot_items")
2194 else if (key ==
"save_dungeon_entrances")
2196 else if (key ==
"save_dungeon_palettes")
2198 else if (key ==
"save_graphics_sheet")
2200 else if (key ==
"save_all_palettes")
2202 else if (key ==
"save_gfx_groups")
2204 else if (key ==
"save_messages")
2206 else if (key ==
"enable_custom_objects")
2210 }
else if (current_section ==
"workspace") {
2211 if (key ==
"font_global_scale")
2213 else if (key ==
"dark_mode")
2215 else if (key ==
"ui_theme")
2217 else if (key ==
"autosave_enabled")
2219 else if (key ==
"autosave_interval_secs")
2221 else if (key ==
"backup_on_save")
2223 else if (key ==
"backup_retention_count")
2225 else if (key ==
"backup_keep_daily")
2227 else if (key ==
"backup_keep_daily_days")
2229 else if (key ==
"show_grid")
2231 else if (key ==
"show_collision")
2233 else if (key ==
"prefer_hmagic_names")
2235 else if (key ==
"last_layout_preset")
2237 else if (key ==
"saved_layouts")
2239 else if (key ==
"recent_files")
2243 }
else if (current_section ==
"dungeon_overlay") {
2244 if (key ==
"track_tiles")
2246 else if (key ==
"track_stop_tiles")
2248 else if (key ==
"track_switch_tiles")
2250 else if (key ==
"track_object_ids")
2252 else if (key ==
"minecart_sprite_ids")
2256 }
else if (current_section ==
"graphics_sheets") {
2257 if (key ==
"reserved_sheets")
2259 else if (key ==
"flagged_sheets")
2261 else if (key ==
"reserved_blocks")
2265 }
else if (current_section ==
"atlas") {
2266 if (key ==
"annotations")
2268 else if (key ==
"defaults")
2270 else if (key ==
"symbols")
2272 else if (key ==
"export_folder")
2276 }
else if (current_section ==
"planning") {
2281 }
else if (current_section ==
"rom_addresses") {
2282 auto parsed = ParseHexUint32(value);
2283 if (parsed.has_value()) {
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);
2293 auto parsed = ParseHexUint32(id_token);
2294 if (parsed.has_value()) {
2296 ParsePositionalStringList(value);
2300 }
else if (current_section ==
"agent_settings") {
2301 if (key ==
"ai_provider")
2303 else if (key ==
"ai_model")
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")
2313 else if (key ==
"show_reasoning")
2315 else if (key ==
"verbose")
2317 else if (key ==
"max_tool_iterations")
2319 else if (key ==
"max_retry_attempts")
2321 else if (key ==
"temperature")
2323 else if (key ==
"top_p")
2325 else if (key ==
"max_output_tokens")
2327 else if (key ==
"stream_responses")
2329 else if (key ==
"favorite_models")
2331 else if (key ==
"model_chain")
2333 else if (key ==
"chain_mode")
2335 else if (key ==
"enable_tool_resources")
2337 else if (key ==
"enable_tool_dungeon")
2339 else if (key ==
"enable_tool_overworld")
2341 else if (key ==
"enable_tool_messages")
2343 else if (key ==
"enable_tool_dialogue")
2345 else if (key ==
"enable_tool_gui")
2347 else if (key ==
"enable_tool_music")
2349 else if (key ==
"enable_tool_sprite")
2351 else if (key ==
"enable_tool_emulator")
2353 else if (key ==
"enable_tool_memory_inspector")
2355 else if (key ==
"builder_blueprint_path")
2359 }
else if (current_section ==
"build") {
2360 if (key ==
"build_script")
2362 else if (key ==
"output_folder")
2364 else if (key ==
"git_repository")
2366 else if (key ==
"track_changes")
2368 else if (key ==
"build_configurations")
2370 else if (key ==
"build_target")
2372 else if (key ==
"asm_entry_point")
2374 else if (key ==
"asm_sources")
2376 else if (key ==
"last_build_hash")
2378 else if (key ==
"build_number")
2382 }
else if (current_section.rfind(
"labels_", 0) == 0) {
2383 std::string label_type = current_section.substr(7);
2385 }
else if (current_section ==
"keybindings") {
2387 }
else if (current_section ==
"editor_visibility") {
2389 }
else if (current_section ==
"zscream_compatibility") {
2390 if (key ==
"original_project_file")
2394 }
else if (current_section ==
"music") {
2395 if (key ==
"persist_custom_music")
2397 else if (key ==
"storage_key")
2399 else if (key ==
"last_saved_at")
2406 passthrough_section->
lines.push_back(
2426 return absl::OkStatus();
2430#ifdef __EMSCRIPTEN__
2432 auto storage_or = platform::WasmStorage::LoadProject(storage_key);
2433 if (storage_or.ok()) {
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));
2444 std::stringstream buffer;
2445 buffer << file.rdbuf();
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");
2467#ifdef __EMSCRIPTEN__
2468 auto storage_status = platform::WasmStorage::SaveProject(
2470 if (!storage_status.ok()) {
2471 return storage_status;
2480 return absl::OkStatus();
2484 const std::string& zscream_project_path) {
2490 std::filesystem::path zs_path(zscream_project_path);
2491 name = zs_path.stem().string() +
"_imported";
2503 return absl::OkStatus();
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));
2515 file <<
"# ZScream Compatible Project File\n";
2517 file <<
"name=" <<
name <<
"\n";
2525 return absl::OkStatus();
2545 std::vector<std::string> errors;
2548 errors.push_back(
"Project name is required");
2550 errors.push_back(
"Project file path is required");
2552 errors.push_back(
"ROM file is required");
2554#ifndef __EMSCRIPTEN__
2558 errors.push_back(
"ROM file does not exist: " +
rom_filename);
2563 errors.push_back(
"Code folder does not exist: " +
code_folder);
2573 errors.push_back(
"Hack manifest file does not exist: " +
2576 errors.push_back(
"Hack manifest file failed to load: " +
2582 if (!errors.empty()) {
2583 return absl::InvalidArgumentError(absl::StrJoin(errors,
"; "));
2586 return absl::OkStatus();
2590 std::vector<std::string> missing;
2592#ifndef __EMSCRIPTEN__
2615#ifdef __EMSCRIPTEN__
2617 return absl::OkStatus();
2624 for (
const auto& folder : folders) {
2625 if (!folder.empty()) {
2627 if (!std::filesystem::exists(abs_path)) {
2628 std::filesystem::create_directories(abs_path);
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();
2644 return absl::OkStatus();
2652 return name.empty() ?
"Untitled Project" :
name;
2656 const std::string& absolute_path)
const {
2657 if (absolute_path.empty() ||
filepath.empty())
2658 return absolute_path;
2660 std::filesystem::path project_dir =
2661 std::filesystem::path(
filepath).parent_path();
2662 std::filesystem::path abs_path(absolute_path);
2665 std::filesystem::path relative =
2666 std::filesystem::relative(abs_path.lexically_normal(), project_dir);
2668 return relative.generic_string();
2671 return abs_path.lexically_normal().generic_string();
2676 const std::string& relative_path)
const {
2677 if (relative_path.empty() ||
filepath.empty())
2678 return relative_path;
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();
2688 abs_path = (project_dir / abs_path).lexically_normal();
2689 abs_path.make_preferred();
2691 return abs_path.string();
2695#ifdef __EMSCRIPTEN__
2703 auto normalize = [
this](std::string* path) {
2704 if (!path || path->empty()) {
2731 normalize(&asset.zsm_path);
2732 for (
auto& source : asset.graphics_sources) {
2733 if (source.kind ==
"bin") {
2734 normalize(&source.path);
2741 if (!rom_path.empty()) {
2748 std::vector<std::string> paths;
2769 const std::string& project_path) {
2773 std::filesystem::path zs_path(project_path);
2774 name = zs_path.stem().string() +
"_imported";
2779 return absl::OkStatus();
2791 absl::string_view artifact_name)
const {
2792 std::filesystem::path base_dir;
2800 base_dir = std::filesystem::path(
filepath).parent_path();
2803 if (base_dir.empty()) {
2804 return std::string(artifact_name);
2806 return (base_dir / std::string(artifact_name)).lexically_normal().string();
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()) {
2818 auto normalized = candidate.lexically_normal();
2819 if (std::find(candidates.begin(), candidates.end(), normalized) ==
2821 candidates.push_back(normalized);
2827 if (!std::filesystem::is_directory(code_path)) {
2828 code_path = code_path.parent_path();
2830 add_candidate(code_path /
"z3dk.toml");
2839 add_candidate(std::filesystem::path(
filepath).parent_path() /
"z3dk.toml");
2842 for (
const auto& candidate : candidates) {
2843 if (!std::filesystem::exists(candidate)) {
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());
2855 const std::filesystem::path base_dir = candidate.parent_path();
2858 if (config.preset.has_value()) {
2863 for (
const auto& include_path : config.include_paths) {
2865 ResolveOptionalPath(base_dir, include_path));
2869 for (
const auto& define : config.defines) {
2874 for (
const auto& main_file : config.main_files) {
2876 ResolveOptionalPath(base_dir, main_file));
2879 if (config.std_includes_path.has_value()) {
2881 ResolveOptionalPath(base_dir, *config.std_includes_path);
2883 if (config.std_defines_path.has_value()) {
2885 ResolveOptionalPath(base_dir, *config.std_defines_path);
2887 if (config.mapper.has_value()) {
2890 if (config.rom_size.has_value()) {
2893 if (config.symbols_format.has_value()) {
2897 if (config.lsp_log_path.has_value()) {
2899 ResolveOptionalPath(base_dir, *config.lsp_log_path);
2903 for (
const auto& emit_path : config.emits) {
2907 for (
const auto& range : config.prohibited_memory_ranges) {
2909 {.start = range.start, .end = range.end, .reason = range.reason});
2913 config.warn_unused_symbols.value_or(
true);
2915 config.warn_branch_outside_bank.value_or(
true);
2919 config.warn_unauthorized_hook.value_or(
true);
2923 if (config.rom_path.has_value()) {
2926 if (config.symbols_path.has_value()) {
2928 ResolveOptionalPath(base_dir, *config.symbols_path);
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") {
2973 "Loaded z3dk config from %s (%zu include paths, %zu defines)",
2982#ifdef __EMSCRIPTEN__
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)) {
3000 LOG_WARN(
"Project",
"Failed to load hack manifest %s: %s",
3001 candidate.string().c_str(),
3002 std::string(status.message()).c_str());
3005 loaded_manifest_path = candidate;
3006 if (update_project_setting) {
3009 LOG_DEBUG(
"Project",
"Loaded hack manifest: %s",
3010 candidate.string().c_str());
3019 if (has_explicit_manifest) {
3027 auto candidate = std::filesystem::path(code_path) /
"hack_manifest.json";
3028 (void)load_manifest(candidate,
true);
3033 const std::filesystem::path project_dir =
3034 std::filesystem::path(
filepath).parent_path();
3035 (void)load_manifest(project_dir /
"hack_manifest.json",
true);
3037 (void)load_manifest(project_dir.parent_path() /
"hack_manifest.json",
3046 auto try_load_registry = [&](
const std::filesystem::path& base) ->
bool {
3050 const auto planning = base /
"Docs" /
"Dev" /
"Planning";
3051 if (!std::filesystem::exists(
planning)) {
3056 LOG_WARN(
"Project",
"Failed to load project registry from %s: %s",
3057 base.string().c_str(), std::string(status.message()).c_str());
3063 bool registry_loaded =
false;
3072 if (!registry_loaded && !loaded_manifest_path.empty()) {
3073 registry_loaded = try_load_registry(loaded_manifest_path.parent_path());
3077 if (!registry_loaded && !
filepath.empty()) {
3079 try_load_registry(std::filesystem::path(
filepath).parent_path());
3082 if (!registry_loaded) {
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());
3094 size_t injected = 0;
3095 for (
const auto& [type_key, labels] :
3097 for (
const auto& [id_str, label] : labels) {
3102 LOG_DEBUG(
"Project",
"Loaded project registry: %zu resource labels injected",
3155 for (uint16_t tile = 0xB0; tile <= 0xBE; ++tile) {
3187 auto now = std::chrono::system_clock::now().time_since_epoch();
3189 std::chrono::duration_cast<std::chrono::milliseconds>(now).count();
3190 return absl::StrFormat(
"yaze_project_%lld", timestamp);
3194std::vector<ProjectManager::ProjectTemplate>
3196 std::vector<ProjectTemplate> templates;
3205 t.
name =
"Vanilla ROM Hack";
3207 "Standard ROM editing without custom ASM. Limited to vanilla features.";
3216 templates.push_back(t);
3222 t.
name =
"ZSCustomOverworld v2";
3224 "Basic overworld expansion: custom BG colors, main palettes, parent "
3237 templates.push_back(t);
3243 t.
name =
"ZSCustomOverworld v3 (Recommended)";
3245 "Full overworld expansion: wide/tall areas, animated GFX, overlays, "
3262 templates.push_back(t);
3268 t.
name =
"Randomizer Compatible";
3270 "Compatible with ALttP Randomizer. Minimal custom features to avoid "
3280 templates.push_back(t);
3290 t.
name =
"Dungeon Designer";
3291 t.
description =
"Focused on dungeon creation and modification.";
3298 templates.push_back(t);
3304 t.
name =
"Graphics Pack";
3306 "Project focused on graphics, sprites, and visual modifications.";
3315 templates.push_back(t);
3321 t.
name =
"Complete Overhaul";
3322 t.
description =
"Full-scale ROM hack with all features enabled.";
3337 templates.push_back(t);
3344 const std::string& template_name,
const std::string& project_name,
3345 const std::string& base_path) {
3347 auto status = project.
Create(project_name, base_path);
3353 if (template_name ==
"Full Overworld Mod") {
3357 project.
metadata.
tags = {
"overworld",
"maps",
"graphics"};
3358 }
else if (template_name ==
"Dungeon Designer") {
3362 project.
metadata.
tags = {
"dungeons",
"rooms",
"design"};
3363 }
else if (template_name ==
"Graphics Pack") {
3367 project.
metadata.
tags = {
"graphics",
"sprites",
"palettes"};
3368 }
else if (template_name ==
"Complete Overhaul") {
3374 project.
metadata.
tags = {
"complete",
"overhaul",
"full-mod"};
3377 status = project.
Save();
3386 const std::string& directory) {
3387#ifdef __EMSCRIPTEN__
3391 std::vector<std::string> projects;
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());
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());
3407 }
catch (
const std::filesystem::filesystem_error& e) {
3416#ifdef __EMSCRIPTEN__
3418 return absl::UnimplementedError(
3419 "Project backups are not supported in the web build");
3422 return absl::InvalidArgumentError(
"Project has no file path");
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);
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");
3434 std::string backup_filename = project.
name +
"_backup_" + ss.str() +
".yaze";
3435 std::filesystem::path backup_path = backup_dir / backup_filename;
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()));
3444 return absl::OkStatus();
3455 std::vector<std::string> recommendations;
3458 recommendations.push_back(
"Add a ROM file to begin editing");
3462 recommendations.push_back(
"Set up a code folder for assembly patches");
3466 recommendations.push_back(
"Create a labels file for better organization");
3470 recommendations.push_back(
"Add a project description for documentation");
3474 recommendations.push_back(
3475 "Consider setting up version control for your project");
3479 if (!missing_files.empty()) {
3480 recommendations.push_back(
3481 "Some project files are missing - use Project > Repair to fix");
3484 return recommendations;
3490 std::ifstream file(filename);
3491 if (!file.is_open()) {
3498 std::string current_type =
"";
3500 while (std::getline(file, line)) {
3501 if (line.empty() || line[0] ==
'#')
3505 if (line[0] ==
'[' && line.back() ==
']') {
3506 current_type = line.substr(1, line.length() - 2);
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;
3520 MigrateGfxGroupLabelKeys(
labels_);
3530 if (!file.is_open())
3533 file <<
"# yaze Resource Labels\n";
3534 file <<
"# Format: [type] followed by key=value pairs\n\n";
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";
3551 if (!p_open || !*p_open)
3555 if (ImGui::Begin(
"Resource Labels", p_open)) {
3556 ImGui::Text(
"Resource Labels Manager");
3558 ImGui::Text(
"Total types: %zu",
labels_.size());
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());
3574 const std::string& key,
3575 const std::string& newValue) {
3576 labels_[type][key] = newValue;
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(),
3592 const std::string& key) {
3593 auto type_it =
labels_.find(type);
3597 auto label_it = type_it->second.find(key);
3598 if (label_it == type_it->second.end())
3601 return label_it->second;
3605 const std::string& type,
const std::string& key,
3606 const std::string& defaultValue) {
3607 auto existing =
GetLabel(type, key);
3608 if (!existing.empty())
3611 labels_[type][key] = defaultValue;
3612 return defaultValue;
3620 const std::unordered_map<
3621 std::string, std::unordered_map<std::string, std::string>>& labels) {
3650 LOG_DEBUG(
"Project",
"Initialized embedded labels:");
3652 LOG_DEBUG(
"Project",
" - %d entrance names",
3654 LOG_DEBUG(
"Project",
" - %d sprite names",
3656 LOG_DEBUG(
"Project",
" - %d overlord names",
3659 LOG_DEBUG(
"Project",
" - %d music names",
3661 LOG_DEBUG(
"Project",
" - %d graphics names",
3663 LOG_DEBUG(
"Project",
" - %d room effect names",
3665 LOG_DEBUG(
"Project",
" - %d room tag names",
3667 LOG_DEBUG(
"Project",
" - %d tile type names",
3670 return absl::OkStatus();
3671 }
catch (
const std::exception& e) {
3672 return absl::InternalError(
3673 absl::StrCat(
"Failed to initialize embedded labels: ", e.what()));
3678 const std::string& default_value)
const {
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;
3688 return default_value.empty() ? resource_type +
"_" + std::to_string(
id)
3693#ifdef __EMSCRIPTEN__
3695 return absl::UnimplementedError(
3696 "File-based label import is not supported in the web build");
3699 if (!file.is_open()) {
3700 return absl::InvalidArgumentError(
3701 absl::StrFormat(
"Cannot open labels file: %s",
filepath));
3704 std::stringstream buffer;
3705 buffer << file.rdbuf();
3713 const std::string& content) {
3720 auto status = provider.ImportFromZScreamFormat(content);
3725 LOG_DEBUG(
"Project",
"Imported ZScream labels:");
3726 LOG_DEBUG(
"Project",
" - %d sprite labels",
3730 LOG_DEBUG(
"Project",
" - %d room tag labels",
3733 return absl::OkStatus();
3742 LOG_DEBUG(
"Project",
"Initialized ResourceLabelProvider with project labels");
3743 LOG_DEBUG(
"Project",
" - prefer_hmagic_names: %s",
3745 LOG_DEBUG(
"Project",
" - hack_manifest: %s",
3753#ifdef YAZE_ENABLE_JSON_PROJECT_FORMAT
3755absl::Status YazeProject::LoadFromJsonFormat(
const std::string& project_path) {
3768#ifdef __EMSCRIPTEN__
3769 return absl::UnimplementedError(
3770 "JSON project format loading is not supported in the web build");
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));
3788 auto& proj = j[
"yaze_project"];
3790 if (proj.contains(
"name"))
3791 name = proj[
"name"].
get<std::string>();
3792 if (proj.contains(
"description"))
3794 if (proj.contains(
"author"))
3796 if (proj.contains(
"version"))
3798 if (proj.contains(
"created"))
3800 if (proj.contains(
"modified"))
3802 if (proj.contains(
"created_by"))
3806 if (proj.contains(
"rom_filename"))
3807 rom_filename = proj[
"rom_filename"].get<std::string>();
3808 if (proj.contains(
"rom_backup_folder"))
3810 if (proj.contains(
"code_folder"))
3811 code_folder = proj[
"code_folder"].get<std::string>();
3812 if (proj.contains(
"assets_folder"))
3814 if (proj.contains(
"patches_folder"))
3816 if (proj.contains(
"labels_filename"))
3818 if (proj.contains(
"symbols_filename"))
3820 if (proj.contains(
"dungeon_tile_patterns"))
3822 proj[
"dungeon_tile_patterns"].get<std::string>();
3823 if (proj.contains(
"palette_catalog_file"))
3825 if (proj.contains(
"terrain_catalog_file"))
3827 if (proj.contains(
"lyra_terrain_file"))
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"))
3843 if (proj.contains(
"sprite_catalog_file"))
3845 if (proj.contains(
"sprite_source_root"))
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"]) {
3854 return asset.status();
3856 if (prior.zsm_path == asset->zsm_path)
3857 return absl::InvalidArgumentError(
"Duplicate sprite asset path");
3862 if (proj.contains(
"rom") && proj[
"rom"].is_object()) {
3863 auto&
rom = proj[
"rom"];
3864 if (
rom.contains(
"role"))
3866 if (
rom.contains(
"expected_hash"))
3868 if (
rom.contains(
"write_policy"))
3874 if (proj.contains(
"use_embedded_labels")) {
3879 if (proj.contains(
"feature_flags")) {
3880 auto& flags = proj[
"feature_flags"];
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"))
3954 if (flags.contains(
"kSaveMessages"))
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"))
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>();
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()) {
3989 if (parsed.has_value()) {
3996 if (proj.contains(
"custom_objects") &&
3997 proj[
"custom_objects"].is_object()) {
3999 for (
auto it = proj[
"custom_objects"].begin();
4000 it != proj[
"custom_objects"].end(); ++it) {
4001 if (!it.value().is_array())
4004 if (!parsed.has_value()) {
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>());
4013 if (!files.empty()) {
4019 if (proj.contains(
"agent_settings") &&
4020 proj[
"agent_settings"].is_object()) {
4021 auto& agent = proj[
"agent_settings"];
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>());
4057 if (agent.contains(
"model_chain") && agent[
"model_chain"].is_array()) {
4059 for (
const auto& model : agent[
"model_chain"]) {
4060 if (model.is_string())
4085 agent.value(
"enable_tool_memory_inspector",
4092 if (proj.contains(
"build_script"))
4093 build_script = proj[
"build_script"].get<std::string>();
4094 if (proj.contains(
"output_folder"))
4096 if (proj.contains(
"git_repository"))
4098 if (proj.contains(
"track_changes"))
4102 return absl::OkStatus();
4103 }
catch (
const Json::exception& e) {
4104 return absl::InvalidArgumentError(
4105 absl::StrFormat(
"JSON parse error: %s", e.what()));
4109absl::Status YazeProject::SaveToJsonFormat() {
4110#ifdef __EMSCRIPTEN__
4111 return absl::UnimplementedError(
4112 "JSON project format saving is not supported in the web build");
4115 auto& proj = j[
"yaze_project"];
4119 proj[
"name"] =
name;
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"] =
4145 proj[
"native_save_producer_file"] =
4149 proj[
"native_save_transfer_proof_file"] =
4153 proj[
"native_save_receipt_file"] =
4157 proj[
"sprite_catalog_file"] =
4159 proj[
"sprite_source_root"] =
4161 proj[
"sprite_assets"] = nlohmann::json::array();
4164 for (
auto& source : asset.graphics_sources) {
4165 if (source.kind ==
"bin") {
4175 proj[
"rom"][
"write_policy"] =
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"] =
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"] =
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"] =
4243 auto& agent = proj[
"agent_settings"];
4270 agent[
"enable_tool_memory_inspector"] =
4275 auto& addrs = proj[
"rom_addresses"];
4282 auto& objs = proj[
"custom_objects"];
4284 objs[absl::StrFormat(
"0x%X", object_id)] = files;
4295 if (!file.is_open()) {
4296 return absl::InvalidArgumentError(
4297 absl::StrFormat(
"Cannot write JSON project file: %s",
filepath));
4301 return absl::OkStatus();
4309 if (!config_dir.ok()) {
4316#ifdef __EMSCRIPTEN__
4317 auto status = platform::WasmStorage::SaveProject(
4320 LOG_WARN(
"RecentFilesManager",
"Could not persist recent files: %s",
4321 status.ToString().c_str());
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());
4334 std::ofstream file(filepath);
4335 if (!file.is_open()) {
4336 LOG_WARN(
"RecentFilesManager",
"Could not save recent files to %s",
4342 file << file_path << std::endl;
4347#ifdef __EMSCRIPTEN__
4349 if (!storage_or.ok()) {
4353 std::istringstream stream(storage_or.value());
4355 while (std::getline(stream, line)) {
4356 if (!line.empty()) {
4364 std::ifstream file(filepath);
4365 if (!file.is_open()) {
4372 while (std::getline(file, line)) {
4373 if (!line.empty()) {
static Json parse(const std::string &)
bool contains(const std::string &) const
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.
std::string Serialize() const
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)
static absl::Status ValidateProjectStructure(const YazeProject &project)
static absl::StatusOr< YazeProject > CreateFromTemplate(const std::string &template_name, const std::string &project_name, const std::string &base_path)
static std::vector< std::string > GetRecommendedFixesForProject(const YazeProject &project)
static std::vector< ProjectTemplate > GetProjectTemplates()
static absl::Status BackupProject(const YazeProject &project)
std::string GetFilePath() const
std::vector< std::string > recent_files_
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_VIDEOGAME_ASSET
#define LOG_DEBUG(category, format,...)
#define LOG_ERROR(category, format,...)
#define LOG_WARN(category, format,...)
#define LOG_INFO(category, format,...)
#define ASSIGN_OR_RETURN(type_variable_name, expression)
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)
std::string ResolveOptionalPath(const std::filesystem::path &base_dir, const std::string &value)
void RemoveProjectSaveTempFile(const std::filesystem::path &temp_path)
bool ContainsLoneCarriageReturn(const std::string &content)
std::vector< std::string > ParsePositionalStringList(const std::string &value)
absl::Status WriteProjectFileAtomicallyImpl(const std::filesystem::path &target_path, absl::string_view contents, bool replace_existing)
std::vector< uint16_t > ParseHexUintList(const std::string &value)
std::string ToLowerCopy(std::string value)
constexpr absl::string_view kProjectFileFooter
bool ParseBool(const std::string &value)
std::pair< std::string, std::string > ParseDefineToken(const std::string &value)
std::optional< uint32_t > ParseHexUint32(const std::string &value)
bool IsKnownProjectSection(absl::string_view name)
std::string FormatHexUint32(uint32_t value)
ProjectFilePassthrough::Section & PassthroughSection(ProjectFilePassthrough &passthrough, const std::string &name)
std::map< uint16_t, std::vector< uint16_t > > ParseHexUintListMap(const std::string &value)
std::string FormatHexUintListMap(const std::map< uint16_t, std::vector< uint16_t > > &values)
bool IsGeneratedProjectComment(absl::string_view line, bool before_sections)
std::string SanitizeStorageKey(absl::string_view input)
std::filesystem::path MakeProjectSaveTempPath(const std::filesystem::path &target_path)
std::string MergeProjectFilePassthrough(const std::string &canonical, const ProjectFilePassthrough &passthrough)
uint64_t CurrentProcessIdForProjectSave()
std::string FormatHexUintList(const std::vector< uint16_t > &values)
void MigrateGfxGroupLabelKeys(std::unordered_map< std::string, std::unordered_map< std::string, std::string > > &labels)
bool IsKeyLine(absl::string_view line)
std::string BasenameLower(const std::string &path)
std::vector< std::string > ParseStringList(const std::string &value)
std::string RomRoleToString(RomRole role)
absl::Status WriteProjectFileAtomically(absl::string_view target_path, absl::string_view contents, bool replace_existing)
RomRole ParseRomRole(absl::string_view value)
const std::string kRecentFilesFilename
RomWritePolicy ParseRomWritePolicy(absl::string_view value)
std::string RomWritePolicyToString(RomWritePolicy policy)
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)
ResourceLabelProvider & GetResourceLabels()
Get the global ResourceLabelProvider instance.
#define RETURN_IF_ERROR(expr)
Room transition destination.
bool kSaveOverworldProperties
bool kEnableSpecialWorldExpansion
bool kSaveOverworldSprites
bool kApplyZSCustomOverworldASM
bool kLoadCustomOverworld
bool kSaveOverworldEntrances
bool kEnableCustomObjects
struct yaze::core::FeatureFlags::Flags::Dungeon dungeon
struct yaze::core::FeatureFlags::Flags::Overworld overworld
bool kSaveWithChangeQueue
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]).
std::vector< std::string > annotations
std::string export_folder
std::vector< uint16_t > track_object_ids
std::vector< uint16_t > minecart_sprite_ids
std::vector< uint16_t > track_stop_tiles
std::vector< uint16_t > track_tiles
std::vector< uint16_t > track_switch_tiles
std::vector< uint16_t > reserved_sheets
std::map< uint16_t, std::vector< uint16_t > > reserved_blocks
std::vector< uint16_t > flagged_sheets
Planning editor sidecar ([planning]).
std::vector< Line > lines
Lines of a .yaze file that this build does not model.
std::vector< Section > sections
YazeProject template_project
std::string CreateOrGetLabel(const std::string &type, const std::string &key, const std::string &defaultValue)
void DisplayLabels(bool *p_open)
std::string GetLabel(const std::string &type, const std::string &key)
void EditLabel(const std::string &type, const std::string &key, const std::string &newValue)
bool LoadLabels(const std::string &filename)
void SelectableLabelWithNameEdit(bool selected, const std::string &type, const std::string &key, const std::string &defaultValue)
std::unordered_map< std::string, std::unordered_map< std::string, std::string > > labels_
int backup_keep_daily_days
std::string last_layout_preset
std::map< std::string, std::string > custom_keybindings
int backup_retention_count
std::vector< std::string > saved_layouts
std::map< std::string, bool > editor_visibility
float autosave_interval_secs
std::vector< std::string > recent_files
std::string custom_system_prompt
std::string gemini_api_key
bool enable_tool_emulator
bool enable_tool_memory_inspector
std::vector< std::string > favorite_models
bool enable_tool_resources
bool enable_tool_messages
std::vector< std::string > model_chain
std::string builder_blueprint_path
bool enable_tool_dialogue
bool enable_tool_overworld
std::string last_saved_at
bool persist_custom_music
Modern project structure with comprehensive settings consolidation.
std::string rom_backup_folder
std::unordered_map< int, std::vector< std::string > > custom_object_files
absl::Status ResetToDefaults()
std::string custom_objects_folder
absl::Status RepairProject()
std::string MakeStorageKey(absl::string_view suffix) const
static std::string ResolveBundleRoot(const std::string &path)
absl::Status ValidateSavedSourceBindings(const std::string &content) const
struct yaze::project::YazeProject::MusicPersistence music_persistence
bool project_opened() const
std::string sprite_catalog_file
absl::StatusOr< std::string > SerializeToString() const
std::string zscream_project_file
absl::Status ExportForZScream(const std::string &target_path)
absl::Status SaveToYazeFormat(bool replace_existing=true)
std::string native_save_transfer_proof_file
absl::Status ImportZScreamProject(const std::string &zscream_project_path)
absl::Status SaveAllSettings()
absl::Status LoadFromString(const std::string &content, const std::string &project_path)
void NormalizePathsToAbsolute()
absl::Status ImportLabelsFromZScreamContent(const std::string &content)
Import labels from ZScream format content directly.
std::string git_repository
core::HackManifest hack_manifest
void InitializeResourceLabelProvider()
Initialize the global ResourceLabelProvider with this project's labels.
absl::Status ParseFromString(const std::string &content)
std::string cutscene_shots
std::vector< std::string > additional_roms
std::string native_save_receipt_file
std::string lyra_terrain_file
std::string terrain_catalog_file
std::string patches_folder
absl::Status LoadFromYazeFormat(const std::string &project_path)
std::unordered_map< std::string, std::unordered_map< std::string, std::string > > resource_labels
std::vector< std::string > AtlasAnnotationPaths() const
std::string GenerateProjectId() const
absl::Status Create(const std::string &project_name, const std::string &base_path)
std::string assets_folder
void ReloadHackManifest()
GraphicsSheetSettings graphics_sheets
std::string AtlasSymbolsPath() const
absl::Status LoadAllSettings()
std::string labels_filename
std::vector< std::string > asm_sources
std::string hack_manifest_file
std::string GetDisplayName() const
PlanningSettings planning
std::string PlanningFilePath() const
std::string native_save_producer_file
std::vector< std::string > GetMissingFiles() const
std::string lyra_build_request_file
WorkspaceSettings workspace_settings
std::string GetZ3dkArtifactPath(absl::string_view artifact_name) const
std::string output_folder
std::string asm_entry_point
std::string GetRelativePath(const std::string &absolute_path) const
std::vector< SpriteAssetBinding > sprite_assets
absl::Status InitializeEmbeddedLabels(const std::unordered_map< std::string, std::unordered_map< std::string, std::string > > &labels)
ProjectFilePassthrough file_passthrough
absl::StatusOr< std::optional< std::string > > ResolveExpandedBuildRunTarget(const std::string &editing_source_path) const
absl::Status SaveAs(const std::string &new_path)
bool IsOracleCompatible() const
struct yaze::project::YazeProject::AgentSettings agent_settings
std::string custom_collision_json
DungeonOverlaySettings dungeon_overlay
absl::Status SaveAsWithLyraTerrain(const std::string &new_path, const core::LyraTerrainDocument &terrain, const std::optional< std::string > &expected_source_bytes)
std::string palette_catalog_file
absl::Status ImportFromZScreamFormat(const std::string &project_path)
void InitializeDefaults()
std::string GetAbsolutePath(const std::string &relative_path) const
std::string overworld_graphics_preview
std::string GetLabel(const std::string &resource_type, int id, const std::string &default_value="") const
std::string sprite_source_root
absl::Status Open(const std::string &project_path)
absl::Status ImportLabelsFromZScream(const std::string &filepath)
Import labels from a ZScream DefaultNames.txt file.
std::string last_build_hash
void TryLoadHackManifest()
std::map< std::string, std::string > zscream_mappings
std::string dungeon_tile_patterns
absl::Status Validate() const
core::FeatureFlags::Flags feature_flags
std::vector< std::string > build_configurations
void ReloadZ3dkSettings()
core::RomAddressOverrides rom_address_overrides
std::string symbols_filename
Z3dkSettings z3dk_settings
std::string annotations_json
std::string sourcemap_json
std::vector< std::string > include_paths
std::string std_includes_path
std::vector< std::string > main_files
std::vector< std::string > emits
std::vector< std::pair< std::string, std::string > > defines
std::string symbols_format
Z3dkArtifactPaths artifact_paths
bool warn_branch_outside_bank
std::optional< bool > lsp_log_enabled
bool warn_unauthorized_hook
std::vector< Z3dkMemoryRange > prohibited_memory_ranges
std::string std_defines_path