16#include "absl/strings/match.h"
17#include "absl/strings/str_format.h"
18#include "absl/strings/str_join.h"
19#include "absl/strings/str_split.h"
20#include "absl/strings/strip.h"
22#include "imgui/imgui.h"
28#include "yaze_config.h"
31#if defined(YAZE_WITH_Z3DK) && __has_include("z3dk_core/config.h")
32#include "z3dk_core/config.h"
54 value.begin(), value.end(), value.begin(),
55 [](
unsigned char c) { return static_cast<char>(std::tolower(c)); });
60std::pair<std::string, std::string> ParseKeyValue(
const std::string& line) {
61 size_t eq_pos = line.find(
'=');
62 if (eq_pos == std::string::npos)
65 std::string key = line.substr(0, eq_pos);
66 std::string value = line.substr(eq_pos + 1);
69 key.erase(0, key.find_first_not_of(
" \t"));
70 key.erase(key.find_last_not_of(
" \t") + 1);
71 value.erase(0, value.find_first_not_of(
" \t"));
72 value.erase(value.find_last_not_of(
" \t") + 1);
78 return value ==
"true" || value ==
"1" || value ==
"yes";
83 return std::stof(value);
90 std::vector<std::string> result;
94 std::vector<std::string> parts = absl::StrSplit(value,
',');
95 for (
const auto& part : parts) {
96 std::string trimmed = std::string(part);
97 trimmed.erase(0, trimmed.find_first_not_of(
" \t"));
98 trimmed.erase(trimmed.find_last_not_of(
" \t") + 1);
99 if (!trimmed.empty()) {
100 result.push_back(trimmed);
107 std::vector<std::string> result;
111 std::vector<std::string> parts = absl::StrSplit(value,
',');
112 result.reserve(parts.size());
113 for (
const auto& part : parts) {
114 std::string trimmed = part;
115 trimmed.erase(0, trimmed.find_first_not_of(
" \t"));
116 trimmed.erase(trimmed.find_last_not_of(
" \t") + 1);
117 result.push_back(std::move(trimmed));
123 std::vector<uint16_t> result;
129 result.reserve(parts.size());
130 for (
const auto& part : parts) {
131 std::string token = part;
132 if (token.rfind(
"0x", 0) == 0 || token.rfind(
"0X", 0) == 0) {
133 token = token.substr(2);
135 result.push_back(
static_cast<uint16_t
>(std::stoul(token,
nullptr, 16)));
141 result.push_back(
static_cast<uint16_t
>(std::stoul(token,
nullptr, 10)));
154 std::string token = value;
155 if (token.rfind(
"0x", 0) == 0 || token.rfind(
"0X", 0) == 0) {
156 token = token.substr(2);
158 return static_cast<uint32_t
>(std::stoul(token,
nullptr, 16));
164 return static_cast<uint32_t
>(std::stoul(token,
nullptr, 10));
171 return absl::StrJoin(values,
",", [](std::string* out, uint16_t value) {
172 out->append(absl::StrFormat(
"0x%02X", value));
183 std::unordered_map<std::string,
184 std::unordered_map<std::string, std::string>>& labels) {
185 auto all_of = [](absl::string_view text,
auto predicate) {
186 return !text.empty() && std::all_of(text.begin(), text.end(), predicate);
188 auto is_decimal = [](
char c) {
189 return c >=
'0' && c <=
'9';
191 auto is_hex = [](
char c) {
192 return std::isxdigit(
static_cast<unsigned char>(c)) != 0;
194 for (
const char* type : {
"blockset",
"roomset",
"spriteset",
"paletteset"}) {
195 auto found = labels.find(type);
196 if (found == labels.end()) {
199 std::unordered_map<std::string, std::string> migrated;
201 for (
int pass = 0; pass < 3; ++pass) {
202 for (
const auto& [key, value] : found->second) {
203 absl::string_view digits = key;
204 const bool prefixed = absl::ConsumePrefix(&digits,
"0x") ||
205 absl::ConsumePrefix(&digits,
"0X");
206 std::string canonical;
207 if (pass == 0 && !prefixed && all_of(digits, is_decimal)) {
208 canonical = std::string(digits);
209 }
else if (pass == 1 && prefixed && all_of(digits, is_decimal)) {
211 std::to_string(std::stoul(std::string(digits),
nullptr, 10));
212 }
else if (pass == 2 && !all_of(digits, is_decimal)) {
215 prefixed && all_of(digits, is_hex)
216 ? std::to_string(std::stoul(std::string(digits),
nullptr, 16))
219 if (!canonical.empty()) {
220 migrated.emplace(canonical, value);
224 found->second = std::move(migrated);
230 const std::string& value) {
231 std::map<uint16_t, std::vector<uint16_t>> result;
232 for (absl::string_view entry : absl::StrSplit(value,
';')) {
233 const size_t colon = entry.find(
':');
234 if (colon == absl::string_view::npos) {
238 if (keys.size() != 1) {
241 result[keys.front()] =
248 const std::map<uint16_t, std::vector<uint16_t>>& values) {
249 return absl::StrJoin(values,
";", [](std::string* out,
const auto& entry) {
250 out->append(absl::StrFormat(
"0x%02X:", entry.first));
251 out->append(absl::StrJoin(entry.second,
","));
256 return absl::StrFormat(
"0x%06X", value);
260 std::string key(input);
261 for (
char& c : key) {
262 if (!std::isalnum(
static_cast<unsigned char>(c))) {
273 auto [key, parsed_value] = ParseKeyValue(value);
277 return {key, parsed_value.empty() ?
"1" : parsed_value};
281 const std::string& value) {
285 std::filesystem::path path(value);
286 if (path.is_absolute()) {
287 return path.lexically_normal().string();
289 return (base_dir / path).lexically_normal().string();
293 return ToLowerCopy(std::filesystem::path(path).filename().
string());
296#ifndef __EMSCRIPTEN__
299 return static_cast<uint64_t
>(::GetCurrentProcessId());
301 return static_cast<uint64_t
>(::getpid());
306 const std::filesystem::path& target_path) {
307 static std::atomic<uint64_t> next_temp_id{0};
308 std::filesystem::path temp_path = target_path;
311 std::to_string(next_temp_id.fetch_add(1, std::memory_order_relaxed));
316 std::error_code remove_error;
317 std::filesystem::remove(temp_path, remove_error);
321 const std::filesystem::path& target_path, absl::string_view contents,
322 bool replace_existing) {
324 std::ofstream file(temp_path, std::ios::binary | std::ios::trunc);
325 if (!file.is_open()) {
326 return absl::InvalidArgumentError(absl::StrFormat(
327 "Cannot create temporary project file: %s", temp_path.string()));
330 file.write(contents.data(),
static_cast<std::streamsize
>(contents.size()));
335 return absl::InternalError(absl::StrFormat(
336 "Failed to write temporary project file: %s", temp_path.string()));
342 return absl::InternalError(absl::StrFormat(
343 "Failed to close temporary project file: %s", temp_path.string()));
346 std::error_code rename_error;
348 const DWORD move_flags =
349 MOVEFILE_WRITE_THROUGH |
350 (replace_existing ? MOVEFILE_REPLACE_EXISTING :
static_cast<DWORD
>(0));
351 if (!::MoveFileExW(temp_path.c_str(), target_path.c_str(), move_flags)) {
352 rename_error = std::error_code(
static_cast<int>(::GetLastError()),
353 std::system_category());
356 if (replace_existing) {
357 std::filesystem::rename(temp_path, target_path, rename_error);
358 }
else if (::link(temp_path.c_str(), target_path.c_str()) != 0) {
359 rename_error = std::error_code(errno, std::generic_category());
366 bool target_already_exists = rename_error == std::errc::file_exists;
368 target_already_exists = target_already_exists ||
369 rename_error.value() == ERROR_FILE_EXISTS ||
370 rename_error.value() == ERROR_ALREADY_EXISTS;
372 if (!replace_existing && target_already_exists) {
373 return absl::AlreadyExistsError(absl::StrFormat(
374 "Project file already exists: %s", target_path.string()));
376 return absl::InternalError(
377 absl::StrFormat(
"Failed to replace project file %s: %s",
378 target_path.string(), rename_error.message()));
381 return absl::OkStatus();
386#ifndef __EMSCRIPTEN__
388 absl::string_view contents,
389 bool replace_existing) {
390 return WriteProjectFileAtomicallyImpl(
391 std::filesystem::path(std::string(target_path)), contents,
411 std::string lower = ToLowerCopy(std::string(value));
412 if (lower ==
"base") {
415 if (lower ==
"patched") {
418 if (lower ==
"release") {
437 std::string lower = ToLowerCopy(std::string(value));
438 if (lower ==
"allow") {
441 if (lower ==
"block") {
451 provider.SetHackManifest(
nullptr);
454 provider.SetProjectLabels(
nullptr);
459 const std::string& base_path) {
461 filepath = base_path +
"/" + project_name +
".yaze";
464 auto now = std::chrono::system_clock::now();
465 auto time_t = std::chrono::system_clock::to_time_t(now);
466 std::stringstream ss;
467 ss << std::put_time(std::localtime(&time_t),
"%Y-%m-%d %H:%M:%S");
478#ifndef __EMSCRIPTEN__
480 std::filesystem::path project_dir(base_path +
"/" + project_name);
481 std::filesystem::create_directories(project_dir);
482 std::filesystem::create_directories(project_dir /
"code");
483 std::filesystem::create_directories(project_dir /
"assets");
484 std::filesystem::create_directories(project_dir /
"patches");
485 std::filesystem::create_directories(project_dir /
"backups");
486 std::filesystem::create_directories(project_dir /
"output");
519 auto current = std::filesystem::path(path).lexically_normal();
521 for (; !current.empty(); current = current.parent_path()) {
522 if (current.extension() ==
".yazeproj") {
524 if (std::filesystem::is_directory(current, ec) && !ec) {
525 return current.string();
529 if (current == current.parent_path()) {
541 std::string resolved_path = project_path;
543 if (!bundle_root.empty() && bundle_root != project_path) {
545 resolved_path = bundle_root;
548#ifndef __EMSCRIPTEN__
553 std::error_code absolute_ec;
554 const auto absolute_path =
555 std::filesystem::absolute(resolved_path, absolute_ec).lexically_normal();
557 resolved_path = absolute_path.string();
566 auto storage_or = platform::WasmStorage::LoadProject(storage_key);
567 if (storage_or.ok()) {
573 absl::Status load_status;
574 if (resolved_path.ends_with(
".yazeproj")) {
577 const std::filesystem::path bundle_path(resolved_path);
579 if (!std::filesystem::exists(bundle_path, ec) || ec ||
580 !std::filesystem::is_directory(bundle_path, ec) || ec) {
581 return absl::InvalidArgumentError(
582 absl::StrFormat(
"Project bundle does not exist: %s", resolved_path));
587 const std::filesystem::path project_file = bundle_path /
"project.yaze";
590 if (!std::filesystem::exists(project_file, ec) || ec) {
593 name = bundle_path.stem().string();
596 auto now = std::chrono::system_clock::now();
597 auto time_t = std::chrono::system_clock::to_time_t(now);
598 std::stringstream ss;
599 ss << std::put_time(std::localtime(&time_t),
"%Y-%m-%d %H:%M:%S");
619 const std::filesystem::path rom_candidate = bundle_path /
"rom";
626 const std::filesystem::path project_dir = bundle_path /
"project";
627 const std::filesystem::path code_dir = bundle_path /
"code";
628 if (std::filesystem::exists(project_dir, ec) &&
629 std::filesystem::is_directory(project_dir, ec) && !ec) {
631 }
else if (std::filesystem::exists(code_dir, ec) &&
632 std::filesystem::is_directory(code_dir, ec) && !ec) {
647 }
else if (resolved_path.ends_with(
".yaze")) {
651 std::ifstream file(resolved_path);
652 if (file.is_open()) {
653 std::stringstream buffer;
654 buffer << file.rdbuf();
655 std::string content = buffer.str();
657#ifdef YAZE_ENABLE_JSON_PROJECT_FORMAT
658 if (!content.empty() && content.front() ==
'{') {
659 LOG_DEBUG(
"Project",
"Detected JSON format project file");
660 load_status = LoadFromJsonFormat(resolved_path);
668 return absl::InvalidArgumentError(
669 absl::StrFormat(
"Cannot open project file: %s", resolved_path));
671 }
else if (resolved_path.ends_with(
".zsproj")) {
675 return absl::InvalidArgumentError(
"Unsupported project file format");
678 if (!load_status.ok()) {
692 return absl::OkStatus();
704 const std::string& project_path) {
705 if (project_path.empty()) {
706 return absl::InvalidArgumentError(
"Project file path cannot be empty");
711#ifndef __EMSCRIPTEN__
713 const auto absolute_path =
714 std::filesystem::absolute(project_path, ec).lexically_normal();
716 ec ? std::filesystem::path(project_path).lexically_normal().string()
717 : absolute_path.string();
724 }
catch (
const std::exception& error) {
725 return absl::InvalidArgumentError(
726 absl::StrFormat(
"Invalid project file value: %s", error.what()));
731 return absl::OkStatus();
735 std::string old_filepath =
filepath;
738 auto status =
Save();
750 }
else if (!
name.empty()) {
753 base = std::filesystem::path(
filepath).stem().string();
755 base = SanitizeStorageKey(base);
756 if (suffix.empty()) {
759 return absl::StrFormat(
"%s_%s", base, suffix);
767 for (
size_t i = 0; i < content.size(); ++i) {
768 if (content[i] ==
'\r' &&
769 (i + 1 >= content.size() || content[i + 1] !=
'\n')) {
780 static constexpr absl::string_view kKnown[] = {
"project",
796 "zscream_compatibility",
798 if (absl::StartsWith(name,
"labels_")) {
801 return std::find(std::begin(kKnown), std::end(kKnown), name) !=
811 if (!before_sections) {
814 return absl::StartsWith(line,
"# yaze Project File") ||
815 absl::StartsWith(line,
"# Format Version:") ||
816 absl::StartsWith(line,
"# Generated by YAZE") ||
817 absl::StartsWith(line,
"# Last Modified:");
822 for (
auto& section : passthrough.
sections) {
823 if (section.name == name) {
827 passthrough.
sections.push_back({name,
false, {}});
832 return !line.empty() && line.front() !=
'#' &&
833 line.find(
'=') != absl::string_view::npos;
840 if (passthrough.
empty()) {
845 struct CanonicalSection {
848 std::vector<std::string> lines;
850 std::vector<std::string> preamble;
851 std::vector<CanonicalSection> canonical_sections;
852 bool has_footer =
false;
853 for (absl::string_view line : absl::StrSplit(canonical,
'\n')) {
858 if (line.size() >= 2 && line.front() ==
'[' && line.back() ==
']') {
859 canonical_sections.push_back(
861 std::string(line.substr(1, line.size() - 2)),
868 (canonical_sections.empty() ? preamble : canonical_sections.back().lines)
869 .push_back(std::string(line));
874 for (
const auto& section : passthrough.
sections) {
875 if (section.name == name) {
882 std::ostringstream out;
885 for (
const auto& line : section.lines) {
886 if (line.kind != Kind::kKnownKey) {
887 out << line.text <<
"\n";
892 for (
const auto& line : preamble) {
896 if (
const auto* before = find_section(
""); before !=
nullptr) {
898 if (!before->lines.empty()) {
903 std::vector<const ProjectFilePassthrough::Section*> merged;
904 for (
const auto& section : canonical_sections) {
905 out << section.header <<
"\n";
906 const auto* kept = find_section(section.name);
907 if (kept ==
nullptr) {
908 for (
const auto& line : section.lines) {
914 merged.push_back(kept);
917 std::vector<std::pair<std::string, std::vector<std::string>>> anchored;
918 std::vector<std::string> pending;
919 std::vector<std::pair<std::string, std::vector<std::string>>> unknown;
920 for (
const auto& line : kept->lines) {
921 if (line.kind == Kind::kComment) {
922 pending.push_back(line.text);
923 }
else if (line.kind == Kind::kUnknownLine) {
924 unknown.push_back({line.text, std::move(pending)});
926 }
else if (!pending.empty()) {
927 anchored.push_back({line.key, std::move(pending)});
931 std::vector<bool> anchor_written(anchored.size(),
false);
932 for (
const auto& line : section.lines) {
934 const std::string key = ParseKeyValue(line).first;
935 for (
size_t i = 0; i < anchored.size(); ++i) {
936 if (!anchor_written[i] && anchored[i].first == key) {
937 for (
const auto& comment : anchored[i].second) {
938 out << comment <<
"\n";
940 anchor_written[i] =
true;
946 for (
const auto& [text, comments] : unknown) {
947 for (
const auto& comment : comments) {
948 out << comment <<
"\n";
953 for (
size_t i = 0; i < anchored.size(); ++i) {
954 if (!anchor_written[i]) {
955 for (
const auto& comment : anchored[i].second) {
956 out << comment <<
"\n";
960 for (
const auto& comment : pending) {
961 out << comment <<
"\n";
968 for (
const auto& section : passthrough.
sections) {
969 if (section.name.empty() ||
970 std::find(merged.begin(), merged.end(), §ion) != merged.end()) {
973 const bool has_content = std::any_of(
974 section.lines.begin(), section.lines.end(),
975 [](
const auto& line) { return line.kind != Kind::kKnownKey; });
979 out <<
"[" << section.name <<
"]\n";
992 return IsKnownProjectSection(name);
996 std::ostringstream file;
999 file <<
"# yaze Project File\n";
1000 file <<
"# Format Version: 2.0\n";
1005 file <<
"[project]\n";
1006 file <<
"name=" <<
name <<
"\n";
1016 file <<
"tags=" << absl::StrJoin(
metadata.
tags,
",") <<
"\n\n";
1019 file <<
"[files]\n";
1028 file <<
"overworld_graphics_preview="
1032 file <<
"dungeon_tile_patterns="
1036 file <<
"cutscene_shots="
1040 file <<
"custom_collision_json="
1046 file <<
"palette_catalog_file="
1048 file <<
"terrain_catalog_file="
1051 file <<
"sprite_catalog_file="
1053 file <<
"sprite_source_root="
1055 file <<
"additional_roms=" << absl::StrJoin(
additional_roms,
",") <<
"\n\n";
1057 file <<
"[sprite_assets]\n";
1073 file <<
"[feature_flags]\n";
1074 file <<
"load_custom_overworld="
1077 file <<
"apply_zs_custom_overworld_asm="
1081 file <<
"save_dungeon_maps="
1083 file <<
"save_overworld_maps="
1086 file <<
"save_overworld_entrances="
1089 file <<
"save_overworld_exits="
1092 file <<
"save_overworld_items="
1095 file <<
"save_overworld_properties="
1098 file <<
"save_dungeon_objects="
1100 file <<
"save_dungeon_sprites="
1102 file <<
"save_dungeon_room_headers="
1104 file <<
"save_dungeon_torches="
1106 file <<
"save_dungeon_pits="
1108 file <<
"save_dungeon_blocks="
1110 file <<
"save_dungeon_collision="
1112 file <<
"save_dungeon_water_fill_zones="
1115 file <<
"save_dungeon_chests="
1117 file <<
"save_dungeon_pot_items="
1119 file <<
"save_dungeon_entrances="
1121 file <<
"save_dungeon_palettes="
1123 file <<
"save_graphics_sheet="
1125 file <<
"save_all_palettes="
1127 file <<
"save_gfx_groups="
1131 file <<
"enable_custom_objects="
1135 file <<
"[workspace]\n";
1140 file <<
"autosave_enabled="
1144 file <<
"backup_on_save="
1148 file <<
"backup_keep_daily="
1154 file <<
"show_collision="
1156 file <<
"prefer_hmagic_names="
1160 file <<
"saved_layouts="
1167 if (track_tiles.empty()) {
1168 for (uint16_t tile = 0xB0; tile <= 0xBE; ++tile) {
1169 track_tiles.push_back(tile);
1173 if (track_stop_tiles.empty()) {
1174 track_stop_tiles = {0xB7, 0xB8, 0xB9, 0xBA};
1177 if (track_switch_tiles.empty()) {
1178 track_switch_tiles = {0xD0, 0xD1, 0xD2, 0xD3};
1181 if (track_object_ids.empty()) {
1182 track_object_ids = {0x31};
1185 if (minecart_sprite_ids.empty()) {
1186 minecart_sprite_ids = {0xA3};
1188 file <<
"[dungeon_overlay]\n";
1189 file <<
"track_tiles=" << FormatHexUintList(track_tiles) <<
"\n";
1190 file <<
"track_stop_tiles=" << FormatHexUintList(track_stop_tiles) <<
"\n";
1191 file <<
"track_switch_tiles=" << FormatHexUintList(track_switch_tiles)
1193 file <<
"track_object_ids=" << FormatHexUintList(track_object_ids) <<
"\n";
1194 file <<
"minecart_sprite_ids=" << FormatHexUintList(minecart_sprite_ids)
1198 file <<
"[graphics_sheets]\n";
1199 file <<
"reserved_sheets="
1201 file <<
"flagged_sheets="
1203 file <<
"reserved_blocks="
1208 file <<
"[atlas]\n";
1216 file <<
"[planning]\n";
1221 file <<
"[rom_addresses]\n";
1223 file << key <<
"=" << FormatHexUint32(value) <<
"\n";
1229 file <<
"[custom_objects]\n";
1231 file << absl::StrFormat(
"object_0x%X", object_id) <<
"="
1232 << absl::StrJoin(files,
",") <<
"\n";
1238 file <<
"[agent_settings]\n";
1243 file <<
"custom_system_prompt="
1246 file <<
"use_custom_prompt="
1248 file <<
"show_reasoning="
1256 file <<
"stream_responses="
1258 file <<
"favorite_models="
1263 file <<
"enable_tool_resources="
1265 file <<
"enable_tool_dungeon="
1267 file <<
"enable_tool_overworld="
1269 file <<
"enable_tool_messages="
1271 file <<
"enable_tool_dialogue="
1273 file <<
"enable_tool_gui="
1275 file <<
"enable_tool_music="
1277 file <<
"enable_tool_sprite="
1279 file <<
"enable_tool_emulator="
1281 file <<
"enable_tool_memory_inspector="
1289 file <<
"[keybindings]\n";
1291 file << key <<
"=" << value <<
"\n";
1298 file <<
"[editor_visibility]\n";
1300 file << key <<
"=" << (value ?
"true" :
"false") <<
"\n";
1307 if (!labels.empty()) {
1308 file <<
"[labels_" << type <<
"]\n";
1309 for (
const auto& [key, value] : labels) {
1310 file << key <<
"=" << value <<
"\n";
1317 file <<
"[build]\n";
1321 file <<
"track_changes=" << (
track_changes ?
"true" :
"false") <<
"\n";
1326 file <<
"asm_sources=" << absl::StrJoin(
asm_sources,
",") <<
"\n";
1331 file <<
"[music]\n";
1332 file <<
"persist_custom_music="
1339 file <<
"[zscream_compatibility]\n";
1342 file << key <<
"=" << value <<
"\n";
1347 file <<
"# End of YAZE Project File\n";
1355 std::string serialized =
1357 if (ContainsLoneCarriageReturn(serialized)) {
1358 return absl::InvalidArgumentError(
1359 "Project contains a lone carriage return in a value; refusing to "
1360 "write a descriptor that could not be read back");
1366 if (ContainsLoneCarriageReturn(content)) {
1367 return absl::InvalidArgumentError(
1368 "Project file contains unsupported lone carriage returns");
1380 std::istringstream stream(content);
1382 std::string current_section;
1386 while (std::getline(stream, line)) {
1390 if (!line.empty() && line.back() ==
'\r') {
1394 if (line.find_first_not_of(
" \t") == std::string::npos)
1397 if (line[0] ==
'#') {
1398 if (!IsGeneratedProjectComment(line, current_section.empty())) {
1399 passthrough_section->
lines.push_back(
1405 if (line.front() ==
'[' && line.back() ==
']') {
1406 current_section = line.substr(1, line.length() - 2);
1407 passthrough_section =
1409 passthrough_section->
known = IsKnownProjectSection(current_section);
1410 if (current_section ==
"atlas") {
1412 }
else if (current_section ==
"planning") {
1418 auto [key, value] = ParseKeyValue(line);
1421 bool understood = !key.empty();
1424 }
else if (current_section ==
"project") {
1427 else if (key ==
"description")
1429 else if (key ==
"author")
1431 else if (key ==
"license")
1433 else if (key ==
"version")
1435 else if (key ==
"created_date")
1437 else if (key ==
"last_modified")
1439 else if (key ==
"yaze_version")
1441 else if (key ==
"created_by")
1443 else if (key ==
"tags")
1445 else if (key ==
"project_id")
1449 }
else if (current_section ==
"files") {
1450 if (key ==
"rom_filename")
1452 else if (key ==
"rom_backup_folder")
1454 else if (key ==
"code_folder")
1456 else if (key ==
"assets_folder")
1458 else if (key ==
"patches_folder")
1460 else if (key ==
"labels_filename")
1462 else if (key ==
"symbols_filename")
1464 else if (key ==
"overworld_graphics_preview")
1466 else if (key ==
"dungeon_tile_patterns")
1468 else if (key ==
"cutscene_shots")
1470 else if (key ==
"custom_collision_json")
1472 else if (key ==
"output_folder")
1474 else if (key ==
"custom_objects_folder")
1476 else if (key ==
"palette_catalog_file")
1478 else if (key ==
"terrain_catalog_file")
1480 else if (key ==
"hack_manifest_file")
1482 else if (key ==
"sprite_catalog_file")
1484 else if (key ==
"sprite_source_root")
1486 else if (key ==
"additional_roms")
1490 }
else if (current_section ==
"sprite_assets") {
1491 if (key.rfind(
"asset_", 0) != 0 || value.size() > 65536 ||
1493 return absl::InvalidArgumentError(
"Invalid sprite_assets section");
1496 return asset.status();
1498 if (prior.zsm_path == asset->zsm_path)
1499 return absl::InvalidArgumentError(
"Duplicate sprite asset path");
1501 }
else if (current_section ==
"rom") {
1504 else if (key ==
"expected_hash")
1506 else if (key ==
"write_policy")
1510 }
else if (current_section ==
"feature_flags") {
1511 if (key ==
"load_custom_overworld")
1513 else if (key ==
"apply_zs_custom_overworld_asm")
1515 else if (key ==
"save_dungeon_maps")
1517 else if (key ==
"save_overworld_maps")
1519 else if (key ==
"save_overworld_entrances")
1521 else if (key ==
"save_overworld_exits")
1523 else if (key ==
"save_overworld_items")
1525 else if (key ==
"save_overworld_properties")
1527 else if (key ==
"save_dungeon_objects")
1529 else if (key ==
"save_dungeon_sprites")
1531 else if (key ==
"save_dungeon_room_headers")
1533 else if (key ==
"save_dungeon_torches")
1535 else if (key ==
"save_dungeon_pits")
1537 else if (key ==
"save_dungeon_blocks")
1539 else if (key ==
"save_dungeon_collision")
1541 else if (key ==
"save_dungeon_water_fill_zones")
1543 else if (key ==
"save_dungeon_chests")
1545 else if (key ==
"save_dungeon_pot_items")
1547 else if (key ==
"save_dungeon_entrances")
1549 else if (key ==
"save_dungeon_palettes")
1551 else if (key ==
"save_graphics_sheet")
1553 else if (key ==
"save_all_palettes")
1555 else if (key ==
"save_gfx_groups")
1557 else if (key ==
"save_messages")
1559 else if (key ==
"enable_custom_objects")
1563 }
else if (current_section ==
"workspace") {
1564 if (key ==
"font_global_scale")
1566 else if (key ==
"dark_mode")
1568 else if (key ==
"ui_theme")
1570 else if (key ==
"autosave_enabled")
1572 else if (key ==
"autosave_interval_secs")
1574 else if (key ==
"backup_on_save")
1576 else if (key ==
"backup_retention_count")
1578 else if (key ==
"backup_keep_daily")
1580 else if (key ==
"backup_keep_daily_days")
1582 else if (key ==
"show_grid")
1584 else if (key ==
"show_collision")
1586 else if (key ==
"prefer_hmagic_names")
1588 else if (key ==
"last_layout_preset")
1590 else if (key ==
"saved_layouts")
1592 else if (key ==
"recent_files")
1596 }
else if (current_section ==
"dungeon_overlay") {
1597 if (key ==
"track_tiles")
1599 else if (key ==
"track_stop_tiles")
1601 else if (key ==
"track_switch_tiles")
1603 else if (key ==
"track_object_ids")
1605 else if (key ==
"minecart_sprite_ids")
1609 }
else if (current_section ==
"graphics_sheets") {
1610 if (key ==
"reserved_sheets")
1612 else if (key ==
"flagged_sheets")
1614 else if (key ==
"reserved_blocks")
1618 }
else if (current_section ==
"atlas") {
1619 if (key ==
"annotations")
1621 else if (key ==
"defaults")
1623 else if (key ==
"symbols")
1625 else if (key ==
"export_folder")
1629 }
else if (current_section ==
"planning") {
1634 }
else if (current_section ==
"rom_addresses") {
1635 auto parsed = ParseHexUint32(value);
1636 if (parsed.has_value()) {
1641 }
else if (current_section ==
"custom_objects") {
1642 std::string id_token = key;
1643 if (absl::StartsWith(id_token,
"object_")) {
1644 id_token = id_token.substr(7);
1646 auto parsed = ParseHexUint32(id_token);
1647 if (parsed.has_value()) {
1649 ParsePositionalStringList(value);
1653 }
else if (current_section ==
"agent_settings") {
1654 if (key ==
"ai_provider")
1656 else if (key ==
"ai_model")
1658 else if (key ==
"ollama_host")
1660 else if (key ==
"gemini_api_key")
1662 else if (key ==
"custom_system_prompt")
1664 else if (key ==
"use_custom_prompt")
1666 else if (key ==
"show_reasoning")
1668 else if (key ==
"verbose")
1670 else if (key ==
"max_tool_iterations")
1672 else if (key ==
"max_retry_attempts")
1674 else if (key ==
"temperature")
1676 else if (key ==
"top_p")
1678 else if (key ==
"max_output_tokens")
1680 else if (key ==
"stream_responses")
1682 else if (key ==
"favorite_models")
1684 else if (key ==
"model_chain")
1686 else if (key ==
"chain_mode")
1688 else if (key ==
"enable_tool_resources")
1690 else if (key ==
"enable_tool_dungeon")
1692 else if (key ==
"enable_tool_overworld")
1694 else if (key ==
"enable_tool_messages")
1696 else if (key ==
"enable_tool_dialogue")
1698 else if (key ==
"enable_tool_gui")
1700 else if (key ==
"enable_tool_music")
1702 else if (key ==
"enable_tool_sprite")
1704 else if (key ==
"enable_tool_emulator")
1706 else if (key ==
"enable_tool_memory_inspector")
1708 else if (key ==
"builder_blueprint_path")
1712 }
else if (current_section ==
"build") {
1713 if (key ==
"build_script")
1715 else if (key ==
"output_folder")
1717 else if (key ==
"git_repository")
1719 else if (key ==
"track_changes")
1721 else if (key ==
"build_configurations")
1723 else if (key ==
"build_target")
1725 else if (key ==
"asm_entry_point")
1727 else if (key ==
"asm_sources")
1729 else if (key ==
"last_build_hash")
1731 else if (key ==
"build_number")
1735 }
else if (current_section.rfind(
"labels_", 0) == 0) {
1736 std::string label_type = current_section.substr(7);
1738 }
else if (current_section ==
"keybindings") {
1740 }
else if (current_section ==
"editor_visibility") {
1742 }
else if (current_section ==
"zscream_compatibility") {
1743 if (key ==
"original_project_file")
1747 }
else if (current_section ==
"music") {
1748 if (key ==
"persist_custom_music")
1750 else if (key ==
"storage_key")
1752 else if (key ==
"last_saved_at")
1759 passthrough_section->
lines.push_back(
1779 return absl::OkStatus();
1783#ifdef __EMSCRIPTEN__
1785 auto storage_or = platform::WasmStorage::LoadProject(storage_key);
1786 if (storage_or.ok()) {
1791 std::ifstream file(project_path);
1792 if (!file.is_open()) {
1793 return absl::InvalidArgumentError(
1794 absl::StrFormat(
"Cannot open project file: %s", project_path));
1797 std::stringstream buffer;
1798 buffer << file.rdbuf();
1805 auto now = std::chrono::system_clock::now();
1806 auto time_t = std::chrono::system_clock::to_time_t(now);
1807 std::stringstream ss;
1808 ss << std::put_time(std::localtime(&time_t),
"%Y-%m-%d %H:%M:%S");
1820#ifdef __EMSCRIPTEN__
1821 auto storage_status = platform::WasmStorage::SaveProject(
1823 if (!storage_status.ok()) {
1824 return storage_status;
1833 return absl::OkStatus();
1837 const std::string& zscream_project_path) {
1843 std::filesystem::path zs_path(zscream_project_path);
1844 name = zs_path.stem().string() +
"_imported";
1856 return absl::OkStatus();
1861 std::ofstream file(target_path);
1862 if (!file.is_open()) {
1863 return absl::InvalidArgumentError(
1864 absl::StrFormat(
"Cannot create ZScream project file: %s", target_path));
1868 file <<
"# ZScream Compatible Project File\n";
1870 file <<
"name=" <<
name <<
"\n";
1878 return absl::OkStatus();
1898 std::vector<std::string> errors;
1901 errors.push_back(
"Project name is required");
1903 errors.push_back(
"Project file path is required");
1905 errors.push_back(
"ROM file is required");
1907#ifndef __EMSCRIPTEN__
1911 errors.push_back(
"ROM file does not exist: " +
rom_filename);
1916 errors.push_back(
"Code folder does not exist: " +
code_folder);
1926 errors.push_back(
"Hack manifest file does not exist: " +
1929 errors.push_back(
"Hack manifest file failed to load: " +
1935 if (!errors.empty()) {
1936 return absl::InvalidArgumentError(absl::StrJoin(errors,
"; "));
1939 return absl::OkStatus();
1943 std::vector<std::string> missing;
1945#ifndef __EMSCRIPTEN__
1968#ifdef __EMSCRIPTEN__
1970 return absl::OkStatus();
1977 for (
const auto& folder : folders) {
1978 if (!folder.empty()) {
1980 if (!std::filesystem::exists(abs_path)) {
1981 std::filesystem::create_directories(abs_path);
1989 if (!std::filesystem::exists(abs_labels)) {
1990 std::ofstream labels_file(abs_labels);
1991 labels_file <<
"# yaze Resource Labels\n";
1992 labels_file <<
"# Format: [type] key=value\n\n";
1993 labels_file.close();
1997 return absl::OkStatus();
2005 return name.empty() ?
"Untitled Project" :
name;
2009 const std::string& absolute_path)
const {
2010 if (absolute_path.empty() ||
filepath.empty())
2011 return absolute_path;
2013 std::filesystem::path project_dir =
2014 std::filesystem::path(
filepath).parent_path();
2015 std::filesystem::path abs_path(absolute_path);
2018 std::filesystem::path relative =
2019 std::filesystem::relative(abs_path.lexically_normal(), project_dir);
2021 return relative.generic_string();
2024 return abs_path.lexically_normal().generic_string();
2029 const std::string& relative_path)
const {
2030 if (relative_path.empty() ||
filepath.empty())
2031 return relative_path;
2033 std::filesystem::path project_dir =
2034 std::filesystem::path(
filepath).parent_path();
2035 std::filesystem::path abs_path(relative_path);
2036 if (abs_path.is_absolute()) {
2037 abs_path = abs_path.lexically_normal();
2038 abs_path.make_preferred();
2039 return abs_path.string();
2041 abs_path = (project_dir / abs_path).lexically_normal();
2042 abs_path.make_preferred();
2044 return abs_path.string();
2048#ifdef __EMSCRIPTEN__
2056 auto normalize = [
this](std::string* path) {
2057 if (!path || path->empty()) {
2079 normalize(&asset.zsm_path);
2083 if (!rom_path.empty()) {
2090 std::vector<std::string> paths;
2111 const std::string& project_path) {
2115 std::filesystem::path zs_path(project_path);
2116 name = zs_path.stem().string() +
"_imported";
2121 return absl::OkStatus();
2133 absl::string_view artifact_name)
const {
2134 std::filesystem::path base_dir;
2142 base_dir = std::filesystem::path(
filepath).parent_path();
2145 if (base_dir.empty()) {
2146 return std::string(artifact_name);
2148 return (base_dir / std::string(artifact_name)).lexically_normal().string();
2154#if defined(YAZE_WITH_Z3DK) && __has_include("z3dk_core/config.h")
2155 std::vector<std::filesystem::path> candidates;
2156 auto add_candidate = [&candidates](
const std::filesystem::path& candidate) {
2157 if (candidate.empty()) {
2160 auto normalized = candidate.lexically_normal();
2161 if (std::find(candidates.begin(), candidates.end(), normalized) ==
2163 candidates.push_back(normalized);
2169 if (!std::filesystem::is_directory(code_path)) {
2170 code_path = code_path.parent_path();
2172 add_candidate(code_path /
"z3dk.toml");
2181 add_candidate(std::filesystem::path(
filepath).parent_path() /
"z3dk.toml");
2184 for (
const auto& candidate : candidates) {
2185 if (!std::filesystem::exists(candidate)) {
2190 z3dk::Config config = z3dk::LoadConfigFile(candidate.string(), &error);
2191 if (!error.empty()) {
2192 LOG_WARN(
"Project",
"Failed to parse z3dk config '%s': %s",
2193 candidate.string().c_str(), error.c_str());
2197 const std::filesystem::path base_dir = candidate.parent_path();
2200 if (config.preset.has_value()) {
2205 for (
const auto& include_path : config.include_paths) {
2207 ResolveOptionalPath(base_dir, include_path));
2211 for (
const auto& define : config.defines) {
2216 for (
const auto& main_file : config.main_files) {
2218 ResolveOptionalPath(base_dir, main_file));
2221 if (config.std_includes_path.has_value()) {
2223 ResolveOptionalPath(base_dir, *config.std_includes_path);
2225 if (config.std_defines_path.has_value()) {
2227 ResolveOptionalPath(base_dir, *config.std_defines_path);
2229 if (config.mapper.has_value()) {
2232 if (config.rom_size.has_value()) {
2235 if (config.symbols_format.has_value()) {
2239 if (config.lsp_log_path.has_value()) {
2241 ResolveOptionalPath(base_dir, *config.lsp_log_path);
2245 for (
const auto& emit_path : config.emits) {
2249 for (
const auto& range : config.prohibited_memory_ranges) {
2251 {.start = range.start, .end = range.end, .reason = range.reason});
2255 config.warn_unused_symbols.value_or(
true);
2257 config.warn_branch_outside_bank.value_or(
true);
2261 config.warn_unauthorized_hook.value_or(
true);
2265 if (config.rom_path.has_value()) {
2268 if (config.symbols_path.has_value()) {
2270 ResolveOptionalPath(base_dir, *config.symbols_path);
2274 const std::string basename = BasenameLower(emit_path);
2275 if (basename.ends_with(
".mlb") &&
2278 }
else if (basename ==
"sourcemap.json") {
2280 }
else if (basename ==
"annotations.json") {
2282 }
else if (basename ==
"hooks.json") {
2284 }
else if (basename ==
"lint.json") {
2315 "Loaded z3dk config from %s (%zu include paths, %zu defines)",
2324#ifdef __EMSCRIPTEN__
2334 std::filesystem::path loaded_manifest_path;
2335 auto load_manifest = [&](
const std::filesystem::path& candidate,
2336 bool update_project_setting) ->
bool {
2337 if (candidate.empty() || !std::filesystem::exists(candidate)) {
2342 LOG_WARN(
"Project",
"Failed to load hack manifest %s: %s",
2343 candidate.string().c_str(),
2344 std::string(status.message()).c_str());
2347 loaded_manifest_path = candidate;
2348 if (update_project_setting) {
2351 LOG_DEBUG(
"Project",
"Loaded hack manifest: %s",
2352 candidate.string().c_str());
2361 if (has_explicit_manifest) {
2369 auto candidate = std::filesystem::path(code_path) /
"hack_manifest.json";
2370 (void)load_manifest(candidate,
true);
2375 const std::filesystem::path project_dir =
2376 std::filesystem::path(
filepath).parent_path();
2377 (void)load_manifest(project_dir /
"hack_manifest.json",
true);
2379 (void)load_manifest(project_dir.parent_path() /
"hack_manifest.json",
2388 auto try_load_registry = [&](
const std::filesystem::path& base) ->
bool {
2392 const auto planning = base /
"Docs" /
"Dev" /
"Planning";
2393 if (!std::filesystem::exists(
planning)) {
2398 LOG_WARN(
"Project",
"Failed to load project registry from %s: %s",
2399 base.string().c_str(), std::string(status.message()).c_str());
2405 bool registry_loaded =
false;
2414 if (!registry_loaded && !loaded_manifest_path.empty()) {
2415 registry_loaded = try_load_registry(loaded_manifest_path.parent_path());
2419 if (!registry_loaded && !
filepath.empty()) {
2421 try_load_registry(std::filesystem::path(
filepath).parent_path());
2424 if (!registry_loaded) {
2429 "Hack manifest loaded but project registry was not found "
2430 "(code_folder='%s', manifest='%s')",
2431 code_folder.c_str(), loaded_manifest_path.string().c_str());
2436 size_t injected = 0;
2437 for (
const auto& [type_key, labels] :
2439 for (
const auto& [id_str, label] : labels) {
2444 LOG_DEBUG(
"Project",
"Loaded project registry: %zu resource labels injected",
2492 for (uint16_t tile = 0xB0; tile <= 0xBE; ++tile) {
2524 auto now = std::chrono::system_clock::now().time_since_epoch();
2526 std::chrono::duration_cast<std::chrono::milliseconds>(now).count();
2527 return absl::StrFormat(
"yaze_project_%lld", timestamp);
2531std::vector<ProjectManager::ProjectTemplate>
2533 std::vector<ProjectTemplate> templates;
2542 t.
name =
"Vanilla ROM Hack";
2544 "Standard ROM editing without custom ASM. Limited to vanilla features.";
2553 templates.push_back(t);
2559 t.
name =
"ZSCustomOverworld v2";
2561 "Basic overworld expansion: custom BG colors, main palettes, parent "
2574 templates.push_back(t);
2580 t.
name =
"ZSCustomOverworld v3 (Recommended)";
2582 "Full overworld expansion: wide/tall areas, animated GFX, overlays, "
2599 templates.push_back(t);
2605 t.
name =
"Randomizer Compatible";
2607 "Compatible with ALttP Randomizer. Minimal custom features to avoid "
2616 templates.push_back(t);
2626 t.
name =
"Dungeon Designer";
2627 t.
description =
"Focused on dungeon creation and modification.";
2634 templates.push_back(t);
2640 t.
name =
"Graphics Pack";
2642 "Project focused on graphics, sprites, and visual modifications.";
2651 templates.push_back(t);
2657 t.
name =
"Complete Overhaul";
2658 t.
description =
"Full-scale ROM hack with all features enabled.";
2673 templates.push_back(t);
2680 const std::string& template_name,
const std::string& project_name,
2681 const std::string& base_path) {
2683 auto status = project.
Create(project_name, base_path);
2689 if (template_name ==
"Full Overworld Mod") {
2693 project.
metadata.
tags = {
"overworld",
"maps",
"graphics"};
2694 }
else if (template_name ==
"Dungeon Designer") {
2698 project.
metadata.
tags = {
"dungeons",
"rooms",
"design"};
2699 }
else if (template_name ==
"Graphics Pack") {
2703 project.
metadata.
tags = {
"graphics",
"sprites",
"palettes"};
2704 }
else if (template_name ==
"Complete Overhaul") {
2710 project.
metadata.
tags = {
"complete",
"overhaul",
"full-mod"};
2713 status = project.
Save();
2722 const std::string& directory) {
2723#ifdef __EMSCRIPTEN__
2727 std::vector<std::string> projects;
2730 for (
const auto& entry : std::filesystem::directory_iterator(directory)) {
2731 if (entry.is_regular_file()) {
2732 std::string filename = entry.path().filename().string();
2733 if (filename.ends_with(
".yaze") || filename.ends_with(
".zsproj")) {
2734 projects.push_back(entry.path().string());
2736 }
else if (entry.is_directory()) {
2737 std::string filename = entry.path().filename().string();
2738 if (filename.ends_with(
".yazeproj")) {
2739 projects.push_back(entry.path().string());
2743 }
catch (
const std::filesystem::filesystem_error& e) {
2752#ifdef __EMSCRIPTEN__
2754 return absl::UnimplementedError(
2755 "Project backups are not supported in the web build");
2758 return absl::InvalidArgumentError(
"Project has no file path");
2761 std::filesystem::path project_path(project.
filepath);
2762 std::filesystem::path backup_dir = project_path.parent_path() /
"backups";
2763 std::filesystem::create_directories(backup_dir);
2765 auto now = std::chrono::system_clock::now();
2766 auto time_t = std::chrono::system_clock::to_time_t(now);
2767 std::stringstream ss;
2768 ss << std::put_time(std::localtime(&time_t),
"%Y%m%d_%H%M%S");
2770 std::string backup_filename = project.
name +
"_backup_" + ss.str() +
".yaze";
2771 std::filesystem::path backup_path = backup_dir / backup_filename;
2774 std::filesystem::copy_file(project.
filepath, backup_path);
2775 }
catch (
const std::filesystem::filesystem_error& e) {
2776 return absl::InternalError(
2777 absl::StrFormat(
"Failed to backup project: %s", e.what()));
2780 return absl::OkStatus();
2791 std::vector<std::string> recommendations;
2794 recommendations.push_back(
"Add a ROM file to begin editing");
2798 recommendations.push_back(
"Set up a code folder for assembly patches");
2802 recommendations.push_back(
"Create a labels file for better organization");
2806 recommendations.push_back(
"Add a project description for documentation");
2810 recommendations.push_back(
2811 "Consider setting up version control for your project");
2815 if (!missing_files.empty()) {
2816 recommendations.push_back(
2817 "Some project files are missing - use Project > Repair to fix");
2820 return recommendations;
2826 std::ifstream file(filename);
2827 if (!file.is_open()) {
2834 std::string current_type =
"";
2836 while (std::getline(file, line)) {
2837 if (line.empty() || line[0] ==
'#')
2841 if (line[0] ==
'[' && line.back() ==
']') {
2842 current_type = line.substr(1, line.length() - 2);
2847 size_t eq_pos = line.find(
'=');
2848 if (eq_pos != std::string::npos && !current_type.empty()) {
2849 std::string key = line.substr(0, eq_pos);
2850 std::string value = line.substr(eq_pos + 1);
2851 labels_[current_type][key] = value;
2856 MigrateGfxGroupLabelKeys(
labels_);
2866 if (!file.is_open())
2869 file <<
"# yaze Resource Labels\n";
2870 file <<
"# Format: [type] followed by key=value pairs\n\n";
2872 for (
const auto& [type, type_labels] :
labels_) {
2873 if (!type_labels.empty()) {
2874 file <<
"[" << type <<
"]\n";
2875 for (
const auto& [key, value] : type_labels) {
2876 file << key <<
"=" << value <<
"\n";
2887 if (!p_open || !*p_open)
2891 if (ImGui::Begin(
"Resource Labels", p_open)) {
2892 ImGui::Text(
"Resource Labels Manager");
2894 ImGui::Text(
"Total types: %zu",
labels_.size());
2896 for (
const auto& [type, type_labels] :
labels_) {
2897 if (ImGui::TreeNode(type.c_str())) {
2898 ImGui::Text(
"Labels: %zu", type_labels.size());
2899 for (
const auto& [key, value] : type_labels) {
2900 ImGui::Text(
"%s = %s", key.c_str(), value.c_str());
2910 const std::string& key,
2911 const std::string& newValue) {
2912 labels_[type][key] = newValue;
2916 bool selected,
const std::string& type,
const std::string& key,
2917 const std::string& defaultValue) {
2918 const auto custom_label =
GetLabel(type, key);
2919 const auto& label = custom_label.empty() ? defaultValue : custom_label;
2920 if (ImGui::Selectable(
2921 absl::StrFormat(
"%s: %s", key.c_str(), label.c_str()).c_str(),
2928 const std::string& key) {
2929 auto type_it =
labels_.find(type);
2933 auto label_it = type_it->second.find(key);
2934 if (label_it == type_it->second.end())
2937 return label_it->second;
2941 const std::string& type,
const std::string& key,
2942 const std::string& defaultValue) {
2943 auto existing =
GetLabel(type, key);
2944 if (!existing.empty())
2947 labels_[type][key] = defaultValue;
2948 return defaultValue;
2956 const std::unordered_map<
2957 std::string, std::unordered_map<std::string, std::string>>& labels) {
2986 LOG_DEBUG(
"Project",
"Initialized embedded labels:");
2988 LOG_DEBUG(
"Project",
" - %d entrance names",
2990 LOG_DEBUG(
"Project",
" - %d sprite names",
2992 LOG_DEBUG(
"Project",
" - %d overlord names",
2995 LOG_DEBUG(
"Project",
" - %d music names",
2997 LOG_DEBUG(
"Project",
" - %d graphics names",
2999 LOG_DEBUG(
"Project",
" - %d room effect names",
3001 LOG_DEBUG(
"Project",
" - %d room tag names",
3003 LOG_DEBUG(
"Project",
" - %d tile type names",
3006 return absl::OkStatus();
3007 }
catch (
const std::exception& e) {
3008 return absl::InternalError(
3009 absl::StrCat(
"Failed to initialize embedded labels: ", e.what()));
3014 const std::string& default_value)
const {
3018 auto label_it = type_it->second.find(std::to_string(
id));
3019 if (label_it != type_it->second.end()) {
3020 return label_it->second;
3024 return default_value.empty() ? resource_type +
"_" + std::to_string(
id)
3029#ifdef __EMSCRIPTEN__
3031 return absl::UnimplementedError(
3032 "File-based label import is not supported in the web build");
3035 if (!file.is_open()) {
3036 return absl::InvalidArgumentError(
3037 absl::StrFormat(
"Cannot open labels file: %s",
filepath));
3040 std::stringstream buffer;
3041 buffer << file.rdbuf();
3049 const std::string& content) {
3056 auto status = provider.ImportFromZScreamFormat(content);
3061 LOG_DEBUG(
"Project",
"Imported ZScream labels:");
3062 LOG_DEBUG(
"Project",
" - %d sprite labels",
3066 LOG_DEBUG(
"Project",
" - %d room tag labels",
3069 return absl::OkStatus();
3078 LOG_DEBUG(
"Project",
"Initialized ResourceLabelProvider with project labels");
3079 LOG_DEBUG(
"Project",
" - prefer_hmagic_names: %s",
3081 LOG_DEBUG(
"Project",
" - hack_manifest: %s",
3089#ifdef YAZE_ENABLE_JSON_PROJECT_FORMAT
3091absl::Status YazeProject::LoadFromJsonFormat(
const std::string& project_path) {
3095#ifdef __EMSCRIPTEN__
3096 return absl::UnimplementedError(
3097 "JSON project format loading is not supported in the web build");
3099 std::ifstream file(project_path);
3100 if (!file.is_open()) {
3101 return absl::InvalidArgumentError(
3102 absl::StrFormat(
"Cannot open JSON project file: %s", project_path));
3114 if (j.contains(
"yaze_project")) {
3115 auto& proj = j[
"yaze_project"];
3117 if (proj.contains(
"name"))
3118 name = proj[
"name"].get<std::string>();
3119 if (proj.contains(
"description"))
3121 if (proj.contains(
"author"))
3123 if (proj.contains(
"version"))
3125 if (proj.contains(
"created"))
3127 if (proj.contains(
"modified"))
3129 if (proj.contains(
"created_by"))
3133 if (proj.contains(
"rom_filename"))
3134 rom_filename = proj[
"rom_filename"].get<std::string>();
3135 if (proj.contains(
"rom_backup_folder"))
3137 if (proj.contains(
"code_folder"))
3138 code_folder = proj[
"code_folder"].get<std::string>();
3139 if (proj.contains(
"assets_folder"))
3141 if (proj.contains(
"patches_folder"))
3143 if (proj.contains(
"labels_filename"))
3145 if (proj.contains(
"symbols_filename"))
3147 if (proj.contains(
"dungeon_tile_patterns"))
3149 proj[
"dungeon_tile_patterns"].get<std::string>();
3150 if (proj.contains(
"palette_catalog_file"))
3152 if (proj.contains(
"terrain_catalog_file"))
3154 if (proj.contains(
"hack_manifest_file"))
3156 if (proj.contains(
"sprite_catalog_file"))
3158 if (proj.contains(
"sprite_source_root"))
3160 if (proj.contains(
"sprite_assets")) {
3161 if (!proj[
"sprite_assets"].is_array() ||
3162 proj[
"sprite_assets"].size() > 4096)
3163 return absl::InvalidArgumentError(
"Invalid sprite_assets array");
3164 for (
const auto& value : proj[
"sprite_assets"]) {
3167 return asset.status();
3169 if (prior.zsm_path == asset->zsm_path)
3170 return absl::InvalidArgumentError(
"Duplicate sprite asset path");
3175 if (proj.contains(
"rom") && proj[
"rom"].is_object()) {
3176 auto&
rom = proj[
"rom"];
3177 if (
rom.contains(
"role"))
3179 if (
rom.contains(
"expected_hash"))
3181 if (
rom.contains(
"write_policy"))
3187 if (proj.contains(
"use_embedded_labels")) {
3192 if (proj.contains(
"feature_flags")) {
3193 auto& flags = proj[
"feature_flags"];
3196 if (flags.contains(
"kSaveDungeonMaps"))
3198 flags[
"kSaveDungeonMaps"].get<
bool>();
3199 if (flags.contains(
"kSaveOverworldMaps"))
3201 flags[
"kSaveOverworldMaps"].get<
bool>();
3202 if (flags.contains(
"kSaveOverworldEntrances"))
3204 flags[
"kSaveOverworldEntrances"].get<
bool>();
3205 if (flags.contains(
"kSaveOverworldExits"))
3207 flags[
"kSaveOverworldExits"].get<
bool>();
3208 if (flags.contains(
"kSaveOverworldItems"))
3210 flags[
"kSaveOverworldItems"].get<
bool>();
3211 if (flags.contains(
"kSaveOverworldProperties"))
3213 flags[
"kSaveOverworldProperties"].get<
bool>();
3214 if (flags.contains(
"kSaveDungeonObjects"))
3216 flags[
"kSaveDungeonObjects"].get<
bool>();
3217 if (flags.contains(
"kSaveDungeonSprites"))
3219 flags[
"kSaveDungeonSprites"].get<
bool>();
3220 if (flags.contains(
"kSaveDungeonRoomHeaders"))
3222 flags[
"kSaveDungeonRoomHeaders"].get<
bool>();
3223 if (flags.contains(
"kSaveDungeonTorches"))
3225 flags[
"kSaveDungeonTorches"].get<
bool>();
3226 if (flags.contains(
"kSaveDungeonPits"))
3228 flags[
"kSaveDungeonPits"].get<
bool>();
3229 if (flags.contains(
"kSaveDungeonBlocks"))
3231 flags[
"kSaveDungeonBlocks"].get<
bool>();
3232 if (flags.contains(
"kSaveDungeonCollision"))
3234 flags[
"kSaveDungeonCollision"].get<
bool>();
3235 if (flags.contains(
"kSaveDungeonWaterFillZones"))
3237 flags[
"kSaveDungeonWaterFillZones"].get<
bool>();
3238 if (flags.contains(
"kSaveDungeonChests"))
3240 flags[
"kSaveDungeonChests"].get<
bool>();
3241 if (flags.contains(
"kSaveDungeonPotItems"))
3243 flags[
"kSaveDungeonPotItems"].get<
bool>();
3244 if (flags.contains(
"kSaveDungeonEntrances"))
3246 flags[
"kSaveDungeonEntrances"].get<
bool>();
3247 if (flags.contains(
"kSaveDungeonPalettes"))
3249 flags[
"kSaveDungeonPalettes"].get<
bool>();
3250 if (flags.contains(
"kSaveGraphicsSheet"))
3252 flags[
"kSaveGraphicsSheet"].get<
bool>();
3253 if (flags.contains(
"kSaveAllPalettes"))
3255 flags[
"kSaveAllPalettes"].get<
bool>();
3256 if (flags.contains(
"kSaveGfxGroups"))
3258 if (flags.contains(
"kSaveMessages"))
3263 if (proj.contains(
"workspace_settings")) {
3264 auto& ws = proj[
"workspace_settings"];
3265 if (ws.contains(
"auto_save_enabled"))
3267 ws[
"auto_save_enabled"].get<
bool>();
3268 if (ws.contains(
"auto_save_interval"))
3270 ws[
"auto_save_interval"].get<
float>();
3271 if (ws.contains(
"backup_on_save"))
3273 if (ws.contains(
"backup_retention_count"))
3275 ws[
"backup_retention_count"].get<
int>();
3276 if (ws.contains(
"backup_keep_daily"))
3278 ws[
"backup_keep_daily"].get<
bool>();
3279 if (ws.contains(
"backup_keep_daily_days"))
3281 ws[
"backup_keep_daily_days"].get<
int>();
3284 if (proj.contains(
"rom_addresses") && proj[
"rom_addresses"].is_object()) {
3286 for (
auto it = proj[
"rom_addresses"].begin();
3287 it != proj[
"rom_addresses"].end(); ++it) {
3288 if (it.value().is_number_unsigned()) {
3290 it.value().get<uint32_t>();
3291 }
else if (it.value().is_string()) {
3293 if (parsed.has_value()) {
3300 if (proj.contains(
"custom_objects") &&
3301 proj[
"custom_objects"].is_object()) {
3303 for (
auto it = proj[
"custom_objects"].begin();
3304 it != proj[
"custom_objects"].end(); ++it) {
3305 if (!it.value().is_array())
3308 if (!parsed.has_value()) {
3311 std::vector<std::string> files;
3312 for (
const auto& entry : it.value()) {
3313 if (entry.is_string()) {
3314 files.push_back(entry.get<std::string>());
3317 if (!files.empty()) {
3323 if (proj.contains(
"agent_settings") &&
3324 proj[
"agent_settings"].is_object()) {
3325 auto& agent = proj[
"agent_settings"];
3352 if (agent.contains(
"favorite_models") &&
3353 agent[
"favorite_models"].is_array()) {
3355 for (
const auto& model : agent[
"favorite_models"]) {
3356 if (model.is_string())
3358 model.get<std::string>());
3361 if (agent.contains(
"model_chain") && agent[
"model_chain"].is_array()) {
3363 for (
const auto& model : agent[
"model_chain"]) {
3364 if (model.is_string())
3389 agent.value(
"enable_tool_memory_inspector",
3396 if (proj.contains(
"build_script"))
3397 build_script = proj[
"build_script"].get<std::string>();
3398 if (proj.contains(
"output_folder"))
3400 if (proj.contains(
"git_repository"))
3402 if (proj.contains(
"track_changes"))
3406 return absl::OkStatus();
3407 }
catch (
const json::exception& e) {
3408 return absl::InvalidArgumentError(
3409 absl::StrFormat(
"JSON parse error: %s", e.what()));
3413absl::Status YazeProject::SaveToJsonFormat() {
3414#ifdef __EMSCRIPTEN__
3415 return absl::UnimplementedError(
3416 "JSON project format saving is not supported in the web build");
3419 auto& proj = j[
"yaze_project"];
3423 proj[
"name"] =
name;
3438 proj[
"dungeon_tile_patterns"] =
3440 proj[
"palette_catalog_file"] =
3442 proj[
"terrain_catalog_file"] =
3445 proj[
"sprite_catalog_file"] =
3447 proj[
"sprite_source_root"] =
3449 proj[
"sprite_assets"] = nlohmann::json::array();
3458 proj[
"rom"][
"write_policy"] =
3467 proj[
"feature_flags"][
"kSaveOverworldMaps"] =
3469 proj[
"feature_flags"][
"kSaveOverworldEntrances"] =
3471 proj[
"feature_flags"][
"kSaveOverworldExits"] =
3473 proj[
"feature_flags"][
"kSaveOverworldItems"] =
3475 proj[
"feature_flags"][
"kSaveOverworldProperties"] =
3477 proj[
"feature_flags"][
"kSaveDungeonObjects"] =
3479 proj[
"feature_flags"][
"kSaveDungeonSprites"] =
3481 proj[
"feature_flags"][
"kSaveDungeonRoomHeaders"] =
3483 proj[
"feature_flags"][
"kSaveDungeonTorches"] =
3486 proj[
"feature_flags"][
"kSaveDungeonBlocks"] =
3488 proj[
"feature_flags"][
"kSaveDungeonCollision"] =
3490 proj[
"feature_flags"][
"kSaveDungeonWaterFillZones"] =
3492 proj[
"feature_flags"][
"kSaveDungeonChests"] =
3494 proj[
"feature_flags"][
"kSaveDungeonPotItems"] =
3496 proj[
"feature_flags"][
"kSaveDungeonEntrances"] =
3498 proj[
"feature_flags"][
"kSaveDungeonPalettes"] =
3500 proj[
"feature_flags"][
"kSaveGraphicsSheet"] =
3507 proj[
"workspace_settings"][
"auto_save_enabled"] =
3509 proj[
"workspace_settings"][
"auto_save_interval"] =
3511 proj[
"workspace_settings"][
"backup_on_save"] =
3513 proj[
"workspace_settings"][
"backup_retention_count"] =
3515 proj[
"workspace_settings"][
"backup_keep_daily"] =
3517 proj[
"workspace_settings"][
"backup_keep_daily_days"] =
3520 auto& agent = proj[
"agent_settings"];
3547 agent[
"enable_tool_memory_inspector"] =
3552 auto& addrs = proj[
"rom_addresses"];
3559 auto& objs = proj[
"custom_objects"];
3561 objs[absl::StrFormat(
"0x%X", object_id)] = files;
3572 if (!file.is_open()) {
3573 return absl::InvalidArgumentError(
3574 absl::StrFormat(
"Cannot write JSON project file: %s",
filepath));
3578 return absl::OkStatus();
3586 if (!config_dir.ok()) {
3593#ifdef __EMSCRIPTEN__
3594 auto status = platform::WasmStorage::SaveProject(
3597 LOG_WARN(
"RecentFilesManager",
"Could not persist recent files: %s",
3598 status.ToString().c_str());
3604 if (!config_dir_status.ok()) {
3605 LOG_ERROR(
"Project",
"Failed to get or create config directory: %s",
3606 config_dir_status.status().ToString().c_str());
3611 std::ofstream file(filepath);
3612 if (!file.is_open()) {
3613 LOG_WARN(
"RecentFilesManager",
"Could not save recent files to %s",
3619 file << file_path << std::endl;
3624#ifdef __EMSCRIPTEN__
3626 if (!storage_or.ok()) {
3630 std::istringstream stream(storage_or.value());
3632 while (std::getline(stream, line)) {
3633 if (!line.empty()) {
3641 std::ifstream file(filepath);
3642 if (!file.is_open()) {
3649 while (std::getline(file, line)) {
3650 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)
Rom * rom()
Get the current ROM instance.
float ParseFloat(const std::string &value)
std::string ResolveOptionalPath(const std::filesystem::path &base_dir, const std::string &value)
void RemoveProjectSaveTempFile(const std::filesystem::path &temp_path)
bool ContainsLoneCarriageReturn(const std::string &content)
std::vector< std::string > ParsePositionalStringList(const std::string &value)
absl::Status WriteProjectFileAtomicallyImpl(const std::filesystem::path &target_path, absl::string_view contents, bool replace_existing)
std::vector< uint16_t > ParseHexUintList(const std::string &value)
std::string ToLowerCopy(std::string value)
constexpr absl::string_view kProjectFileFooter
bool ParseBool(const std::string &value)
std::pair< std::string, std::string > ParseDefineToken(const std::string &value)
std::optional< uint32_t > ParseHexUint32(const std::string &value)
bool IsKnownProjectSection(absl::string_view name)
std::string FormatHexUint32(uint32_t value)
ProjectFilePassthrough::Section & PassthroughSection(ProjectFilePassthrough &passthrough, const std::string &name)
std::map< uint16_t, std::vector< uint16_t > > ParseHexUintListMap(const std::string &value)
std::string FormatHexUintListMap(const std::map< uint16_t, std::vector< uint16_t > > &values)
bool IsGeneratedProjectComment(absl::string_view line, bool before_sections)
std::string SanitizeStorageKey(absl::string_view input)
std::filesystem::path MakeProjectSaveTempPath(const std::filesystem::path &target_path)
std::string MergeProjectFilePassthrough(const std::string &canonical, const ProjectFilePassthrough &passthrough)
uint64_t CurrentProcessIdForProjectSave()
std::string FormatHexUintList(const std::vector< uint16_t > &values)
void MigrateGfxGroupLabelKeys(std::unordered_map< std::string, std::unordered_map< std::string, std::string > > &labels)
bool IsKeyLine(absl::string_view line)
std::string BasenameLower(const std::string &path)
std::vector< std::string > ParseStringList(const std::string &value)
std::string RomRoleToString(RomRole role)
absl::Status WriteProjectFileAtomically(absl::string_view target_path, absl::string_view contents, bool replace_existing)
RomRole ParseRomRole(absl::string_view value)
const std::string kRecentFilesFilename
RomWritePolicy ParseRomWritePolicy(absl::string_view value)
std::string RomWritePolicyToString(RomWritePolicy policy)
absl::StatusOr< SpriteAssetBinding > ParseSpriteAssetBinding(const std::string &text)
nlohmann::json SpriteAssetToJson(const SpriteAssetBinding &asset)
absl::StatusOr< SpriteAssetBinding > SpriteAssetFromJson(const nlohmann::json &value)
bool IsKnownProjectFileSection(absl::string_view name)
ResourceLabelProvider & GetResourceLabels()
Get the global ResourceLabelProvider instance.
#define RETURN_IF_ERROR(expr)
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
Atlas map views ([atlas]).
std::vector< std::string > annotations
std::string export_folder
std::vector< uint16_t > track_object_ids
std::vector< uint16_t > minecart_sprite_ids
std::vector< uint16_t > track_stop_tiles
std::vector< uint16_t > track_tiles
std::vector< uint16_t > track_switch_tiles
std::vector< uint16_t > reserved_sheets
std::map< uint16_t, std::vector< uint16_t > > reserved_blocks
std::vector< uint16_t > flagged_sheets
Planning editor sidecar ([planning]).
std::vector< Line > lines
Lines of a .yaze file that this build does not model.
std::vector< Section > sections
YazeProject template_project
std::string CreateOrGetLabel(const std::string &type, const std::string &key, const std::string &defaultValue)
void DisplayLabels(bool *p_open)
std::string GetLabel(const std::string &type, const std::string &key)
void EditLabel(const std::string &type, const std::string &key, const std::string &newValue)
bool LoadLabels(const std::string &filename)
void SelectableLabelWithNameEdit(bool selected, const std::string &type, const std::string &key, const std::string &defaultValue)
std::unordered_map< std::string, std::unordered_map< std::string, std::string > > labels_
int backup_keep_daily_days
std::string last_layout_preset
std::map< std::string, std::string > custom_keybindings
int backup_retention_count
std::vector< std::string > saved_layouts
std::map< std::string, bool > editor_visibility
float autosave_interval_secs
std::vector< std::string > recent_files
std::string custom_system_prompt
std::string gemini_api_key
bool enable_tool_emulator
bool enable_tool_memory_inspector
std::vector< std::string > favorite_models
bool enable_tool_resources
bool enable_tool_messages
std::vector< std::string > model_chain
std::string builder_blueprint_path
bool enable_tool_dialogue
bool enable_tool_overworld
std::string last_saved_at
bool persist_custom_music
Modern project structure with comprehensive settings consolidation.
std::string rom_backup_folder
std::unordered_map< int, std::vector< std::string > > custom_object_files
absl::Status ResetToDefaults()
std::string custom_objects_folder
absl::Status RepairProject()
std::string MakeStorageKey(absl::string_view suffix) const
static std::string ResolveBundleRoot(const std::string &path)
struct yaze::project::YazeProject::MusicPersistence music_persistence
std::string sprite_catalog_file
absl::StatusOr< std::string > SerializeToString() const
std::string zscream_project_file
absl::Status ExportForZScream(const std::string &target_path)
absl::Status SaveToYazeFormat(bool replace_existing=true)
absl::Status ImportZScreamProject(const std::string &zscream_project_path)
absl::Status SaveAllSettings()
absl::Status LoadFromString(const std::string &content, const std::string &project_path)
void NormalizePathsToAbsolute()
absl::Status ImportLabelsFromZScreamContent(const std::string &content)
Import labels from ZScream format content directly.
std::string git_repository
core::HackManifest hack_manifest
void InitializeResourceLabelProvider()
Initialize the global ResourceLabelProvider with this project's labels.
absl::Status ParseFromString(const std::string &content)
std::string cutscene_shots
std::vector< std::string > additional_roms
std::string terrain_catalog_file
std::string patches_folder
absl::Status LoadFromYazeFormat(const std::string &project_path)
std::unordered_map< std::string, std::unordered_map< std::string, std::string > > resource_labels
std::vector< std::string > AtlasAnnotationPaths() const
std::string GenerateProjectId() const
absl::Status Create(const std::string &project_name, const std::string &base_path)
std::string assets_folder
void ReloadHackManifest()
GraphicsSheetSettings graphics_sheets
std::string AtlasSymbolsPath() const
absl::Status LoadAllSettings()
std::string labels_filename
std::vector< std::string > asm_sources
std::string hack_manifest_file
std::string GetDisplayName() const
PlanningSettings planning
std::string PlanningFilePath() const
std::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
std::vector< SpriteAssetBinding > sprite_assets
absl::Status InitializeEmbeddedLabels(const std::unordered_map< std::string, std::unordered_map< std::string, std::string > > &labels)
ProjectFilePassthrough file_passthrough
absl::Status SaveAs(const std::string &new_path)
struct yaze::project::YazeProject::AgentSettings agent_settings
std::string custom_collision_json
DungeonOverlaySettings dungeon_overlay
std::string palette_catalog_file
absl::Status ImportFromZScreamFormat(const std::string &project_path)
void InitializeDefaults()
std::string GetAbsolutePath(const std::string &relative_path) const
std::string overworld_graphics_preview
std::string GetLabel(const std::string &resource_type, int id, const std::string &default_value="") const
std::string sprite_source_root
absl::Status Open(const std::string &project_path)
absl::Status ImportLabelsFromZScream(const std::string &filepath)
Import labels from a ZScream DefaultNames.txt file.
std::string last_build_hash
void TryLoadHackManifest()
std::map< std::string, std::string > zscream_mappings
std::string dungeon_tile_patterns
absl::Status Validate() const
core::FeatureFlags::Flags feature_flags
std::vector< std::string > build_configurations
void ReloadZ3dkSettings()
core::RomAddressOverrides rom_address_overrides
std::string symbols_filename
Z3dkSettings z3dk_settings
std::string annotations_json
std::string sourcemap_json
std::vector< std::string > include_paths
std::string std_includes_path
std::vector< std::string > main_files
std::vector< std::string > emits
std::vector< std::pair< std::string, std::string > > defines
std::string symbols_format
Z3dkArtifactPaths artifact_paths
bool warn_branch_outside_bank
std::optional< bool > lsp_log_enabled
bool warn_unauthorized_hook
std::vector< Z3dkMemoryRange > prohibited_memory_ranges
std::string std_defines_path