14#include "absl/strings/match.h"
15#include "absl/strings/str_format.h"
16#include "absl/strings/str_join.h"
17#include "absl/strings/str_split.h"
19#include "imgui/imgui.h"
25#include "yaze_config.h"
28#if defined(YAZE_WITH_Z3DK) && __has_include("z3dk_core/config.h")
29#include "z3dk_core/config.h"
51 value.begin(), value.end(), value.begin(),
52 [](
unsigned char c) { return static_cast<char>(std::tolower(c)); });
57std::pair<std::string, std::string> ParseKeyValue(
const std::string& line) {
58 size_t eq_pos = line.find(
'=');
59 if (eq_pos == std::string::npos)
62 std::string key = line.substr(0, eq_pos);
63 std::string value = line.substr(eq_pos + 1);
66 key.erase(0, key.find_first_not_of(
" \t"));
67 key.erase(key.find_last_not_of(
" \t") + 1);
68 value.erase(0, value.find_first_not_of(
" \t"));
69 value.erase(value.find_last_not_of(
" \t") + 1);
75 return value ==
"true" || value ==
"1" || value ==
"yes";
80 return std::stof(value);
87 std::vector<std::string> result;
91 std::vector<std::string> parts = absl::StrSplit(value,
',');
92 for (
const auto& part : parts) {
93 std::string trimmed = std::string(part);
94 trimmed.erase(0, trimmed.find_first_not_of(
" \t"));
95 trimmed.erase(trimmed.find_last_not_of(
" \t") + 1);
96 if (!trimmed.empty()) {
97 result.push_back(trimmed);
104 std::vector<uint16_t> result;
110 result.reserve(parts.size());
111 for (
const auto& part : parts) {
112 std::string token = part;
113 if (token.rfind(
"0x", 0) == 0 || token.rfind(
"0X", 0) == 0) {
114 token = token.substr(2);
116 result.push_back(
static_cast<uint16_t
>(std::stoul(token,
nullptr, 16)));
122 result.push_back(
static_cast<uint16_t
>(std::stoul(token,
nullptr, 10)));
135 std::string token = value;
136 if (token.rfind(
"0x", 0) == 0 || token.rfind(
"0X", 0) == 0) {
137 token = token.substr(2);
139 return static_cast<uint32_t
>(std::stoul(token,
nullptr, 16));
145 return static_cast<uint32_t
>(std::stoul(token,
nullptr, 10));
152 return absl::StrJoin(values,
",", [](std::string* out, uint16_t value) {
153 out->append(absl::StrFormat(
"0x%02X", value));
158 return absl::StrFormat(
"0x%06X", value);
162 std::string key(input);
163 for (
char& c : key) {
164 if (!std::isalnum(
static_cast<unsigned char>(c))) {
175 auto [key, parsed_value] = ParseKeyValue(value);
179 return {key, parsed_value.empty() ?
"1" : parsed_value};
183 const std::string& value) {
187 std::filesystem::path path(value);
188 if (path.is_absolute()) {
189 return path.lexically_normal().string();
191 return (base_dir / path).lexically_normal().string();
195 return ToLowerCopy(std::filesystem::path(path).filename().
string());
198#ifndef __EMSCRIPTEN__
201 return static_cast<uint64_t
>(::GetCurrentProcessId());
203 return static_cast<uint64_t
>(::getpid());
208 const std::filesystem::path& target_path) {
209 static std::atomic<uint64_t> next_temp_id{0};
210 std::filesystem::path temp_path = target_path;
213 std::to_string(next_temp_id.fetch_add(1, std::memory_order_relaxed));
218 std::error_code remove_error;
219 std::filesystem::remove(temp_path, remove_error);
223 const std::filesystem::path& target_path, absl::string_view contents,
224 bool replace_existing) {
226 std::ofstream file(temp_path, std::ios::binary | std::ios::trunc);
227 if (!file.is_open()) {
228 return absl::InvalidArgumentError(absl::StrFormat(
229 "Cannot create temporary project file: %s", temp_path.string()));
232 file.write(contents.data(),
static_cast<std::streamsize
>(contents.size()));
237 return absl::InternalError(absl::StrFormat(
238 "Failed to write temporary project file: %s", temp_path.string()));
244 return absl::InternalError(absl::StrFormat(
245 "Failed to close temporary project file: %s", temp_path.string()));
248 std::error_code rename_error;
250 const DWORD move_flags =
251 MOVEFILE_WRITE_THROUGH |
252 (replace_existing ? MOVEFILE_REPLACE_EXISTING :
static_cast<DWORD
>(0));
253 if (!::MoveFileExW(temp_path.c_str(), target_path.c_str(), move_flags)) {
254 rename_error = std::error_code(
static_cast<int>(::GetLastError()),
255 std::system_category());
258 if (replace_existing) {
259 std::filesystem::rename(temp_path, target_path, rename_error);
260 }
else if (::link(temp_path.c_str(), target_path.c_str()) != 0) {
261 rename_error = std::error_code(errno, std::generic_category());
268 bool target_already_exists = rename_error == std::errc::file_exists;
270 target_already_exists = target_already_exists ||
271 rename_error.value() == ERROR_FILE_EXISTS ||
272 rename_error.value() == ERROR_ALREADY_EXISTS;
274 if (!replace_existing && target_already_exists) {
275 return absl::AlreadyExistsError(absl::StrFormat(
276 "Project file already exists: %s", target_path.string()));
278 return absl::InternalError(
279 absl::StrFormat(
"Failed to replace project file %s: %s",
280 target_path.string(), rename_error.message()));
283 return absl::OkStatus();
288#ifndef __EMSCRIPTEN__
290 absl::string_view contents,
291 bool replace_existing) {
292 return WriteProjectFileAtomicallyImpl(
293 std::filesystem::path(std::string(target_path)), contents,
313 std::string lower = ToLowerCopy(std::string(value));
314 if (lower ==
"base") {
317 if (lower ==
"patched") {
320 if (lower ==
"release") {
339 std::string lower = ToLowerCopy(std::string(value));
340 if (lower ==
"allow") {
343 if (lower ==
"block") {
351 const std::string& base_path) {
353 filepath = base_path +
"/" + project_name +
".yaze";
356 auto now = std::chrono::system_clock::now();
357 auto time_t = std::chrono::system_clock::to_time_t(now);
358 std::stringstream ss;
359 ss << std::put_time(std::localtime(&time_t),
"%Y-%m-%d %H:%M:%S");
370#ifndef __EMSCRIPTEN__
372 std::filesystem::path project_dir(base_path +
"/" + project_name);
373 std::filesystem::create_directories(project_dir);
374 std::filesystem::create_directories(project_dir /
"code");
375 std::filesystem::create_directories(project_dir /
"assets");
376 std::filesystem::create_directories(project_dir /
"patches");
377 std::filesystem::create_directories(project_dir /
"backups");
378 std::filesystem::create_directories(project_dir /
"output");
411 auto current = std::filesystem::path(path).lexically_normal();
413 for (; !current.empty(); current = current.parent_path()) {
414 if (current.extension() ==
".yazeproj") {
416 if (std::filesystem::is_directory(current, ec) && !ec) {
417 return current.string();
421 if (current == current.parent_path()) {
433 std::string resolved_path = project_path;
435 if (!bundle_root.empty() && bundle_root != project_path) {
437 resolved_path = bundle_root;
440#ifndef __EMSCRIPTEN__
445 std::error_code absolute_ec;
446 const auto absolute_path =
447 std::filesystem::absolute(resolved_path, absolute_ec).lexically_normal();
449 resolved_path = absolute_path.string();
458 auto storage_or = platform::WasmStorage::LoadProject(storage_key);
459 if (storage_or.ok()) {
465 absl::Status load_status;
466 if (resolved_path.ends_with(
".yazeproj")) {
469 const std::filesystem::path bundle_path(resolved_path);
471 if (!std::filesystem::exists(bundle_path, ec) || ec ||
472 !std::filesystem::is_directory(bundle_path, ec) || ec) {
473 return absl::InvalidArgumentError(
474 absl::StrFormat(
"Project bundle does not exist: %s", resolved_path));
479 const std::filesystem::path project_file = bundle_path /
"project.yaze";
482 if (!std::filesystem::exists(project_file, ec) || ec) {
485 name = bundle_path.stem().string();
488 auto now = std::chrono::system_clock::now();
489 auto time_t = std::chrono::system_clock::to_time_t(now);
490 std::stringstream ss;
491 ss << std::put_time(std::localtime(&time_t),
"%Y-%m-%d %H:%M:%S");
511 const std::filesystem::path rom_candidate = bundle_path /
"rom";
518 const std::filesystem::path project_dir = bundle_path /
"project";
519 const std::filesystem::path code_dir = bundle_path /
"code";
520 if (std::filesystem::exists(project_dir, ec) &&
521 std::filesystem::is_directory(project_dir, ec) && !ec) {
523 }
else if (std::filesystem::exists(code_dir, ec) &&
524 std::filesystem::is_directory(code_dir, ec) && !ec) {
539 }
else if (resolved_path.ends_with(
".yaze")) {
543 std::ifstream file(resolved_path);
544 if (file.is_open()) {
545 std::stringstream buffer;
546 buffer << file.rdbuf();
547 std::string content = buffer.str();
549#ifdef YAZE_ENABLE_JSON_PROJECT_FORMAT
550 if (!content.empty() && content.front() ==
'{') {
551 LOG_DEBUG(
"Project",
"Detected JSON format project file");
552 load_status = LoadFromJsonFormat(resolved_path);
560 return absl::InvalidArgumentError(
561 absl::StrFormat(
"Cannot open project file: %s", resolved_path));
563 }
else if (resolved_path.ends_with(
".zsproj")) {
567 return absl::InvalidArgumentError(
"Unsupported project file format");
570 if (!load_status.ok()) {
584 return absl::OkStatus();
596 const std::string& project_path) {
597 if (project_path.empty()) {
598 return absl::InvalidArgumentError(
"Project file path cannot be empty");
603#ifndef __EMSCRIPTEN__
605 const auto absolute_path =
606 std::filesystem::absolute(project_path, ec).lexically_normal();
608 ec ? std::filesystem::path(project_path).lexically_normal().string()
609 : absolute_path.string();
616 }
catch (
const std::exception& error) {
617 return absl::InvalidArgumentError(
618 absl::StrFormat(
"Invalid project file value: %s", error.what()));
623 return absl::OkStatus();
627 std::string old_filepath =
filepath;
630 auto status =
Save();
642 }
else if (!
name.empty()) {
645 base = std::filesystem::path(
filepath).stem().string();
647 base = SanitizeStorageKey(base);
648 if (suffix.empty()) {
651 return absl::StrFormat(
"%s_%s", base, suffix);
655 std::ostringstream file;
658 file <<
"# yaze Project File\n";
659 file <<
"# Format Version: 2.0\n";
664 file <<
"[project]\n";
665 file <<
"name=" <<
name <<
"\n";
675 file <<
"tags=" << absl::StrJoin(
metadata.
tags,
",") <<
"\n\n";
690 file <<
"additional_roms=" << absl::StrJoin(
additional_roms,
",") <<
"\n\n";
700 file <<
"[feature_flags]\n";
701 file <<
"load_custom_overworld="
704 file <<
"apply_zs_custom_overworld_asm="
708 file <<
"save_dungeon_maps="
710 file <<
"save_overworld_maps="
713 file <<
"save_overworld_entrances="
716 file <<
"save_overworld_exits="
719 file <<
"save_overworld_items="
722 file <<
"save_overworld_properties="
725 file <<
"save_dungeon_objects="
727 file <<
"save_dungeon_sprites="
729 file <<
"save_dungeon_room_headers="
731 file <<
"save_dungeon_torches="
733 file <<
"save_dungeon_pits="
735 file <<
"save_dungeon_blocks="
737 file <<
"save_dungeon_collision="
739 file <<
"save_dungeon_water_fill_zones="
742 file <<
"save_dungeon_chests="
744 file <<
"save_dungeon_pot_items="
746 file <<
"save_dungeon_entrances="
748 file <<
"save_dungeon_palettes="
750 file <<
"save_graphics_sheet="
752 file <<
"save_all_palettes="
754 file <<
"save_gfx_groups="
758 file <<
"enable_custom_objects="
762 file <<
"[workspace]\n";
767 file <<
"autosave_enabled="
771 file <<
"backup_on_save="
775 file <<
"backup_keep_daily="
781 file <<
"show_collision="
783 file <<
"prefer_hmagic_names="
787 file <<
"saved_layouts="
794 if (track_tiles.empty()) {
795 for (uint16_t tile = 0xB0; tile <= 0xBE; ++tile) {
796 track_tiles.push_back(tile);
800 if (track_stop_tiles.empty()) {
801 track_stop_tiles = {0xB7, 0xB8, 0xB9, 0xBA};
804 if (track_switch_tiles.empty()) {
805 track_switch_tiles = {0xD0, 0xD1, 0xD2, 0xD3};
808 if (track_object_ids.empty()) {
809 track_object_ids = {0x31};
812 if (minecart_sprite_ids.empty()) {
813 minecart_sprite_ids = {0xA3};
815 file <<
"[dungeon_overlay]\n";
816 file <<
"track_tiles=" << FormatHexUintList(track_tiles) <<
"\n";
817 file <<
"track_stop_tiles=" << FormatHexUintList(track_stop_tiles) <<
"\n";
818 file <<
"track_switch_tiles=" << FormatHexUintList(track_switch_tiles)
820 file <<
"track_object_ids=" << FormatHexUintList(track_object_ids) <<
"\n";
821 file <<
"minecart_sprite_ids=" << FormatHexUintList(minecart_sprite_ids)
825 file <<
"[rom_addresses]\n";
827 file << key <<
"=" << FormatHexUint32(value) <<
"\n";
833 file <<
"[custom_objects]\n";
835 file << absl::StrFormat(
"object_0x%X", object_id) <<
"="
836 << absl::StrJoin(files,
",") <<
"\n";
842 file <<
"[agent_settings]\n";
847 file <<
"custom_system_prompt="
849 file <<
"use_custom_prompt="
851 file <<
"show_reasoning="
859 file <<
"stream_responses="
861 file <<
"favorite_models="
866 file <<
"enable_tool_resources="
868 file <<
"enable_tool_dungeon="
870 file <<
"enable_tool_overworld="
872 file <<
"enable_tool_messages="
874 file <<
"enable_tool_dialogue="
876 file <<
"enable_tool_gui="
878 file <<
"enable_tool_music="
880 file <<
"enable_tool_sprite="
882 file <<
"enable_tool_emulator="
884 file <<
"enable_tool_memory_inspector="
892 file <<
"[keybindings]\n";
894 file << key <<
"=" << value <<
"\n";
901 file <<
"[editor_visibility]\n";
903 file << key <<
"=" << (value ?
"true" :
"false") <<
"\n";
910 if (!labels.empty()) {
911 file <<
"[labels_" << type <<
"]\n";
912 for (
const auto& [key, value] : labels) {
913 file << key <<
"=" << value <<
"\n";
924 file <<
"track_changes=" << (
track_changes ?
"true" :
"false") <<
"\n";
929 file <<
"asm_sources=" << absl::StrJoin(
asm_sources,
",") <<
"\n";
935 file <<
"persist_custom_music="
942 file <<
"[zscream_compatibility]\n";
945 file << key <<
"=" << value <<
"\n";
950 file <<
"# End of YAZE Project File\n";
955 std::istringstream stream(content);
957 std::string current_section;
959 while (std::getline(stream, line)) {
960 if (line.empty() || line[0] ==
'#')
963 if (line.front() ==
'[' && line.back() ==
']') {
964 current_section = line.substr(1, line.length() - 2);
968 auto [key, value] = ParseKeyValue(line);
972 if (current_section ==
"project") {
975 else if (key ==
"description")
977 else if (key ==
"author")
979 else if (key ==
"license")
981 else if (key ==
"version")
983 else if (key ==
"created_date")
985 else if (key ==
"last_modified")
987 else if (key ==
"yaze_version")
989 else if (key ==
"created_by")
991 else if (key ==
"tags")
993 else if (key ==
"project_id")
995 }
else if (current_section ==
"files") {
996 if (key ==
"rom_filename")
998 else if (key ==
"rom_backup_folder")
1000 else if (key ==
"code_folder")
1002 else if (key ==
"assets_folder")
1004 else if (key ==
"patches_folder")
1006 else if (key ==
"labels_filename")
1008 else if (key ==
"symbols_filename")
1010 else if (key ==
"output_folder")
1012 else if (key ==
"custom_objects_folder")
1014 else if (key ==
"hack_manifest_file")
1016 else if (key ==
"additional_roms")
1018 }
else if (current_section ==
"rom") {
1021 else if (key ==
"expected_hash")
1023 else if (key ==
"write_policy")
1025 }
else if (current_section ==
"feature_flags") {
1026 if (key ==
"load_custom_overworld")
1028 else if (key ==
"apply_zs_custom_overworld_asm")
1030 else if (key ==
"save_dungeon_maps")
1032 else if (key ==
"save_overworld_maps")
1034 else if (key ==
"save_overworld_entrances")
1036 else if (key ==
"save_overworld_exits")
1038 else if (key ==
"save_overworld_items")
1040 else if (key ==
"save_overworld_properties")
1042 else if (key ==
"save_dungeon_objects")
1044 else if (key ==
"save_dungeon_sprites")
1046 else if (key ==
"save_dungeon_room_headers")
1048 else if (key ==
"save_dungeon_torches")
1050 else if (key ==
"save_dungeon_pits")
1052 else if (key ==
"save_dungeon_blocks")
1054 else if (key ==
"save_dungeon_collision")
1056 else if (key ==
"save_dungeon_water_fill_zones")
1058 else if (key ==
"save_dungeon_chests")
1060 else if (key ==
"save_dungeon_pot_items")
1062 else if (key ==
"save_dungeon_entrances")
1064 else if (key ==
"save_dungeon_palettes")
1066 else if (key ==
"save_graphics_sheet")
1068 else if (key ==
"save_all_palettes")
1070 else if (key ==
"save_gfx_groups")
1072 else if (key ==
"save_messages")
1074 else if (key ==
"enable_custom_objects")
1076 }
else if (current_section ==
"workspace") {
1077 if (key ==
"font_global_scale")
1079 else if (key ==
"dark_mode")
1081 else if (key ==
"ui_theme")
1083 else if (key ==
"autosave_enabled")
1085 else if (key ==
"autosave_interval_secs")
1087 else if (key ==
"backup_on_save")
1089 else if (key ==
"backup_retention_count")
1091 else if (key ==
"backup_keep_daily")
1093 else if (key ==
"backup_keep_daily_days")
1095 else if (key ==
"show_grid")
1097 else if (key ==
"show_collision")
1099 else if (key ==
"prefer_hmagic_names")
1101 else if (key ==
"last_layout_preset")
1103 else if (key ==
"saved_layouts")
1105 else if (key ==
"recent_files")
1107 }
else if (current_section ==
"dungeon_overlay") {
1108 if (key ==
"track_tiles")
1110 else if (key ==
"track_stop_tiles")
1112 else if (key ==
"track_switch_tiles")
1114 else if (key ==
"track_object_ids")
1116 else if (key ==
"minecart_sprite_ids")
1118 }
else if (current_section ==
"rom_addresses") {
1119 auto parsed = ParseHexUint32(value);
1120 if (parsed.has_value()) {
1123 }
else if (current_section ==
"custom_objects") {
1124 std::string id_token = key;
1125 if (absl::StartsWith(id_token,
"object_")) {
1126 id_token = id_token.substr(7);
1128 auto parsed = ParseHexUint32(id_token);
1129 if (parsed.has_value()) {
1132 }
else if (current_section ==
"agent_settings") {
1133 if (key ==
"ai_provider")
1135 else if (key ==
"ai_model")
1137 else if (key ==
"ollama_host")
1139 else if (key ==
"gemini_api_key")
1141 else if (key ==
"custom_system_prompt")
1143 else if (key ==
"use_custom_prompt")
1145 else if (key ==
"show_reasoning")
1147 else if (key ==
"verbose")
1149 else if (key ==
"max_tool_iterations")
1151 else if (key ==
"max_retry_attempts")
1153 else if (key ==
"temperature")
1155 else if (key ==
"top_p")
1157 else if (key ==
"max_output_tokens")
1159 else if (key ==
"stream_responses")
1161 else if (key ==
"favorite_models")
1163 else if (key ==
"model_chain")
1165 else if (key ==
"chain_mode")
1167 else if (key ==
"enable_tool_resources")
1169 else if (key ==
"enable_tool_dungeon")
1171 else if (key ==
"enable_tool_overworld")
1173 else if (key ==
"enable_tool_messages")
1175 else if (key ==
"enable_tool_dialogue")
1177 else if (key ==
"enable_tool_gui")
1179 else if (key ==
"enable_tool_music")
1181 else if (key ==
"enable_tool_sprite")
1183 else if (key ==
"enable_tool_emulator")
1185 else if (key ==
"enable_tool_memory_inspector")
1187 else if (key ==
"builder_blueprint_path")
1189 }
else if (current_section ==
"build") {
1190 if (key ==
"build_script")
1192 else if (key ==
"output_folder")
1194 else if (key ==
"git_repository")
1196 else if (key ==
"track_changes")
1198 else if (key ==
"build_configurations")
1200 else if (key ==
"build_target")
1202 else if (key ==
"asm_entry_point")
1204 else if (key ==
"asm_sources")
1206 else if (key ==
"last_build_hash")
1208 else if (key ==
"build_number")
1210 }
else if (current_section.rfind(
"labels_", 0) == 0) {
1211 std::string label_type = current_section.substr(7);
1213 }
else if (current_section ==
"keybindings") {
1215 }
else if (current_section ==
"editor_visibility") {
1217 }
else if (current_section ==
"zscream_compatibility") {
1218 if (key ==
"original_project_file")
1222 }
else if (current_section ==
"music") {
1223 if (key ==
"persist_custom_music")
1225 else if (key ==
"storage_key")
1227 else if (key ==
"last_saved_at")
1242 return absl::OkStatus();
1246#ifdef __EMSCRIPTEN__
1248 auto storage_or = platform::WasmStorage::LoadProject(storage_key);
1249 if (storage_or.ok()) {
1254 std::ifstream file(project_path);
1255 if (!file.is_open()) {
1256 return absl::InvalidArgumentError(
1257 absl::StrFormat(
"Cannot open project file: %s", project_path));
1260 std::stringstream buffer;
1261 buffer << file.rdbuf();
1268 auto now = std::chrono::system_clock::now();
1269 auto time_t = std::chrono::system_clock::to_time_t(now);
1270 std::stringstream ss;
1271 ss << std::put_time(std::localtime(&time_t),
"%Y-%m-%d %H:%M:%S");
1283#ifdef __EMSCRIPTEN__
1284 auto storage_status = platform::WasmStorage::SaveProject(
1286 if (!storage_status.ok()) {
1287 return storage_status;
1296 return absl::OkStatus();
1300 const std::string& zscream_project_path) {
1306 std::filesystem::path zs_path(zscream_project_path);
1307 name = zs_path.stem().string() +
"_imported";
1319 return absl::OkStatus();
1324 std::ofstream file(target_path);
1325 if (!file.is_open()) {
1326 return absl::InvalidArgumentError(
1327 absl::StrFormat(
"Cannot create ZScream project file: %s", target_path));
1331 file <<
"# ZScream Compatible Project File\n";
1333 file <<
"name=" <<
name <<
"\n";
1341 return absl::OkStatus();
1361 std::vector<std::string> errors;
1364 errors.push_back(
"Project name is required");
1366 errors.push_back(
"Project file path is required");
1368 errors.push_back(
"ROM file is required");
1370#ifndef __EMSCRIPTEN__
1374 errors.push_back(
"ROM file does not exist: " +
rom_filename);
1379 errors.push_back(
"Code folder does not exist: " +
code_folder);
1389 errors.push_back(
"Hack manifest file does not exist: " +
1392 errors.push_back(
"Hack manifest file failed to load: " +
1398 if (!errors.empty()) {
1399 return absl::InvalidArgumentError(absl::StrJoin(errors,
"; "));
1402 return absl::OkStatus();
1406 std::vector<std::string> missing;
1408#ifndef __EMSCRIPTEN__
1431#ifdef __EMSCRIPTEN__
1433 return absl::OkStatus();
1440 for (
const auto& folder : folders) {
1441 if (!folder.empty()) {
1443 if (!std::filesystem::exists(abs_path)) {
1444 std::filesystem::create_directories(abs_path);
1452 if (!std::filesystem::exists(abs_labels)) {
1453 std::ofstream labels_file(abs_labels);
1454 labels_file <<
"# yaze Resource Labels\n";
1455 labels_file <<
"# Format: [type] key=value\n\n";
1456 labels_file.close();
1460 return absl::OkStatus();
1468 return name.empty() ?
"Untitled Project" :
name;
1472 const std::string& absolute_path)
const {
1473 if (absolute_path.empty() ||
filepath.empty())
1474 return absolute_path;
1476 std::filesystem::path project_dir =
1477 std::filesystem::path(
filepath).parent_path();
1478 std::filesystem::path abs_path(absolute_path);
1481 std::filesystem::path relative =
1482 std::filesystem::relative(abs_path.lexically_normal(), project_dir);
1484 return relative.generic_string();
1487 return abs_path.lexically_normal().generic_string();
1492 const std::string& relative_path)
const {
1493 if (relative_path.empty() ||
filepath.empty())
1494 return relative_path;
1496 std::filesystem::path project_dir =
1497 std::filesystem::path(
filepath).parent_path();
1498 std::filesystem::path abs_path(relative_path);
1499 if (abs_path.is_absolute()) {
1500 abs_path = abs_path.lexically_normal();
1501 abs_path.make_preferred();
1502 return abs_path.string();
1504 abs_path = (project_dir / abs_path).lexically_normal();
1505 abs_path.make_preferred();
1507 return abs_path.string();
1511#ifdef __EMSCRIPTEN__
1519 auto normalize = [
this](std::string* path) {
1520 if (!path || path->empty()) {
1538 if (!rom_path.empty()) {
1549 const std::string& project_path) {
1553 std::filesystem::path zs_path(project_path);
1554 name = zs_path.stem().string() +
"_imported";
1559 return absl::OkStatus();
1571 absl::string_view artifact_name)
const {
1572 std::filesystem::path base_dir;
1580 base_dir = std::filesystem::path(
filepath).parent_path();
1583 if (base_dir.empty()) {
1584 return std::string(artifact_name);
1586 return (base_dir / std::string(artifact_name)).lexically_normal().string();
1592#if defined(YAZE_WITH_Z3DK) && __has_include("z3dk_core/config.h")
1593 std::vector<std::filesystem::path> candidates;
1594 auto add_candidate = [&candidates](
const std::filesystem::path& candidate) {
1595 if (candidate.empty()) {
1598 auto normalized = candidate.lexically_normal();
1599 if (std::find(candidates.begin(), candidates.end(), normalized) ==
1601 candidates.push_back(normalized);
1607 if (!std::filesystem::is_directory(code_path)) {
1608 code_path = code_path.parent_path();
1610 add_candidate(code_path /
"z3dk.toml");
1619 add_candidate(std::filesystem::path(
filepath).parent_path() /
"z3dk.toml");
1622 for (
const auto& candidate : candidates) {
1623 if (!std::filesystem::exists(candidate)) {
1628 z3dk::Config config = z3dk::LoadConfigFile(candidate.string(), &error);
1629 if (!error.empty()) {
1630 LOG_WARN(
"Project",
"Failed to parse z3dk config '%s': %s",
1631 candidate.string().c_str(), error.c_str());
1635 const std::filesystem::path base_dir = candidate.parent_path();
1638 if (config.preset.has_value()) {
1643 for (
const auto& include_path : config.include_paths) {
1645 ResolveOptionalPath(base_dir, include_path));
1649 for (
const auto& define : config.defines) {
1654 for (
const auto& main_file : config.main_files) {
1656 ResolveOptionalPath(base_dir, main_file));
1659 if (config.std_includes_path.has_value()) {
1661 ResolveOptionalPath(base_dir, *config.std_includes_path);
1663 if (config.std_defines_path.has_value()) {
1665 ResolveOptionalPath(base_dir, *config.std_defines_path);
1667 if (config.mapper.has_value()) {
1670 if (config.rom_size.has_value()) {
1673 if (config.symbols_format.has_value()) {
1677 if (config.lsp_log_path.has_value()) {
1679 ResolveOptionalPath(base_dir, *config.lsp_log_path);
1683 for (
const auto& emit_path : config.emits) {
1687 for (
const auto& range : config.prohibited_memory_ranges) {
1689 {.start = range.start, .end = range.end, .reason = range.reason});
1693 config.warn_unused_symbols.value_or(
true);
1695 config.warn_branch_outside_bank.value_or(
true);
1699 config.warn_unauthorized_hook.value_or(
true);
1703 if (config.rom_path.has_value()) {
1706 if (config.symbols_path.has_value()) {
1708 ResolveOptionalPath(base_dir, *config.symbols_path);
1712 const std::string basename = BasenameLower(emit_path);
1713 if (basename.ends_with(
".mlb") &&
1716 }
else if (basename ==
"sourcemap.json") {
1718 }
else if (basename ==
"annotations.json") {
1720 }
else if (basename ==
"hooks.json") {
1722 }
else if (basename ==
"lint.json") {
1753 "Loaded z3dk config from %s (%zu include paths, %zu defines)",
1762#ifdef __EMSCRIPTEN__
1772 std::filesystem::path loaded_manifest_path;
1773 auto load_manifest = [&](
const std::filesystem::path& candidate,
1774 bool update_project_setting) ->
bool {
1775 if (candidate.empty() || !std::filesystem::exists(candidate)) {
1780 LOG_WARN(
"Project",
"Failed to load hack manifest %s: %s",
1781 candidate.string().c_str(),
1782 std::string(status.message()).c_str());
1785 loaded_manifest_path = candidate;
1786 if (update_project_setting) {
1789 LOG_DEBUG(
"Project",
"Loaded hack manifest: %s",
1790 candidate.string().c_str());
1799 if (has_explicit_manifest) {
1807 auto candidate = std::filesystem::path(code_path) /
"hack_manifest.json";
1808 (void)load_manifest(candidate,
true);
1813 const std::filesystem::path project_dir =
1814 std::filesystem::path(
filepath).parent_path();
1815 (void)load_manifest(project_dir /
"hack_manifest.json",
true);
1817 (void)load_manifest(project_dir.parent_path() /
"hack_manifest.json",
1826 auto try_load_registry = [&](
const std::filesystem::path& base) ->
bool {
1830 const auto planning = base /
"Docs" /
"Dev" /
"Planning";
1831 if (!std::filesystem::exists(planning)) {
1836 LOG_WARN(
"Project",
"Failed to load project registry from %s: %s",
1837 base.string().c_str(), std::string(status.message()).c_str());
1843 bool registry_loaded =
false;
1852 if (!registry_loaded && !loaded_manifest_path.empty()) {
1853 registry_loaded = try_load_registry(loaded_manifest_path.parent_path());
1857 if (!registry_loaded && !
filepath.empty()) {
1859 try_load_registry(std::filesystem::path(
filepath).parent_path());
1862 if (!registry_loaded) {
1867 "Hack manifest loaded but project registry was not found "
1868 "(code_folder='%s', manifest='%s')",
1869 code_folder.c_str(), loaded_manifest_path.string().c_str());
1874 size_t injected = 0;
1875 for (
const auto& [type_key, labels] :
1877 for (
const auto& [id_str, label] : labels) {
1882 LOG_DEBUG(
"Project",
"Loaded project registry: %zu resource labels injected",
1924 for (uint16_t tile = 0xB0; tile <= 0xBE; ++tile) {
1956 auto now = std::chrono::system_clock::now().time_since_epoch();
1958 std::chrono::duration_cast<std::chrono::milliseconds>(now).count();
1959 return absl::StrFormat(
"yaze_project_%lld", timestamp);
1963std::vector<ProjectManager::ProjectTemplate>
1965 std::vector<ProjectTemplate> templates;
1974 t.
name =
"Vanilla ROM Hack";
1976 "Standard ROM editing without custom ASM. Limited to vanilla features.";
1985 templates.push_back(t);
1991 t.
name =
"ZSCustomOverworld v2";
1993 "Basic overworld expansion: custom BG colors, main palettes, parent "
2006 templates.push_back(t);
2012 t.
name =
"ZSCustomOverworld v3 (Recommended)";
2014 "Full overworld expansion: wide/tall areas, animated GFX, overlays, "
2031 templates.push_back(t);
2037 t.
name =
"Randomizer Compatible";
2039 "Compatible with ALttP Randomizer. Minimal custom features to avoid "
2048 templates.push_back(t);
2058 t.
name =
"Dungeon Designer";
2059 t.
description =
"Focused on dungeon creation and modification.";
2066 templates.push_back(t);
2072 t.
name =
"Graphics Pack";
2074 "Project focused on graphics, sprites, and visual modifications.";
2083 templates.push_back(t);
2089 t.
name =
"Complete Overhaul";
2090 t.
description =
"Full-scale ROM hack with all features enabled.";
2105 templates.push_back(t);
2112 const std::string& template_name,
const std::string& project_name,
2113 const std::string& base_path) {
2115 auto status = project.
Create(project_name, base_path);
2121 if (template_name ==
"Full Overworld Mod") {
2125 project.
metadata.
tags = {
"overworld",
"maps",
"graphics"};
2126 }
else if (template_name ==
"Dungeon Designer") {
2130 project.
metadata.
tags = {
"dungeons",
"rooms",
"design"};
2131 }
else if (template_name ==
"Graphics Pack") {
2135 project.
metadata.
tags = {
"graphics",
"sprites",
"palettes"};
2136 }
else if (template_name ==
"Complete Overhaul") {
2142 project.
metadata.
tags = {
"complete",
"overhaul",
"full-mod"};
2145 status = project.
Save();
2154 const std::string& directory) {
2155#ifdef __EMSCRIPTEN__
2159 std::vector<std::string> projects;
2162 for (
const auto& entry : std::filesystem::directory_iterator(directory)) {
2163 if (entry.is_regular_file()) {
2164 std::string filename = entry.path().filename().string();
2165 if (filename.ends_with(
".yaze") || filename.ends_with(
".zsproj")) {
2166 projects.push_back(entry.path().string());
2168 }
else if (entry.is_directory()) {
2169 std::string filename = entry.path().filename().string();
2170 if (filename.ends_with(
".yazeproj")) {
2171 projects.push_back(entry.path().string());
2175 }
catch (
const std::filesystem::filesystem_error& e) {
2184#ifdef __EMSCRIPTEN__
2186 return absl::UnimplementedError(
2187 "Project backups are not supported in the web build");
2190 return absl::InvalidArgumentError(
"Project has no file path");
2193 std::filesystem::path project_path(project.
filepath);
2194 std::filesystem::path backup_dir = project_path.parent_path() /
"backups";
2195 std::filesystem::create_directories(backup_dir);
2197 auto now = std::chrono::system_clock::now();
2198 auto time_t = std::chrono::system_clock::to_time_t(now);
2199 std::stringstream ss;
2200 ss << std::put_time(std::localtime(&time_t),
"%Y%m%d_%H%M%S");
2202 std::string backup_filename = project.
name +
"_backup_" + ss.str() +
".yaze";
2203 std::filesystem::path backup_path = backup_dir / backup_filename;
2206 std::filesystem::copy_file(project.
filepath, backup_path);
2207 }
catch (
const std::filesystem::filesystem_error& e) {
2208 return absl::InternalError(
2209 absl::StrFormat(
"Failed to backup project: %s", e.what()));
2212 return absl::OkStatus();
2223 std::vector<std::string> recommendations;
2226 recommendations.push_back(
"Add a ROM file to begin editing");
2230 recommendations.push_back(
"Set up a code folder for assembly patches");
2234 recommendations.push_back(
"Create a labels file for better organization");
2238 recommendations.push_back(
"Add a project description for documentation");
2242 recommendations.push_back(
2243 "Consider setting up version control for your project");
2247 if (!missing_files.empty()) {
2248 recommendations.push_back(
2249 "Some project files are missing - use Project > Repair to fix");
2252 return recommendations;
2258 std::ifstream file(filename);
2259 if (!file.is_open()) {
2266 std::string current_type =
"";
2268 while (std::getline(file, line)) {
2269 if (line.empty() || line[0] ==
'#')
2273 if (line[0] ==
'[' && line.back() ==
']') {
2274 current_type = line.substr(1, line.length() - 2);
2279 size_t eq_pos = line.find(
'=');
2280 if (eq_pos != std::string::npos && !current_type.empty()) {
2281 std::string key = line.substr(0, eq_pos);
2282 std::string value = line.substr(eq_pos + 1);
2283 labels_[current_type][key] = value;
2297 if (!file.is_open())
2300 file <<
"# yaze Resource Labels\n";
2301 file <<
"# Format: [type] followed by key=value pairs\n\n";
2303 for (
const auto& [type, type_labels] :
labels_) {
2304 if (!type_labels.empty()) {
2305 file <<
"[" << type <<
"]\n";
2306 for (
const auto& [key, value] : type_labels) {
2307 file << key <<
"=" << value <<
"\n";
2318 if (!p_open || !*p_open)
2322 if (ImGui::Begin(
"Resource Labels", p_open)) {
2323 ImGui::Text(
"Resource Labels Manager");
2325 ImGui::Text(
"Total types: %zu",
labels_.size());
2327 for (
const auto& [type, type_labels] :
labels_) {
2328 if (ImGui::TreeNode(type.c_str())) {
2329 ImGui::Text(
"Labels: %zu", type_labels.size());
2330 for (
const auto& [key, value] : type_labels) {
2331 ImGui::Text(
"%s = %s", key.c_str(), value.c_str());
2341 const std::string& key,
2342 const std::string& newValue) {
2343 labels_[type][key] = newValue;
2347 bool selected,
const std::string& type,
const std::string& key,
2348 const std::string& defaultValue) {
2350 if (ImGui::Selectable(
2351 absl::StrFormat(
"%s: %s", key.c_str(),
GetLabel(type, key).c_str())
2359 const std::string& key) {
2360 auto type_it =
labels_.find(type);
2364 auto label_it = type_it->second.find(key);
2365 if (label_it == type_it->second.end())
2368 return label_it->second;
2372 const std::string& type,
const std::string& key,
2373 const std::string& defaultValue) {
2374 auto existing =
GetLabel(type, key);
2375 if (!existing.empty())
2378 labels_[type][key] = defaultValue;
2379 return defaultValue;
2387 const std::unordered_map<
2388 std::string, std::unordered_map<std::string, std::string>>& labels) {
2417 LOG_DEBUG(
"Project",
"Initialized embedded labels:");
2419 LOG_DEBUG(
"Project",
" - %d entrance names",
2421 LOG_DEBUG(
"Project",
" - %d sprite names",
2423 LOG_DEBUG(
"Project",
" - %d overlord names",
2426 LOG_DEBUG(
"Project",
" - %d music names",
2428 LOG_DEBUG(
"Project",
" - %d graphics names",
2430 LOG_DEBUG(
"Project",
" - %d room effect names",
2432 LOG_DEBUG(
"Project",
" - %d room tag names",
2434 LOG_DEBUG(
"Project",
" - %d tile type names",
2437 return absl::OkStatus();
2438 }
catch (
const std::exception& e) {
2439 return absl::InternalError(
2440 absl::StrCat(
"Failed to initialize embedded labels: ", e.what()));
2445 const std::string& default_value)
const {
2449 auto label_it = type_it->second.find(std::to_string(
id));
2450 if (label_it != type_it->second.end()) {
2451 return label_it->second;
2455 return default_value.empty() ? resource_type +
"_" + std::to_string(
id)
2460#ifdef __EMSCRIPTEN__
2462 return absl::UnimplementedError(
2463 "File-based label import is not supported in the web build");
2466 if (!file.is_open()) {
2467 return absl::InvalidArgumentError(
2468 absl::StrFormat(
"Cannot open labels file: %s",
filepath));
2471 std::stringstream buffer;
2472 buffer << file.rdbuf();
2480 const std::string& content) {
2487 auto status = provider.ImportFromZScreamFormat(content);
2492 LOG_DEBUG(
"Project",
"Imported ZScream labels:");
2493 LOG_DEBUG(
"Project",
" - %d sprite labels",
2497 LOG_DEBUG(
"Project",
" - %d room tag labels",
2500 return absl::OkStatus();
2509 LOG_DEBUG(
"Project",
"Initialized ResourceLabelProvider with project labels");
2510 LOG_DEBUG(
"Project",
" - prefer_hmagic_names: %s",
2512 LOG_DEBUG(
"Project",
" - hack_manifest: %s",
2520#ifdef YAZE_ENABLE_JSON_PROJECT_FORMAT
2522absl::Status YazeProject::LoadFromJsonFormat(
const std::string& project_path) {
2523#ifdef __EMSCRIPTEN__
2524 return absl::UnimplementedError(
2525 "JSON project format loading is not supported in the web build");
2527 std::ifstream file(project_path);
2528 if (!file.is_open()) {
2529 return absl::InvalidArgumentError(
2530 absl::StrFormat(
"Cannot open JSON project file: %s", project_path));
2538 if (j.contains(
"yaze_project")) {
2539 auto& proj = j[
"yaze_project"];
2541 if (proj.contains(
"name"))
2542 name = proj[
"name"].get<std::string>();
2543 if (proj.contains(
"description"))
2545 if (proj.contains(
"author"))
2547 if (proj.contains(
"version"))
2549 if (proj.contains(
"created"))
2551 if (proj.contains(
"modified"))
2553 if (proj.contains(
"created_by"))
2557 if (proj.contains(
"rom_filename"))
2558 rom_filename = proj[
"rom_filename"].get<std::string>();
2559 if (proj.contains(
"rom_backup_folder"))
2561 if (proj.contains(
"code_folder"))
2562 code_folder = proj[
"code_folder"].get<std::string>();
2563 if (proj.contains(
"assets_folder"))
2565 if (proj.contains(
"patches_folder"))
2567 if (proj.contains(
"labels_filename"))
2569 if (proj.contains(
"symbols_filename"))
2571 if (proj.contains(
"hack_manifest_file"))
2574 if (proj.contains(
"rom") && proj[
"rom"].is_object()) {
2575 auto& rom = proj[
"rom"];
2576 if (rom.contains(
"role"))
2578 if (rom.contains(
"expected_hash"))
2580 if (rom.contains(
"write_policy"))
2586 if (proj.contains(
"use_embedded_labels")) {
2591 if (proj.contains(
"feature_flags")) {
2592 auto& flags = proj[
"feature_flags"];
2595 if (flags.contains(
"kSaveDungeonMaps"))
2597 flags[
"kSaveDungeonMaps"].get<
bool>();
2598 if (flags.contains(
"kSaveOverworldMaps"))
2600 flags[
"kSaveOverworldMaps"].get<
bool>();
2601 if (flags.contains(
"kSaveOverworldEntrances"))
2603 flags[
"kSaveOverworldEntrances"].get<
bool>();
2604 if (flags.contains(
"kSaveOverworldExits"))
2606 flags[
"kSaveOverworldExits"].get<
bool>();
2607 if (flags.contains(
"kSaveOverworldItems"))
2609 flags[
"kSaveOverworldItems"].get<
bool>();
2610 if (flags.contains(
"kSaveOverworldProperties"))
2612 flags[
"kSaveOverworldProperties"].get<
bool>();
2613 if (flags.contains(
"kSaveDungeonObjects"))
2615 flags[
"kSaveDungeonObjects"].get<
bool>();
2616 if (flags.contains(
"kSaveDungeonSprites"))
2618 flags[
"kSaveDungeonSprites"].get<
bool>();
2619 if (flags.contains(
"kSaveDungeonRoomHeaders"))
2621 flags[
"kSaveDungeonRoomHeaders"].get<
bool>();
2622 if (flags.contains(
"kSaveDungeonTorches"))
2624 flags[
"kSaveDungeonTorches"].get<
bool>();
2625 if (flags.contains(
"kSaveDungeonPits"))
2627 flags[
"kSaveDungeonPits"].get<
bool>();
2628 if (flags.contains(
"kSaveDungeonBlocks"))
2630 flags[
"kSaveDungeonBlocks"].get<
bool>();
2631 if (flags.contains(
"kSaveDungeonCollision"))
2633 flags[
"kSaveDungeonCollision"].get<
bool>();
2634 if (flags.contains(
"kSaveDungeonWaterFillZones"))
2636 flags[
"kSaveDungeonWaterFillZones"].get<
bool>();
2637 if (flags.contains(
"kSaveDungeonChests"))
2639 flags[
"kSaveDungeonChests"].get<
bool>();
2640 if (flags.contains(
"kSaveDungeonPotItems"))
2642 flags[
"kSaveDungeonPotItems"].get<
bool>();
2643 if (flags.contains(
"kSaveDungeonEntrances"))
2645 flags[
"kSaveDungeonEntrances"].get<
bool>();
2646 if (flags.contains(
"kSaveDungeonPalettes"))
2648 flags[
"kSaveDungeonPalettes"].get<
bool>();
2649 if (flags.contains(
"kSaveGraphicsSheet"))
2651 flags[
"kSaveGraphicsSheet"].get<
bool>();
2652 if (flags.contains(
"kSaveAllPalettes"))
2654 flags[
"kSaveAllPalettes"].get<
bool>();
2655 if (flags.contains(
"kSaveGfxGroups"))
2657 if (flags.contains(
"kSaveMessages"))
2662 if (proj.contains(
"workspace_settings")) {
2663 auto& ws = proj[
"workspace_settings"];
2664 if (ws.contains(
"auto_save_enabled"))
2666 ws[
"auto_save_enabled"].get<
bool>();
2667 if (ws.contains(
"auto_save_interval"))
2669 ws[
"auto_save_interval"].get<
float>();
2670 if (ws.contains(
"backup_on_save"))
2672 if (ws.contains(
"backup_retention_count"))
2674 ws[
"backup_retention_count"].get<
int>();
2675 if (ws.contains(
"backup_keep_daily"))
2677 ws[
"backup_keep_daily"].get<
bool>();
2678 if (ws.contains(
"backup_keep_daily_days"))
2680 ws[
"backup_keep_daily_days"].get<
int>();
2683 if (proj.contains(
"rom_addresses") && proj[
"rom_addresses"].is_object()) {
2685 for (
auto it = proj[
"rom_addresses"].begin();
2686 it != proj[
"rom_addresses"].end(); ++it) {
2687 if (it.value().is_number_unsigned()) {
2689 it.value().get<uint32_t>();
2690 }
else if (it.value().is_string()) {
2692 if (parsed.has_value()) {
2699 if (proj.contains(
"custom_objects") &&
2700 proj[
"custom_objects"].is_object()) {
2702 for (
auto it = proj[
"custom_objects"].begin();
2703 it != proj[
"custom_objects"].end(); ++it) {
2704 if (!it.value().is_array())
2707 if (!parsed.has_value()) {
2710 std::vector<std::string> files;
2711 for (
const auto& entry : it.value()) {
2712 if (entry.is_string()) {
2713 files.push_back(entry.get<std::string>());
2716 if (!files.empty()) {
2722 if (proj.contains(
"agent_settings") &&
2723 proj[
"agent_settings"].is_object()) {
2724 auto& agent = proj[
"agent_settings"];
2751 if (agent.contains(
"favorite_models") &&
2752 agent[
"favorite_models"].is_array()) {
2754 for (
const auto& model : agent[
"favorite_models"]) {
2755 if (model.is_string())
2757 model.get<std::string>());
2760 if (agent.contains(
"model_chain") && agent[
"model_chain"].is_array()) {
2762 for (
const auto& model : agent[
"model_chain"]) {
2763 if (model.is_string())
2788 agent.value(
"enable_tool_memory_inspector",
2795 if (proj.contains(
"build_script"))
2796 build_script = proj[
"build_script"].get<std::string>();
2797 if (proj.contains(
"output_folder"))
2799 if (proj.contains(
"git_repository"))
2801 if (proj.contains(
"track_changes"))
2805 return absl::OkStatus();
2806 }
catch (
const json::exception& e) {
2807 return absl::InvalidArgumentError(
2808 absl::StrFormat(
"JSON parse error: %s", e.what()));
2812absl::Status YazeProject::SaveToJsonFormat() {
2813#ifdef __EMSCRIPTEN__
2814 return absl::UnimplementedError(
2815 "JSON project format saving is not supported in the web build");
2818 auto& proj = j[
"yaze_project"];
2822 proj[
"name"] =
name;
2842 proj[
"rom"][
"write_policy"] =
2851 proj[
"feature_flags"][
"kSaveOverworldMaps"] =
2853 proj[
"feature_flags"][
"kSaveOverworldEntrances"] =
2855 proj[
"feature_flags"][
"kSaveOverworldExits"] =
2857 proj[
"feature_flags"][
"kSaveOverworldItems"] =
2859 proj[
"feature_flags"][
"kSaveOverworldProperties"] =
2861 proj[
"feature_flags"][
"kSaveDungeonObjects"] =
2863 proj[
"feature_flags"][
"kSaveDungeonSprites"] =
2865 proj[
"feature_flags"][
"kSaveDungeonRoomHeaders"] =
2867 proj[
"feature_flags"][
"kSaveDungeonTorches"] =
2870 proj[
"feature_flags"][
"kSaveDungeonBlocks"] =
2872 proj[
"feature_flags"][
"kSaveDungeonCollision"] =
2874 proj[
"feature_flags"][
"kSaveDungeonWaterFillZones"] =
2876 proj[
"feature_flags"][
"kSaveDungeonChests"] =
2878 proj[
"feature_flags"][
"kSaveDungeonPotItems"] =
2880 proj[
"feature_flags"][
"kSaveDungeonEntrances"] =
2882 proj[
"feature_flags"][
"kSaveDungeonPalettes"] =
2884 proj[
"feature_flags"][
"kSaveGraphicsSheet"] =
2891 proj[
"workspace_settings"][
"auto_save_enabled"] =
2893 proj[
"workspace_settings"][
"auto_save_interval"] =
2895 proj[
"workspace_settings"][
"backup_on_save"] =
2897 proj[
"workspace_settings"][
"backup_retention_count"] =
2899 proj[
"workspace_settings"][
"backup_keep_daily"] =
2901 proj[
"workspace_settings"][
"backup_keep_daily_days"] =
2904 auto& agent = proj[
"agent_settings"];
2931 agent[
"enable_tool_memory_inspector"] =
2936 auto& addrs = proj[
"rom_addresses"];
2943 auto& objs = proj[
"custom_objects"];
2945 objs[absl::StrFormat(
"0x%X", object_id)] = files;
2956 if (!file.is_open()) {
2957 return absl::InvalidArgumentError(
2958 absl::StrFormat(
"Cannot write JSON project file: %s",
filepath));
2962 return absl::OkStatus();
2970 if (!config_dir.ok()) {
2977#ifdef __EMSCRIPTEN__
2978 auto status = platform::WasmStorage::SaveProject(
2981 LOG_WARN(
"RecentFilesManager",
"Could not persist recent files: %s",
2982 status.ToString().c_str());
2988 if (!config_dir_status.ok()) {
2989 LOG_ERROR(
"Project",
"Failed to get or create config directory: %s",
2990 config_dir_status.status().ToString().c_str());
2995 std::ofstream file(filepath);
2996 if (!file.is_open()) {
2997 LOG_WARN(
"RecentFilesManager",
"Could not save recent files to %s",
3003 file << file_path << std::endl;
3008#ifdef __EMSCRIPTEN__
3010 if (!storage_or.ok()) {
3014 std::istringstream stream(storage_or.value());
3016 while (std::getline(stream, line)) {
3017 if (!line.empty()) {
3025 std::ifstream file(filepath);
3026 if (!file.is_open()) {
3033 while (std::getline(file, line)) {
3034 if (!line.empty()) {
void Clear()
Clear any loaded manifest state.
const ProjectRegistry & project_registry() const
bool HasProjectRegistry() const
absl::Status LoadProjectRegistry(const std::string &code_folder)
Load project registry data from the code folder.
absl::Status LoadFromFile(const std::string &filepath)
Load manifest from a JSON file path.
bool loaded() const
Check if the manifest has been loaded.
static std::vector< std::string > FindProjectsInDirectory(const std::string &directory)
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.
#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)
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)
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)
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)
std::string FormatHexUint32(uint32_t value)
std::string SanitizeStorageKey(absl::string_view input)
std::filesystem::path MakeProjectSaveTempPath(const std::filesystem::path &target_path)
uint64_t CurrentProcessIdForProjectSave()
std::string FormatHexUintList(const std::vector< uint16_t > &values)
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)
ResourceLabelProvider & GetResourceLabels()
Get the global ResourceLabelProvider instance.
#define RETURN_IF_ERROR(expr)
bool kSaveOverworldProperties
bool kApplyZSCustomOverworldASM
bool kLoadCustomOverworld
bool kSaveOverworldEntrances
bool kEnableCustomObjects
struct yaze::core::FeatureFlags::Flags::Dungeon dungeon
struct yaze::core::FeatureFlags::Flags::Overworld overworld
std::unordered_map< std::string, std::unordered_map< std::string, std::string > > all_resource_labels
std::unordered_map< std::string, uint32_t > addresses
std::vector< uint16_t > track_object_ids
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
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)
struct yaze::project::YazeProject::MusicPersistence music_persistence
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)
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::vector< std::string > additional_roms
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::string GenerateProjectId() const
absl::Status Create(const std::string &project_name, const std::string &base_path)
std::string assets_folder
void ReloadHackManifest()
absl::Status LoadAllSettings()
std::string labels_filename
std::vector< std::string > asm_sources
std::string hack_manifest_file
std::string GetDisplayName() const
std::vector< std::string > GetMissingFiles() const
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
absl::Status InitializeEmbeddedLabels(const std::unordered_map< std::string, std::unordered_map< std::string, std::string > > &labels)
absl::Status SaveAs(const std::string &new_path)
struct yaze::project::YazeProject::AgentSettings agent_settings
DungeonOverlaySettings dungeon_overlay
absl::Status ImportFromZScreamFormat(const std::string &project_path)
void InitializeDefaults()
std::string GetAbsolutePath(const std::string &relative_path) const
std::string GetLabel(const std::string &resource_type, int id, const std::string &default_value="") const
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
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