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<std::string> result;
108 std::vector<std::string> parts = absl::StrSplit(value,
',');
109 result.reserve(parts.size());
110 for (
const auto& part : parts) {
111 std::string trimmed = part;
112 trimmed.erase(0, trimmed.find_first_not_of(
" \t"));
113 trimmed.erase(trimmed.find_last_not_of(
" \t") + 1);
114 result.push_back(std::move(trimmed));
120 std::vector<uint16_t> result;
126 result.reserve(parts.size());
127 for (
const auto& part : parts) {
128 std::string token = part;
129 if (token.rfind(
"0x", 0) == 0 || token.rfind(
"0X", 0) == 0) {
130 token = token.substr(2);
132 result.push_back(
static_cast<uint16_t
>(std::stoul(token,
nullptr, 16)));
138 result.push_back(
static_cast<uint16_t
>(std::stoul(token,
nullptr, 10)));
151 std::string token = value;
152 if (token.rfind(
"0x", 0) == 0 || token.rfind(
"0X", 0) == 0) {
153 token = token.substr(2);
155 return static_cast<uint32_t
>(std::stoul(token,
nullptr, 16));
161 return static_cast<uint32_t
>(std::stoul(token,
nullptr, 10));
168 return absl::StrJoin(values,
",", [](std::string* out, uint16_t value) {
169 out->append(absl::StrFormat(
"0x%02X", value));
174 return absl::StrFormat(
"0x%06X", value);
178 std::string key(input);
179 for (
char& c : key) {
180 if (!std::isalnum(
static_cast<unsigned char>(c))) {
191 auto [key, parsed_value] = ParseKeyValue(value);
195 return {key, parsed_value.empty() ?
"1" : parsed_value};
199 const std::string& value) {
203 std::filesystem::path path(value);
204 if (path.is_absolute()) {
205 return path.lexically_normal().string();
207 return (base_dir / path).lexically_normal().string();
211 return ToLowerCopy(std::filesystem::path(path).filename().
string());
214#ifndef __EMSCRIPTEN__
217 return static_cast<uint64_t
>(::GetCurrentProcessId());
219 return static_cast<uint64_t
>(::getpid());
224 const std::filesystem::path& target_path) {
225 static std::atomic<uint64_t> next_temp_id{0};
226 std::filesystem::path temp_path = target_path;
229 std::to_string(next_temp_id.fetch_add(1, std::memory_order_relaxed));
234 std::error_code remove_error;
235 std::filesystem::remove(temp_path, remove_error);
239 const std::filesystem::path& target_path, absl::string_view contents,
240 bool replace_existing) {
242 std::ofstream file(temp_path, std::ios::binary | std::ios::trunc);
243 if (!file.is_open()) {
244 return absl::InvalidArgumentError(absl::StrFormat(
245 "Cannot create temporary project file: %s", temp_path.string()));
248 file.write(contents.data(),
static_cast<std::streamsize
>(contents.size()));
253 return absl::InternalError(absl::StrFormat(
254 "Failed to write temporary project file: %s", temp_path.string()));
260 return absl::InternalError(absl::StrFormat(
261 "Failed to close temporary project file: %s", temp_path.string()));
264 std::error_code rename_error;
266 const DWORD move_flags =
267 MOVEFILE_WRITE_THROUGH |
268 (replace_existing ? MOVEFILE_REPLACE_EXISTING :
static_cast<DWORD
>(0));
269 if (!::MoveFileExW(temp_path.c_str(), target_path.c_str(), move_flags)) {
270 rename_error = std::error_code(
static_cast<int>(::GetLastError()),
271 std::system_category());
274 if (replace_existing) {
275 std::filesystem::rename(temp_path, target_path, rename_error);
276 }
else if (::link(temp_path.c_str(), target_path.c_str()) != 0) {
277 rename_error = std::error_code(errno, std::generic_category());
284 bool target_already_exists = rename_error == std::errc::file_exists;
286 target_already_exists = target_already_exists ||
287 rename_error.value() == ERROR_FILE_EXISTS ||
288 rename_error.value() == ERROR_ALREADY_EXISTS;
290 if (!replace_existing && target_already_exists) {
291 return absl::AlreadyExistsError(absl::StrFormat(
292 "Project file already exists: %s", target_path.string()));
294 return absl::InternalError(
295 absl::StrFormat(
"Failed to replace project file %s: %s",
296 target_path.string(), rename_error.message()));
299 return absl::OkStatus();
304#ifndef __EMSCRIPTEN__
306 absl::string_view contents,
307 bool replace_existing) {
308 return WriteProjectFileAtomicallyImpl(
309 std::filesystem::path(std::string(target_path)), contents,
329 std::string lower = ToLowerCopy(std::string(value));
330 if (lower ==
"base") {
333 if (lower ==
"patched") {
336 if (lower ==
"release") {
355 std::string lower = ToLowerCopy(std::string(value));
356 if (lower ==
"allow") {
359 if (lower ==
"block") {
367 const std::string& base_path) {
369 filepath = base_path +
"/" + project_name +
".yaze";
372 auto now = std::chrono::system_clock::now();
373 auto time_t = std::chrono::system_clock::to_time_t(now);
374 std::stringstream ss;
375 ss << std::put_time(std::localtime(&time_t),
"%Y-%m-%d %H:%M:%S");
386#ifndef __EMSCRIPTEN__
388 std::filesystem::path project_dir(base_path +
"/" + project_name);
389 std::filesystem::create_directories(project_dir);
390 std::filesystem::create_directories(project_dir /
"code");
391 std::filesystem::create_directories(project_dir /
"assets");
392 std::filesystem::create_directories(project_dir /
"patches");
393 std::filesystem::create_directories(project_dir /
"backups");
394 std::filesystem::create_directories(project_dir /
"output");
427 auto current = std::filesystem::path(path).lexically_normal();
429 for (; !current.empty(); current = current.parent_path()) {
430 if (current.extension() ==
".yazeproj") {
432 if (std::filesystem::is_directory(current, ec) && !ec) {
433 return current.string();
437 if (current == current.parent_path()) {
449 std::string resolved_path = project_path;
451 if (!bundle_root.empty() && bundle_root != project_path) {
453 resolved_path = bundle_root;
456#ifndef __EMSCRIPTEN__
461 std::error_code absolute_ec;
462 const auto absolute_path =
463 std::filesystem::absolute(resolved_path, absolute_ec).lexically_normal();
465 resolved_path = absolute_path.string();
474 auto storage_or = platform::WasmStorage::LoadProject(storage_key);
475 if (storage_or.ok()) {
481 absl::Status load_status;
482 if (resolved_path.ends_with(
".yazeproj")) {
485 const std::filesystem::path bundle_path(resolved_path);
487 if (!std::filesystem::exists(bundle_path, ec) || ec ||
488 !std::filesystem::is_directory(bundle_path, ec) || ec) {
489 return absl::InvalidArgumentError(
490 absl::StrFormat(
"Project bundle does not exist: %s", resolved_path));
495 const std::filesystem::path project_file = bundle_path /
"project.yaze";
498 if (!std::filesystem::exists(project_file, ec) || ec) {
501 name = bundle_path.stem().string();
504 auto now = std::chrono::system_clock::now();
505 auto time_t = std::chrono::system_clock::to_time_t(now);
506 std::stringstream ss;
507 ss << std::put_time(std::localtime(&time_t),
"%Y-%m-%d %H:%M:%S");
527 const std::filesystem::path rom_candidate = bundle_path /
"rom";
534 const std::filesystem::path project_dir = bundle_path /
"project";
535 const std::filesystem::path code_dir = bundle_path /
"code";
536 if (std::filesystem::exists(project_dir, ec) &&
537 std::filesystem::is_directory(project_dir, ec) && !ec) {
539 }
else if (std::filesystem::exists(code_dir, ec) &&
540 std::filesystem::is_directory(code_dir, ec) && !ec) {
555 }
else if (resolved_path.ends_with(
".yaze")) {
559 std::ifstream file(resolved_path);
560 if (file.is_open()) {
561 std::stringstream buffer;
562 buffer << file.rdbuf();
563 std::string content = buffer.str();
565#ifdef YAZE_ENABLE_JSON_PROJECT_FORMAT
566 if (!content.empty() && content.front() ==
'{') {
567 LOG_DEBUG(
"Project",
"Detected JSON format project file");
568 load_status = LoadFromJsonFormat(resolved_path);
576 return absl::InvalidArgumentError(
577 absl::StrFormat(
"Cannot open project file: %s", resolved_path));
579 }
else if (resolved_path.ends_with(
".zsproj")) {
583 return absl::InvalidArgumentError(
"Unsupported project file format");
586 if (!load_status.ok()) {
600 return absl::OkStatus();
612 const std::string& project_path) {
613 if (project_path.empty()) {
614 return absl::InvalidArgumentError(
"Project file path cannot be empty");
619#ifndef __EMSCRIPTEN__
621 const auto absolute_path =
622 std::filesystem::absolute(project_path, ec).lexically_normal();
624 ec ? std::filesystem::path(project_path).lexically_normal().string()
625 : absolute_path.string();
632 }
catch (
const std::exception& error) {
633 return absl::InvalidArgumentError(
634 absl::StrFormat(
"Invalid project file value: %s", error.what()));
639 return absl::OkStatus();
643 std::string old_filepath =
filepath;
646 auto status =
Save();
658 }
else if (!
name.empty()) {
661 base = std::filesystem::path(
filepath).stem().string();
663 base = SanitizeStorageKey(base);
664 if (suffix.empty()) {
667 return absl::StrFormat(
"%s_%s", base, suffix);
675 for (
size_t i = 0; i < content.size(); ++i) {
676 if (content[i] ==
'\r' &&
677 (i + 1 >= content.size() || content[i + 1] !=
'\n')) {
686 std::ostringstream file;
689 file <<
"# yaze Project File\n";
690 file <<
"# Format Version: 2.0\n";
695 file <<
"[project]\n";
696 file <<
"name=" <<
name <<
"\n";
706 file <<
"tags=" << absl::StrJoin(
metadata.
tags,
",") <<
"\n\n";
721 file <<
"additional_roms=" << absl::StrJoin(
additional_roms,
",") <<
"\n\n";
731 file <<
"[feature_flags]\n";
732 file <<
"load_custom_overworld="
735 file <<
"apply_zs_custom_overworld_asm="
739 file <<
"save_dungeon_maps="
741 file <<
"save_overworld_maps="
744 file <<
"save_overworld_entrances="
747 file <<
"save_overworld_exits="
750 file <<
"save_overworld_items="
753 file <<
"save_overworld_properties="
756 file <<
"save_dungeon_objects="
758 file <<
"save_dungeon_sprites="
760 file <<
"save_dungeon_room_headers="
762 file <<
"save_dungeon_torches="
764 file <<
"save_dungeon_pits="
766 file <<
"save_dungeon_blocks="
768 file <<
"save_dungeon_collision="
770 file <<
"save_dungeon_water_fill_zones="
773 file <<
"save_dungeon_chests="
775 file <<
"save_dungeon_pot_items="
777 file <<
"save_dungeon_entrances="
779 file <<
"save_dungeon_palettes="
781 file <<
"save_graphics_sheet="
783 file <<
"save_all_palettes="
785 file <<
"save_gfx_groups="
789 file <<
"enable_custom_objects="
793 file <<
"[workspace]\n";
798 file <<
"autosave_enabled="
802 file <<
"backup_on_save="
806 file <<
"backup_keep_daily="
812 file <<
"show_collision="
814 file <<
"prefer_hmagic_names="
818 file <<
"saved_layouts="
825 if (track_tiles.empty()) {
826 for (uint16_t tile = 0xB0; tile <= 0xBE; ++tile) {
827 track_tiles.push_back(tile);
831 if (track_stop_tiles.empty()) {
832 track_stop_tiles = {0xB7, 0xB8, 0xB9, 0xBA};
835 if (track_switch_tiles.empty()) {
836 track_switch_tiles = {0xD0, 0xD1, 0xD2, 0xD3};
839 if (track_object_ids.empty()) {
840 track_object_ids = {0x31};
843 if (minecart_sprite_ids.empty()) {
844 minecart_sprite_ids = {0xA3};
846 file <<
"[dungeon_overlay]\n";
847 file <<
"track_tiles=" << FormatHexUintList(track_tiles) <<
"\n";
848 file <<
"track_stop_tiles=" << FormatHexUintList(track_stop_tiles) <<
"\n";
849 file <<
"track_switch_tiles=" << FormatHexUintList(track_switch_tiles)
851 file <<
"track_object_ids=" << FormatHexUintList(track_object_ids) <<
"\n";
852 file <<
"minecart_sprite_ids=" << FormatHexUintList(minecart_sprite_ids)
856 file <<
"[rom_addresses]\n";
858 file << key <<
"=" << FormatHexUint32(value) <<
"\n";
864 file <<
"[custom_objects]\n";
866 file << absl::StrFormat(
"object_0x%X", object_id) <<
"="
867 << absl::StrJoin(files,
",") <<
"\n";
873 file <<
"[agent_settings]\n";
878 file <<
"custom_system_prompt="
880 file <<
"use_custom_prompt="
882 file <<
"show_reasoning="
890 file <<
"stream_responses="
892 file <<
"favorite_models="
897 file <<
"enable_tool_resources="
899 file <<
"enable_tool_dungeon="
901 file <<
"enable_tool_overworld="
903 file <<
"enable_tool_messages="
905 file <<
"enable_tool_dialogue="
907 file <<
"enable_tool_gui="
909 file <<
"enable_tool_music="
911 file <<
"enable_tool_sprite="
913 file <<
"enable_tool_emulator="
915 file <<
"enable_tool_memory_inspector="
923 file <<
"[keybindings]\n";
925 file << key <<
"=" << value <<
"\n";
932 file <<
"[editor_visibility]\n";
934 file << key <<
"=" << (value ?
"true" :
"false") <<
"\n";
941 if (!labels.empty()) {
942 file <<
"[labels_" << type <<
"]\n";
943 for (
const auto& [key, value] : labels) {
944 file << key <<
"=" << value <<
"\n";
955 file <<
"track_changes=" << (
track_changes ?
"true" :
"false") <<
"\n";
960 file <<
"asm_sources=" << absl::StrJoin(
asm_sources,
",") <<
"\n";
966 file <<
"persist_custom_music="
973 file <<
"[zscream_compatibility]\n";
976 file << key <<
"=" << value <<
"\n";
981 file <<
"# End of YAZE Project File\n";
989 std::string serialized = file.str();
990 if (ContainsLoneCarriageReturn(serialized)) {
991 return absl::InvalidArgumentError(
992 "Project contains a lone carriage return in a value; refusing to "
993 "write a descriptor that could not be read back");
999 if (ContainsLoneCarriageReturn(content)) {
1000 return absl::InvalidArgumentError(
1001 "Project file contains unsupported lone carriage returns");
1004 std::istringstream stream(content);
1006 std::string current_section;
1008 while (std::getline(stream, line)) {
1012 if (!line.empty() && line.back() ==
'\r') {
1016 if (line.empty() || line[0] ==
'#')
1019 if (line.front() ==
'[' && line.back() ==
']') {
1020 current_section = line.substr(1, line.length() - 2);
1024 auto [key, value] = ParseKeyValue(line);
1028 if (current_section ==
"project") {
1031 else if (key ==
"description")
1033 else if (key ==
"author")
1035 else if (key ==
"license")
1037 else if (key ==
"version")
1039 else if (key ==
"created_date")
1041 else if (key ==
"last_modified")
1043 else if (key ==
"yaze_version")
1045 else if (key ==
"created_by")
1047 else if (key ==
"tags")
1049 else if (key ==
"project_id")
1051 }
else if (current_section ==
"files") {
1052 if (key ==
"rom_filename")
1054 else if (key ==
"rom_backup_folder")
1056 else if (key ==
"code_folder")
1058 else if (key ==
"assets_folder")
1060 else if (key ==
"patches_folder")
1062 else if (key ==
"labels_filename")
1064 else if (key ==
"symbols_filename")
1066 else if (key ==
"output_folder")
1068 else if (key ==
"custom_objects_folder")
1070 else if (key ==
"hack_manifest_file")
1072 else if (key ==
"additional_roms")
1074 }
else if (current_section ==
"rom") {
1077 else if (key ==
"expected_hash")
1079 else if (key ==
"write_policy")
1081 }
else if (current_section ==
"feature_flags") {
1082 if (key ==
"load_custom_overworld")
1084 else if (key ==
"apply_zs_custom_overworld_asm")
1086 else if (key ==
"save_dungeon_maps")
1088 else if (key ==
"save_overworld_maps")
1090 else if (key ==
"save_overworld_entrances")
1092 else if (key ==
"save_overworld_exits")
1094 else if (key ==
"save_overworld_items")
1096 else if (key ==
"save_overworld_properties")
1098 else if (key ==
"save_dungeon_objects")
1100 else if (key ==
"save_dungeon_sprites")
1102 else if (key ==
"save_dungeon_room_headers")
1104 else if (key ==
"save_dungeon_torches")
1106 else if (key ==
"save_dungeon_pits")
1108 else if (key ==
"save_dungeon_blocks")
1110 else if (key ==
"save_dungeon_collision")
1112 else if (key ==
"save_dungeon_water_fill_zones")
1114 else if (key ==
"save_dungeon_chests")
1116 else if (key ==
"save_dungeon_pot_items")
1118 else if (key ==
"save_dungeon_entrances")
1120 else if (key ==
"save_dungeon_palettes")
1122 else if (key ==
"save_graphics_sheet")
1124 else if (key ==
"save_all_palettes")
1126 else if (key ==
"save_gfx_groups")
1128 else if (key ==
"save_messages")
1130 else if (key ==
"enable_custom_objects")
1132 }
else if (current_section ==
"workspace") {
1133 if (key ==
"font_global_scale")
1135 else if (key ==
"dark_mode")
1137 else if (key ==
"ui_theme")
1139 else if (key ==
"autosave_enabled")
1141 else if (key ==
"autosave_interval_secs")
1143 else if (key ==
"backup_on_save")
1145 else if (key ==
"backup_retention_count")
1147 else if (key ==
"backup_keep_daily")
1149 else if (key ==
"backup_keep_daily_days")
1151 else if (key ==
"show_grid")
1153 else if (key ==
"show_collision")
1155 else if (key ==
"prefer_hmagic_names")
1157 else if (key ==
"last_layout_preset")
1159 else if (key ==
"saved_layouts")
1161 else if (key ==
"recent_files")
1163 }
else if (current_section ==
"dungeon_overlay") {
1164 if (key ==
"track_tiles")
1166 else if (key ==
"track_stop_tiles")
1168 else if (key ==
"track_switch_tiles")
1170 else if (key ==
"track_object_ids")
1172 else if (key ==
"minecart_sprite_ids")
1174 }
else if (current_section ==
"rom_addresses") {
1175 auto parsed = ParseHexUint32(value);
1176 if (parsed.has_value()) {
1179 }
else if (current_section ==
"custom_objects") {
1180 std::string id_token = key;
1181 if (absl::StartsWith(id_token,
"object_")) {
1182 id_token = id_token.substr(7);
1184 auto parsed = ParseHexUint32(id_token);
1185 if (parsed.has_value()) {
1187 ParsePositionalStringList(value);
1189 }
else if (current_section ==
"agent_settings") {
1190 if (key ==
"ai_provider")
1192 else if (key ==
"ai_model")
1194 else if (key ==
"ollama_host")
1196 else if (key ==
"gemini_api_key")
1198 else if (key ==
"custom_system_prompt")
1200 else if (key ==
"use_custom_prompt")
1202 else if (key ==
"show_reasoning")
1204 else if (key ==
"verbose")
1206 else if (key ==
"max_tool_iterations")
1208 else if (key ==
"max_retry_attempts")
1210 else if (key ==
"temperature")
1212 else if (key ==
"top_p")
1214 else if (key ==
"max_output_tokens")
1216 else if (key ==
"stream_responses")
1218 else if (key ==
"favorite_models")
1220 else if (key ==
"model_chain")
1222 else if (key ==
"chain_mode")
1224 else if (key ==
"enable_tool_resources")
1226 else if (key ==
"enable_tool_dungeon")
1228 else if (key ==
"enable_tool_overworld")
1230 else if (key ==
"enable_tool_messages")
1232 else if (key ==
"enable_tool_dialogue")
1234 else if (key ==
"enable_tool_gui")
1236 else if (key ==
"enable_tool_music")
1238 else if (key ==
"enable_tool_sprite")
1240 else if (key ==
"enable_tool_emulator")
1242 else if (key ==
"enable_tool_memory_inspector")
1244 else if (key ==
"builder_blueprint_path")
1246 }
else if (current_section ==
"build") {
1247 if (key ==
"build_script")
1249 else if (key ==
"output_folder")
1251 else if (key ==
"git_repository")
1253 else if (key ==
"track_changes")
1255 else if (key ==
"build_configurations")
1257 else if (key ==
"build_target")
1259 else if (key ==
"asm_entry_point")
1261 else if (key ==
"asm_sources")
1263 else if (key ==
"last_build_hash")
1265 else if (key ==
"build_number")
1267 }
else if (current_section.rfind(
"labels_", 0) == 0) {
1268 std::string label_type = current_section.substr(7);
1270 }
else if (current_section ==
"keybindings") {
1272 }
else if (current_section ==
"editor_visibility") {
1274 }
else if (current_section ==
"zscream_compatibility") {
1275 if (key ==
"original_project_file")
1279 }
else if (current_section ==
"music") {
1280 if (key ==
"persist_custom_music")
1282 else if (key ==
"storage_key")
1284 else if (key ==
"last_saved_at")
1299 return absl::OkStatus();
1303#ifdef __EMSCRIPTEN__
1305 auto storage_or = platform::WasmStorage::LoadProject(storage_key);
1306 if (storage_or.ok()) {
1311 std::ifstream file(project_path);
1312 if (!file.is_open()) {
1313 return absl::InvalidArgumentError(
1314 absl::StrFormat(
"Cannot open project file: %s", project_path));
1317 std::stringstream buffer;
1318 buffer << file.rdbuf();
1325 auto now = std::chrono::system_clock::now();
1326 auto time_t = std::chrono::system_clock::to_time_t(now);
1327 std::stringstream ss;
1328 ss << std::put_time(std::localtime(&time_t),
"%Y-%m-%d %H:%M:%S");
1340#ifdef __EMSCRIPTEN__
1341 auto storage_status = platform::WasmStorage::SaveProject(
1343 if (!storage_status.ok()) {
1344 return storage_status;
1353 return absl::OkStatus();
1357 const std::string& zscream_project_path) {
1363 std::filesystem::path zs_path(zscream_project_path);
1364 name = zs_path.stem().string() +
"_imported";
1376 return absl::OkStatus();
1381 std::ofstream file(target_path);
1382 if (!file.is_open()) {
1383 return absl::InvalidArgumentError(
1384 absl::StrFormat(
"Cannot create ZScream project file: %s", target_path));
1388 file <<
"# ZScream Compatible Project File\n";
1390 file <<
"name=" <<
name <<
"\n";
1398 return absl::OkStatus();
1418 std::vector<std::string> errors;
1421 errors.push_back(
"Project name is required");
1423 errors.push_back(
"Project file path is required");
1425 errors.push_back(
"ROM file is required");
1427#ifndef __EMSCRIPTEN__
1431 errors.push_back(
"ROM file does not exist: " +
rom_filename);
1436 errors.push_back(
"Code folder does not exist: " +
code_folder);
1446 errors.push_back(
"Hack manifest file does not exist: " +
1449 errors.push_back(
"Hack manifest file failed to load: " +
1455 if (!errors.empty()) {
1456 return absl::InvalidArgumentError(absl::StrJoin(errors,
"; "));
1459 return absl::OkStatus();
1463 std::vector<std::string> missing;
1465#ifndef __EMSCRIPTEN__
1488#ifdef __EMSCRIPTEN__
1490 return absl::OkStatus();
1497 for (
const auto& folder : folders) {
1498 if (!folder.empty()) {
1500 if (!std::filesystem::exists(abs_path)) {
1501 std::filesystem::create_directories(abs_path);
1509 if (!std::filesystem::exists(abs_labels)) {
1510 std::ofstream labels_file(abs_labels);
1511 labels_file <<
"# yaze Resource Labels\n";
1512 labels_file <<
"# Format: [type] key=value\n\n";
1513 labels_file.close();
1517 return absl::OkStatus();
1525 return name.empty() ?
"Untitled Project" :
name;
1529 const std::string& absolute_path)
const {
1530 if (absolute_path.empty() ||
filepath.empty())
1531 return absolute_path;
1533 std::filesystem::path project_dir =
1534 std::filesystem::path(
filepath).parent_path();
1535 std::filesystem::path abs_path(absolute_path);
1538 std::filesystem::path relative =
1539 std::filesystem::relative(abs_path.lexically_normal(), project_dir);
1541 return relative.generic_string();
1544 return abs_path.lexically_normal().generic_string();
1549 const std::string& relative_path)
const {
1550 if (relative_path.empty() ||
filepath.empty())
1551 return relative_path;
1553 std::filesystem::path project_dir =
1554 std::filesystem::path(
filepath).parent_path();
1555 std::filesystem::path abs_path(relative_path);
1556 if (abs_path.is_absolute()) {
1557 abs_path = abs_path.lexically_normal();
1558 abs_path.make_preferred();
1559 return abs_path.string();
1561 abs_path = (project_dir / abs_path).lexically_normal();
1562 abs_path.make_preferred();
1564 return abs_path.string();
1568#ifdef __EMSCRIPTEN__
1576 auto normalize = [
this](std::string* path) {
1577 if (!path || path->empty()) {
1595 if (!rom_path.empty()) {
1606 const std::string& project_path) {
1610 std::filesystem::path zs_path(project_path);
1611 name = zs_path.stem().string() +
"_imported";
1616 return absl::OkStatus();
1628 absl::string_view artifact_name)
const {
1629 std::filesystem::path base_dir;
1637 base_dir = std::filesystem::path(
filepath).parent_path();
1640 if (base_dir.empty()) {
1641 return std::string(artifact_name);
1643 return (base_dir / std::string(artifact_name)).lexically_normal().string();
1649#if defined(YAZE_WITH_Z3DK) && __has_include("z3dk_core/config.h")
1650 std::vector<std::filesystem::path> candidates;
1651 auto add_candidate = [&candidates](
const std::filesystem::path& candidate) {
1652 if (candidate.empty()) {
1655 auto normalized = candidate.lexically_normal();
1656 if (std::find(candidates.begin(), candidates.end(), normalized) ==
1658 candidates.push_back(normalized);
1664 if (!std::filesystem::is_directory(code_path)) {
1665 code_path = code_path.parent_path();
1667 add_candidate(code_path /
"z3dk.toml");
1676 add_candidate(std::filesystem::path(
filepath).parent_path() /
"z3dk.toml");
1679 for (
const auto& candidate : candidates) {
1680 if (!std::filesystem::exists(candidate)) {
1685 z3dk::Config config = z3dk::LoadConfigFile(candidate.string(), &error);
1686 if (!error.empty()) {
1687 LOG_WARN(
"Project",
"Failed to parse z3dk config '%s': %s",
1688 candidate.string().c_str(), error.c_str());
1692 const std::filesystem::path base_dir = candidate.parent_path();
1695 if (config.preset.has_value()) {
1700 for (
const auto& include_path : config.include_paths) {
1702 ResolveOptionalPath(base_dir, include_path));
1706 for (
const auto& define : config.defines) {
1711 for (
const auto& main_file : config.main_files) {
1713 ResolveOptionalPath(base_dir, main_file));
1716 if (config.std_includes_path.has_value()) {
1718 ResolveOptionalPath(base_dir, *config.std_includes_path);
1720 if (config.std_defines_path.has_value()) {
1722 ResolveOptionalPath(base_dir, *config.std_defines_path);
1724 if (config.mapper.has_value()) {
1727 if (config.rom_size.has_value()) {
1730 if (config.symbols_format.has_value()) {
1734 if (config.lsp_log_path.has_value()) {
1736 ResolveOptionalPath(base_dir, *config.lsp_log_path);
1740 for (
const auto& emit_path : config.emits) {
1744 for (
const auto& range : config.prohibited_memory_ranges) {
1746 {.start = range.start, .end = range.end, .reason = range.reason});
1750 config.warn_unused_symbols.value_or(
true);
1752 config.warn_branch_outside_bank.value_or(
true);
1756 config.warn_unauthorized_hook.value_or(
true);
1760 if (config.rom_path.has_value()) {
1763 if (config.symbols_path.has_value()) {
1765 ResolveOptionalPath(base_dir, *config.symbols_path);
1769 const std::string basename = BasenameLower(emit_path);
1770 if (basename.ends_with(
".mlb") &&
1773 }
else if (basename ==
"sourcemap.json") {
1775 }
else if (basename ==
"annotations.json") {
1777 }
else if (basename ==
"hooks.json") {
1779 }
else if (basename ==
"lint.json") {
1810 "Loaded z3dk config from %s (%zu include paths, %zu defines)",
1819#ifdef __EMSCRIPTEN__
1829 std::filesystem::path loaded_manifest_path;
1830 auto load_manifest = [&](
const std::filesystem::path& candidate,
1831 bool update_project_setting) ->
bool {
1832 if (candidate.empty() || !std::filesystem::exists(candidate)) {
1837 LOG_WARN(
"Project",
"Failed to load hack manifest %s: %s",
1838 candidate.string().c_str(),
1839 std::string(status.message()).c_str());
1842 loaded_manifest_path = candidate;
1843 if (update_project_setting) {
1846 LOG_DEBUG(
"Project",
"Loaded hack manifest: %s",
1847 candidate.string().c_str());
1856 if (has_explicit_manifest) {
1864 auto candidate = std::filesystem::path(code_path) /
"hack_manifest.json";
1865 (void)load_manifest(candidate,
true);
1870 const std::filesystem::path project_dir =
1871 std::filesystem::path(
filepath).parent_path();
1872 (void)load_manifest(project_dir /
"hack_manifest.json",
true);
1874 (void)load_manifest(project_dir.parent_path() /
"hack_manifest.json",
1883 auto try_load_registry = [&](
const std::filesystem::path& base) ->
bool {
1887 const auto planning = base /
"Docs" /
"Dev" /
"Planning";
1888 if (!std::filesystem::exists(planning)) {
1893 LOG_WARN(
"Project",
"Failed to load project registry from %s: %s",
1894 base.string().c_str(), std::string(status.message()).c_str());
1900 bool registry_loaded =
false;
1909 if (!registry_loaded && !loaded_manifest_path.empty()) {
1910 registry_loaded = try_load_registry(loaded_manifest_path.parent_path());
1914 if (!registry_loaded && !
filepath.empty()) {
1916 try_load_registry(std::filesystem::path(
filepath).parent_path());
1919 if (!registry_loaded) {
1924 "Hack manifest loaded but project registry was not found "
1925 "(code_folder='%s', manifest='%s')",
1926 code_folder.c_str(), loaded_manifest_path.string().c_str());
1931 size_t injected = 0;
1932 for (
const auto& [type_key, labels] :
1934 for (
const auto& [id_str, label] : labels) {
1939 LOG_DEBUG(
"Project",
"Loaded project registry: %zu resource labels injected",
1981 for (uint16_t tile = 0xB0; tile <= 0xBE; ++tile) {
2013 auto now = std::chrono::system_clock::now().time_since_epoch();
2015 std::chrono::duration_cast<std::chrono::milliseconds>(now).count();
2016 return absl::StrFormat(
"yaze_project_%lld", timestamp);
2020std::vector<ProjectManager::ProjectTemplate>
2022 std::vector<ProjectTemplate> templates;
2031 t.
name =
"Vanilla ROM Hack";
2033 "Standard ROM editing without custom ASM. Limited to vanilla features.";
2042 templates.push_back(t);
2048 t.
name =
"ZSCustomOverworld v2";
2050 "Basic overworld expansion: custom BG colors, main palettes, parent "
2063 templates.push_back(t);
2069 t.
name =
"ZSCustomOverworld v3 (Recommended)";
2071 "Full overworld expansion: wide/tall areas, animated GFX, overlays, "
2088 templates.push_back(t);
2094 t.
name =
"Randomizer Compatible";
2096 "Compatible with ALttP Randomizer. Minimal custom features to avoid "
2105 templates.push_back(t);
2115 t.
name =
"Dungeon Designer";
2116 t.
description =
"Focused on dungeon creation and modification.";
2123 templates.push_back(t);
2129 t.
name =
"Graphics Pack";
2131 "Project focused on graphics, sprites, and visual modifications.";
2140 templates.push_back(t);
2146 t.
name =
"Complete Overhaul";
2147 t.
description =
"Full-scale ROM hack with all features enabled.";
2162 templates.push_back(t);
2169 const std::string& template_name,
const std::string& project_name,
2170 const std::string& base_path) {
2172 auto status = project.
Create(project_name, base_path);
2178 if (template_name ==
"Full Overworld Mod") {
2182 project.
metadata.
tags = {
"overworld",
"maps",
"graphics"};
2183 }
else if (template_name ==
"Dungeon Designer") {
2187 project.
metadata.
tags = {
"dungeons",
"rooms",
"design"};
2188 }
else if (template_name ==
"Graphics Pack") {
2192 project.
metadata.
tags = {
"graphics",
"sprites",
"palettes"};
2193 }
else if (template_name ==
"Complete Overhaul") {
2199 project.
metadata.
tags = {
"complete",
"overhaul",
"full-mod"};
2202 status = project.
Save();
2211 const std::string& directory) {
2212#ifdef __EMSCRIPTEN__
2216 std::vector<std::string> projects;
2219 for (
const auto& entry : std::filesystem::directory_iterator(directory)) {
2220 if (entry.is_regular_file()) {
2221 std::string filename = entry.path().filename().string();
2222 if (filename.ends_with(
".yaze") || filename.ends_with(
".zsproj")) {
2223 projects.push_back(entry.path().string());
2225 }
else if (entry.is_directory()) {
2226 std::string filename = entry.path().filename().string();
2227 if (filename.ends_with(
".yazeproj")) {
2228 projects.push_back(entry.path().string());
2232 }
catch (
const std::filesystem::filesystem_error& e) {
2241#ifdef __EMSCRIPTEN__
2243 return absl::UnimplementedError(
2244 "Project backups are not supported in the web build");
2247 return absl::InvalidArgumentError(
"Project has no file path");
2250 std::filesystem::path project_path(project.
filepath);
2251 std::filesystem::path backup_dir = project_path.parent_path() /
"backups";
2252 std::filesystem::create_directories(backup_dir);
2254 auto now = std::chrono::system_clock::now();
2255 auto time_t = std::chrono::system_clock::to_time_t(now);
2256 std::stringstream ss;
2257 ss << std::put_time(std::localtime(&time_t),
"%Y%m%d_%H%M%S");
2259 std::string backup_filename = project.
name +
"_backup_" + ss.str() +
".yaze";
2260 std::filesystem::path backup_path = backup_dir / backup_filename;
2263 std::filesystem::copy_file(project.
filepath, backup_path);
2264 }
catch (
const std::filesystem::filesystem_error& e) {
2265 return absl::InternalError(
2266 absl::StrFormat(
"Failed to backup project: %s", e.what()));
2269 return absl::OkStatus();
2280 std::vector<std::string> recommendations;
2283 recommendations.push_back(
"Add a ROM file to begin editing");
2287 recommendations.push_back(
"Set up a code folder for assembly patches");
2291 recommendations.push_back(
"Create a labels file for better organization");
2295 recommendations.push_back(
"Add a project description for documentation");
2299 recommendations.push_back(
2300 "Consider setting up version control for your project");
2304 if (!missing_files.empty()) {
2305 recommendations.push_back(
2306 "Some project files are missing - use Project > Repair to fix");
2309 return recommendations;
2315 std::ifstream file(filename);
2316 if (!file.is_open()) {
2323 std::string current_type =
"";
2325 while (std::getline(file, line)) {
2326 if (line.empty() || line[0] ==
'#')
2330 if (line[0] ==
'[' && line.back() ==
']') {
2331 current_type = line.substr(1, line.length() - 2);
2336 size_t eq_pos = line.find(
'=');
2337 if (eq_pos != std::string::npos && !current_type.empty()) {
2338 std::string key = line.substr(0, eq_pos);
2339 std::string value = line.substr(eq_pos + 1);
2340 labels_[current_type][key] = value;
2354 if (!file.is_open())
2357 file <<
"# yaze Resource Labels\n";
2358 file <<
"# Format: [type] followed by key=value pairs\n\n";
2360 for (
const auto& [type, type_labels] :
labels_) {
2361 if (!type_labels.empty()) {
2362 file <<
"[" << type <<
"]\n";
2363 for (
const auto& [key, value] : type_labels) {
2364 file << key <<
"=" << value <<
"\n";
2375 if (!p_open || !*p_open)
2379 if (ImGui::Begin(
"Resource Labels", p_open)) {
2380 ImGui::Text(
"Resource Labels Manager");
2382 ImGui::Text(
"Total types: %zu",
labels_.size());
2384 for (
const auto& [type, type_labels] :
labels_) {
2385 if (ImGui::TreeNode(type.c_str())) {
2386 ImGui::Text(
"Labels: %zu", type_labels.size());
2387 for (
const auto& [key, value] : type_labels) {
2388 ImGui::Text(
"%s = %s", key.c_str(), value.c_str());
2398 const std::string& key,
2399 const std::string& newValue) {
2400 labels_[type][key] = newValue;
2404 bool selected,
const std::string& type,
const std::string& key,
2405 const std::string& defaultValue) {
2407 if (ImGui::Selectable(
2408 absl::StrFormat(
"%s: %s", key.c_str(),
GetLabel(type, key).c_str())
2416 const std::string& key) {
2417 auto type_it =
labels_.find(type);
2421 auto label_it = type_it->second.find(key);
2422 if (label_it == type_it->second.end())
2425 return label_it->second;
2429 const std::string& type,
const std::string& key,
2430 const std::string& defaultValue) {
2431 auto existing =
GetLabel(type, key);
2432 if (!existing.empty())
2435 labels_[type][key] = defaultValue;
2436 return defaultValue;
2444 const std::unordered_map<
2445 std::string, std::unordered_map<std::string, std::string>>& labels) {
2474 LOG_DEBUG(
"Project",
"Initialized embedded labels:");
2476 LOG_DEBUG(
"Project",
" - %d entrance names",
2478 LOG_DEBUG(
"Project",
" - %d sprite names",
2480 LOG_DEBUG(
"Project",
" - %d overlord names",
2483 LOG_DEBUG(
"Project",
" - %d music names",
2485 LOG_DEBUG(
"Project",
" - %d graphics names",
2487 LOG_DEBUG(
"Project",
" - %d room effect names",
2489 LOG_DEBUG(
"Project",
" - %d room tag names",
2491 LOG_DEBUG(
"Project",
" - %d tile type names",
2494 return absl::OkStatus();
2495 }
catch (
const std::exception& e) {
2496 return absl::InternalError(
2497 absl::StrCat(
"Failed to initialize embedded labels: ", e.what()));
2502 const std::string& default_value)
const {
2506 auto label_it = type_it->second.find(std::to_string(
id));
2507 if (label_it != type_it->second.end()) {
2508 return label_it->second;
2512 return default_value.empty() ? resource_type +
"_" + std::to_string(
id)
2517#ifdef __EMSCRIPTEN__
2519 return absl::UnimplementedError(
2520 "File-based label import is not supported in the web build");
2523 if (!file.is_open()) {
2524 return absl::InvalidArgumentError(
2525 absl::StrFormat(
"Cannot open labels file: %s",
filepath));
2528 std::stringstream buffer;
2529 buffer << file.rdbuf();
2537 const std::string& content) {
2544 auto status = provider.ImportFromZScreamFormat(content);
2549 LOG_DEBUG(
"Project",
"Imported ZScream labels:");
2550 LOG_DEBUG(
"Project",
" - %d sprite labels",
2554 LOG_DEBUG(
"Project",
" - %d room tag labels",
2557 return absl::OkStatus();
2566 LOG_DEBUG(
"Project",
"Initialized ResourceLabelProvider with project labels");
2567 LOG_DEBUG(
"Project",
" - prefer_hmagic_names: %s",
2569 LOG_DEBUG(
"Project",
" - hack_manifest: %s",
2577#ifdef YAZE_ENABLE_JSON_PROJECT_FORMAT
2579absl::Status YazeProject::LoadFromJsonFormat(
const std::string& project_path) {
2580#ifdef __EMSCRIPTEN__
2581 return absl::UnimplementedError(
2582 "JSON project format loading is not supported in the web build");
2584 std::ifstream file(project_path);
2585 if (!file.is_open()) {
2586 return absl::InvalidArgumentError(
2587 absl::StrFormat(
"Cannot open JSON project file: %s", project_path));
2595 if (j.contains(
"yaze_project")) {
2596 auto& proj = j[
"yaze_project"];
2598 if (proj.contains(
"name"))
2599 name = proj[
"name"].get<std::string>();
2600 if (proj.contains(
"description"))
2602 if (proj.contains(
"author"))
2604 if (proj.contains(
"version"))
2606 if (proj.contains(
"created"))
2608 if (proj.contains(
"modified"))
2610 if (proj.contains(
"created_by"))
2614 if (proj.contains(
"rom_filename"))
2615 rom_filename = proj[
"rom_filename"].get<std::string>();
2616 if (proj.contains(
"rom_backup_folder"))
2618 if (proj.contains(
"code_folder"))
2619 code_folder = proj[
"code_folder"].get<std::string>();
2620 if (proj.contains(
"assets_folder"))
2622 if (proj.contains(
"patches_folder"))
2624 if (proj.contains(
"labels_filename"))
2626 if (proj.contains(
"symbols_filename"))
2628 if (proj.contains(
"hack_manifest_file"))
2631 if (proj.contains(
"rom") && proj[
"rom"].is_object()) {
2632 auto& rom = proj[
"rom"];
2633 if (rom.contains(
"role"))
2635 if (rom.contains(
"expected_hash"))
2637 if (rom.contains(
"write_policy"))
2643 if (proj.contains(
"use_embedded_labels")) {
2648 if (proj.contains(
"feature_flags")) {
2649 auto& flags = proj[
"feature_flags"];
2652 if (flags.contains(
"kSaveDungeonMaps"))
2654 flags[
"kSaveDungeonMaps"].get<
bool>();
2655 if (flags.contains(
"kSaveOverworldMaps"))
2657 flags[
"kSaveOverworldMaps"].get<
bool>();
2658 if (flags.contains(
"kSaveOverworldEntrances"))
2660 flags[
"kSaveOverworldEntrances"].get<
bool>();
2661 if (flags.contains(
"kSaveOverworldExits"))
2663 flags[
"kSaveOverworldExits"].get<
bool>();
2664 if (flags.contains(
"kSaveOverworldItems"))
2666 flags[
"kSaveOverworldItems"].get<
bool>();
2667 if (flags.contains(
"kSaveOverworldProperties"))
2669 flags[
"kSaveOverworldProperties"].get<
bool>();
2670 if (flags.contains(
"kSaveDungeonObjects"))
2672 flags[
"kSaveDungeonObjects"].get<
bool>();
2673 if (flags.contains(
"kSaveDungeonSprites"))
2675 flags[
"kSaveDungeonSprites"].get<
bool>();
2676 if (flags.contains(
"kSaveDungeonRoomHeaders"))
2678 flags[
"kSaveDungeonRoomHeaders"].get<
bool>();
2679 if (flags.contains(
"kSaveDungeonTorches"))
2681 flags[
"kSaveDungeonTorches"].get<
bool>();
2682 if (flags.contains(
"kSaveDungeonPits"))
2684 flags[
"kSaveDungeonPits"].get<
bool>();
2685 if (flags.contains(
"kSaveDungeonBlocks"))
2687 flags[
"kSaveDungeonBlocks"].get<
bool>();
2688 if (flags.contains(
"kSaveDungeonCollision"))
2690 flags[
"kSaveDungeonCollision"].get<
bool>();
2691 if (flags.contains(
"kSaveDungeonWaterFillZones"))
2693 flags[
"kSaveDungeonWaterFillZones"].get<
bool>();
2694 if (flags.contains(
"kSaveDungeonChests"))
2696 flags[
"kSaveDungeonChests"].get<
bool>();
2697 if (flags.contains(
"kSaveDungeonPotItems"))
2699 flags[
"kSaveDungeonPotItems"].get<
bool>();
2700 if (flags.contains(
"kSaveDungeonEntrances"))
2702 flags[
"kSaveDungeonEntrances"].get<
bool>();
2703 if (flags.contains(
"kSaveDungeonPalettes"))
2705 flags[
"kSaveDungeonPalettes"].get<
bool>();
2706 if (flags.contains(
"kSaveGraphicsSheet"))
2708 flags[
"kSaveGraphicsSheet"].get<
bool>();
2709 if (flags.contains(
"kSaveAllPalettes"))
2711 flags[
"kSaveAllPalettes"].get<
bool>();
2712 if (flags.contains(
"kSaveGfxGroups"))
2714 if (flags.contains(
"kSaveMessages"))
2719 if (proj.contains(
"workspace_settings")) {
2720 auto& ws = proj[
"workspace_settings"];
2721 if (ws.contains(
"auto_save_enabled"))
2723 ws[
"auto_save_enabled"].get<
bool>();
2724 if (ws.contains(
"auto_save_interval"))
2726 ws[
"auto_save_interval"].get<
float>();
2727 if (ws.contains(
"backup_on_save"))
2729 if (ws.contains(
"backup_retention_count"))
2731 ws[
"backup_retention_count"].get<
int>();
2732 if (ws.contains(
"backup_keep_daily"))
2734 ws[
"backup_keep_daily"].get<
bool>();
2735 if (ws.contains(
"backup_keep_daily_days"))
2737 ws[
"backup_keep_daily_days"].get<
int>();
2740 if (proj.contains(
"rom_addresses") && proj[
"rom_addresses"].is_object()) {
2742 for (
auto it = proj[
"rom_addresses"].begin();
2743 it != proj[
"rom_addresses"].end(); ++it) {
2744 if (it.value().is_number_unsigned()) {
2746 it.value().get<uint32_t>();
2747 }
else if (it.value().is_string()) {
2749 if (parsed.has_value()) {
2756 if (proj.contains(
"custom_objects") &&
2757 proj[
"custom_objects"].is_object()) {
2759 for (
auto it = proj[
"custom_objects"].begin();
2760 it != proj[
"custom_objects"].end(); ++it) {
2761 if (!it.value().is_array())
2764 if (!parsed.has_value()) {
2767 std::vector<std::string> files;
2768 for (
const auto& entry : it.value()) {
2769 if (entry.is_string()) {
2770 files.push_back(entry.get<std::string>());
2773 if (!files.empty()) {
2779 if (proj.contains(
"agent_settings") &&
2780 proj[
"agent_settings"].is_object()) {
2781 auto& agent = proj[
"agent_settings"];
2808 if (agent.contains(
"favorite_models") &&
2809 agent[
"favorite_models"].is_array()) {
2811 for (
const auto& model : agent[
"favorite_models"]) {
2812 if (model.is_string())
2814 model.get<std::string>());
2817 if (agent.contains(
"model_chain") && agent[
"model_chain"].is_array()) {
2819 for (
const auto& model : agent[
"model_chain"]) {
2820 if (model.is_string())
2845 agent.value(
"enable_tool_memory_inspector",
2852 if (proj.contains(
"build_script"))
2853 build_script = proj[
"build_script"].get<std::string>();
2854 if (proj.contains(
"output_folder"))
2856 if (proj.contains(
"git_repository"))
2858 if (proj.contains(
"track_changes"))
2862 return absl::OkStatus();
2863 }
catch (
const json::exception& e) {
2864 return absl::InvalidArgumentError(
2865 absl::StrFormat(
"JSON parse error: %s", e.what()));
2869absl::Status YazeProject::SaveToJsonFormat() {
2870#ifdef __EMSCRIPTEN__
2871 return absl::UnimplementedError(
2872 "JSON project format saving is not supported in the web build");
2875 auto& proj = j[
"yaze_project"];
2879 proj[
"name"] =
name;
2899 proj[
"rom"][
"write_policy"] =
2908 proj[
"feature_flags"][
"kSaveOverworldMaps"] =
2910 proj[
"feature_flags"][
"kSaveOverworldEntrances"] =
2912 proj[
"feature_flags"][
"kSaveOverworldExits"] =
2914 proj[
"feature_flags"][
"kSaveOverworldItems"] =
2916 proj[
"feature_flags"][
"kSaveOverworldProperties"] =
2918 proj[
"feature_flags"][
"kSaveDungeonObjects"] =
2920 proj[
"feature_flags"][
"kSaveDungeonSprites"] =
2922 proj[
"feature_flags"][
"kSaveDungeonRoomHeaders"] =
2924 proj[
"feature_flags"][
"kSaveDungeonTorches"] =
2927 proj[
"feature_flags"][
"kSaveDungeonBlocks"] =
2929 proj[
"feature_flags"][
"kSaveDungeonCollision"] =
2931 proj[
"feature_flags"][
"kSaveDungeonWaterFillZones"] =
2933 proj[
"feature_flags"][
"kSaveDungeonChests"] =
2935 proj[
"feature_flags"][
"kSaveDungeonPotItems"] =
2937 proj[
"feature_flags"][
"kSaveDungeonEntrances"] =
2939 proj[
"feature_flags"][
"kSaveDungeonPalettes"] =
2941 proj[
"feature_flags"][
"kSaveGraphicsSheet"] =
2948 proj[
"workspace_settings"][
"auto_save_enabled"] =
2950 proj[
"workspace_settings"][
"auto_save_interval"] =
2952 proj[
"workspace_settings"][
"backup_on_save"] =
2954 proj[
"workspace_settings"][
"backup_retention_count"] =
2956 proj[
"workspace_settings"][
"backup_keep_daily"] =
2958 proj[
"workspace_settings"][
"backup_keep_daily_days"] =
2961 auto& agent = proj[
"agent_settings"];
2988 agent[
"enable_tool_memory_inspector"] =
2993 auto& addrs = proj[
"rom_addresses"];
3000 auto& objs = proj[
"custom_objects"];
3002 objs[absl::StrFormat(
"0x%X", object_id)] = files;
3013 if (!file.is_open()) {
3014 return absl::InvalidArgumentError(
3015 absl::StrFormat(
"Cannot write JSON project file: %s",
filepath));
3019 return absl::OkStatus();
3027 if (!config_dir.ok()) {
3034#ifdef __EMSCRIPTEN__
3035 auto status = platform::WasmStorage::SaveProject(
3038 LOG_WARN(
"RecentFilesManager",
"Could not persist recent files: %s",
3039 status.ToString().c_str());
3045 if (!config_dir_status.ok()) {
3046 LOG_ERROR(
"Project",
"Failed to get or create config directory: %s",
3047 config_dir_status.status().ToString().c_str());
3052 std::ofstream file(filepath);
3053 if (!file.is_open()) {
3054 LOG_WARN(
"RecentFilesManager",
"Could not save recent files to %s",
3060 file << file_path << std::endl;
3065#ifdef __EMSCRIPTEN__
3067 if (!storage_or.ok()) {
3071 std::istringstream stream(storage_or.value());
3073 while (std::getline(stream, line)) {
3074 if (!line.empty()) {
3082 std::ifstream file(filepath);
3083 if (!file.is_open()) {
3090 while (std::getline(file, line)) {
3091 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)
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)
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