9#include "absl/strings/str_format.h"
10#include "absl/strings/str_join.h"
11#include "absl/strings/str_split.h"
17#include "imgui/imgui.h"
18#include "yaze_config.h"
20#ifdef YAZE_ENABLE_JSON_PROJECT_FORMAT
21#include "nlohmann/json.hpp"
22using json = nlohmann::json;
30 std::pair<std::string, std::string>
ParseKeyValue(
const std::string& line) {
31 size_t eq_pos = line.find(
'=');
32 if (eq_pos == std::string::npos)
return {
"",
""};
34 std::string key = line.substr(0, eq_pos);
35 std::string value = line.substr(eq_pos + 1);
38 key.erase(0, key.find_first_not_of(
" \t"));
39 key.erase(key.find_last_not_of(
" \t") + 1);
40 value.erase(0, value.find_first_not_of(
" \t"));
41 value.erase(value.find_last_not_of(
" \t") + 1);
47 return value ==
"true" || value ==
"1" || value ==
"yes";
52 return std::stof(value);
59 std::vector<std::string> result;
60 if (value.empty())
return result;
62 std::vector<std::string> parts = absl::StrSplit(value,
',');
63 for (
const auto& part : parts) {
64 std::string trimmed = std::string(part);
65 trimmed.erase(0, trimmed.find_first_not_of(
" \t"));
66 trimmed.erase(trimmed.find_last_not_of(
" \t") + 1);
67 if (!trimmed.empty()) {
68 result.push_back(trimmed);
78 filepath = base_path +
"/" + project_name +
".yaze";
81 auto now = std::chrono::system_clock::now();
82 auto time_t = std::chrono::system_clock::to_time_t(now);
84 ss << std::put_time(std::localtime(&time_t),
"%Y-%m-%d %H:%M:%S");
95 std::filesystem::path project_dir(base_path +
"/" + project_name);
96 std::filesystem::create_directories(project_dir);
97 std::filesystem::create_directories(project_dir /
"code");
98 std::filesystem::create_directories(project_dir /
"assets");
99 std::filesystem::create_directories(project_dir /
"patches");
100 std::filesystem::create_directories(project_dir /
"backups");
101 std::filesystem::create_directories(project_dir /
"output");
119 if (project_path.ends_with(
".yaze")) {
123 std::ifstream file(project_path);
124 if (file.is_open()) {
126 file.get(first_char);
129#ifdef YAZE_ENABLE_JSON_PROJECT_FORMAT
130 if (first_char ==
'{') {
131 LOG_DEBUG(
"Project",
"Detected JSON format project file");
132 return LoadFromJsonFormat(project_path);
140 return absl::InvalidArgumentError(
141 absl::StrFormat(
"Cannot open project file: %s", project_path));
142 }
else if (project_path.ends_with(
".zsproj")) {
147 return absl::InvalidArgumentError(
"Unsupported project file format");
155 std::string old_filepath =
filepath;
158 auto status =
Save();
167 std::ifstream file(project_path);
168 if (!file.is_open()) {
169 return absl::InvalidArgumentError(absl::StrFormat(
"Cannot open project file: %s", project_path));
173 std::string current_section =
"";
175 while (std::getline(file, line)) {
177 if (line.empty() || line[0] ==
'#')
continue;
180 if (line[0] ==
'[' && line.back() ==
']') {
181 current_section = line.substr(1, line.length() - 2);
185 auto [key, value] = ParseKeyValue(line);
186 if (key.empty())
continue;
189 if (current_section ==
"project") {
190 if (key ==
"name")
name = value;
198 else if (key ==
"tags")
metadata.
tags = ParseStringList(value);
200 else if (current_section ==
"files") {
203 else if (key ==
"code_folder")
code_folder = value;
209 else if (key ==
"additional_roms")
additional_roms = ParseStringList(value);
211 else if (current_section ==
"feature_flags") {
218 else if (current_section ==
"workspace") {
231 else if (current_section ==
"agent_settings") {
243 else if (current_section ==
"build") {
247 else if (key ==
"track_changes")
track_changes = ParseBool(value);
250 else if (current_section.starts_with(
"labels_")) {
252 std::string label_type = current_section.substr(7);
255 else if (current_section ==
"keybindings") {
258 else if (current_section ==
"editor_visibility") {
261 else if (current_section ==
"zscream_compatibility") {
268 return absl::OkStatus();
273 auto now = std::chrono::system_clock::now();
274 auto time_t = std::chrono::system_clock::to_time_t(now);
275 std::stringstream ss;
276 ss << std::put_time(std::localtime(&time_t),
"%Y-%m-%d %H:%M:%S");
280 if (!file.is_open()) {
281 return absl::InvalidArgumentError(absl::StrFormat(
"Cannot create project file: %s",
filepath));
285 file <<
"# yaze Project File\n";
286 file <<
"# Format Version: 2.0\n";
291 file <<
"[project]\n";
292 file <<
"name=" <<
name <<
"\n";
300 file <<
"tags=" << absl::StrJoin(
metadata.
tags,
",") <<
"\n\n";
312 file <<
"additional_roms=" << absl::StrJoin(
additional_roms,
",") <<
"\n\n";
315 file <<
"[feature_flags]\n";
323 file <<
"[workspace]\n";
337 file <<
"[agent_settings]\n";
351 file <<
"[keybindings]\n";
353 file << key <<
"=" << value <<
"\n";
360 file <<
"[editor_visibility]\n";
362 file << key <<
"=" << (value ?
"true" :
"false") <<
"\n";
369 if (!labels.empty()) {
370 file <<
"[labels_" << type <<
"]\n";
371 for (
const auto& [key, value] : labels) {
372 file << key <<
"=" << value <<
"\n";
383 file <<
"track_changes=" << (
track_changes ?
"true" :
"false") <<
"\n";
388 file <<
"[zscream_compatibility]\n";
391 file << key <<
"=" << value <<
"\n";
396 file <<
"# End of YAZE Project File\n";
399 return absl::OkStatus();
408 std::filesystem::path zs_path(zscream_project_path);
409 name = zs_path.stem().string() +
"_imported";
421 return absl::OkStatus();
426 std::ofstream file(target_path);
427 if (!file.is_open()) {
428 return absl::InvalidArgumentError(absl::StrFormat(
"Cannot create ZScream project file: %s", target_path));
432 file <<
"# ZScream Compatible Project File\n";
434 file <<
"name=" <<
name <<
"\n";
442 return absl::OkStatus();
462 std::vector<std::string> errors;
464 if (
name.empty()) errors.push_back(
"Project name is required");
465 if (
filepath.empty()) errors.push_back(
"Project file path is required");
466 if (
rom_filename.empty()) errors.push_back(
"ROM file is required");
470 errors.push_back(
"ROM file does not exist: " +
rom_filename);
474 errors.push_back(
"Code folder does not exist: " +
code_folder);
481 if (!errors.empty()) {
482 return absl::InvalidArgumentError(absl::StrJoin(errors,
"; "));
485 return absl::OkStatus();
489 std::vector<std::string> missing;
509 for (
const auto& folder : folders) {
510 if (!folder.empty()) {
512 if (!std::filesystem::exists(abs_path)) {
513 std::filesystem::create_directories(abs_path);
521 if (!std::filesystem::exists(abs_labels)) {
522 std::ofstream labels_file(abs_labels);
523 labels_file <<
"# yaze Resource Labels\n";
524 labels_file <<
"# Format: [type] key=value\n\n";
529 return absl::OkStatus();
536 return name.empty() ?
"Untitled Project" :
name;
540 if (absolute_path.empty() ||
filepath.empty())
return absolute_path;
542 std::filesystem::path project_dir = std::filesystem::path(
filepath).parent_path();
543 std::filesystem::path abs_path(absolute_path);
546 std::filesystem::path relative = std::filesystem::relative(abs_path, project_dir);
547 return relative.string();
549 return absolute_path;
554 if (relative_path.empty() ||
filepath.empty())
return relative_path;
556 std::filesystem::path project_dir = std::filesystem::path(
filepath).parent_path();
557 std::filesystem::path abs_path = project_dir / relative_path;
559 return abs_path.string();
570 std::filesystem::path zs_path(project_path);
571 name = zs_path.stem().string() +
"_imported";
576 return absl::OkStatus();
604 auto now = std::chrono::system_clock::now().time_since_epoch();
605 auto timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(now).count();
606 return absl::StrFormat(
"yaze_project_%lld", timestamp);
614 "Simple project for modifying an existing ROM with basic tools",
619 "Full Overworld Mod",
620 "Complete overworld modification with custom graphics and maps",
626 "Focused on dungeon creation and modification",
632 "Project focused on graphics, sprites, and visual modifications",
638 "Full-scale ROM hack with all features enabled",
646 const std::string& template_name,
647 const std::string& project_name,
648 const std::string& base_path) {
651 auto status = project.
Create(project_name, base_path);
657 if (template_name ==
"Full Overworld Mod") {
661 project.
metadata.
tags = {
"overworld",
"maps",
"graphics"};
662 }
else if (template_name ==
"Dungeon Designer") {
666 project.
metadata.
tags = {
"dungeons",
"rooms",
"design"};
667 }
else if (template_name ==
"Graphics Pack") {
671 project.
metadata.
tags = {
"graphics",
"sprites",
"palettes"};
672 }
else if (template_name ==
"Complete Overhaul") {
678 project.
metadata.
tags = {
"complete",
"overhaul",
"full-mod"};
681 status = project.
Save();
690 std::vector<std::string> projects;
693 for (
const auto& entry : std::filesystem::directory_iterator(directory)) {
694 if (entry.is_regular_file()) {
695 std::string filename = entry.path().filename().string();
696 if (filename.ends_with(
".yaze") || filename.ends_with(
".zsproj")) {
697 projects.push_back(entry.path().string());
701 }
catch (
const std::filesystem::filesystem_error& e) {
710 return absl::InvalidArgumentError(
"Project has no file path");
713 std::filesystem::path project_path(project.
filepath);
714 std::filesystem::path backup_dir = project_path.parent_path() /
"backups";
715 std::filesystem::create_directories(backup_dir);
717 auto now = std::chrono::system_clock::now();
718 auto time_t = std::chrono::system_clock::to_time_t(now);
719 std::stringstream ss;
720 ss << std::put_time(std::localtime(&time_t),
"%Y%m%d_%H%M%S");
722 std::string backup_filename = project.
name +
"_backup_" + ss.str() +
".yaze";
723 std::filesystem::path backup_path = backup_dir / backup_filename;
726 std::filesystem::copy_file(project.
filepath, backup_path);
727 }
catch (
const std::filesystem::filesystem_error& e) {
728 return absl::InternalError(absl::StrFormat(
"Failed to backup project: %s", e.what()));
731 return absl::OkStatus();
739 std::vector<std::string> recommendations;
742 recommendations.push_back(
"Add a ROM file to begin editing");
746 recommendations.push_back(
"Set up a code folder for assembly patches");
750 recommendations.push_back(
"Create a labels file for better organization");
754 recommendations.push_back(
"Add a project description for documentation");
758 recommendations.push_back(
"Consider setting up version control for your project");
762 if (!missing_files.empty()) {
763 recommendations.push_back(
"Some project files are missing - use Project > Repair to fix");
766 return recommendations;
772 std::ifstream file(filename);
773 if (!file.is_open()) {
780 std::string current_type =
"";
782 while (std::getline(file, line)) {
783 if (line.empty() || line[0] ==
'#')
continue;
786 if (line[0] ==
'[' && line.back() ==
']') {
787 current_type = line.substr(1, line.length() - 2);
792 size_t eq_pos = line.find(
'=');
793 if (eq_pos != std::string::npos && !current_type.empty()) {
794 std::string key = line.substr(0, eq_pos);
795 std::string value = line.substr(eq_pos + 1);
796 labels_[current_type][key] = value;
809 if (!file.is_open())
return false;
811 file <<
"# yaze Resource Labels\n";
812 file <<
"# Format: [type] followed by key=value pairs\n\n";
814 for (
const auto& [type, type_labels] :
labels_) {
815 if (!type_labels.empty()) {
816 file <<
"[" << type <<
"]\n";
817 for (
const auto& [key, value] : type_labels) {
818 file << key <<
"=" << value <<
"\n";
829 if (!p_open || !*p_open)
return;
832 if (ImGui::Begin(
"Resource Labels", p_open)) {
833 ImGui::Text(
"Resource Labels Manager");
835 ImGui::Text(
"Total types: %zu",
labels_.size());
837 for (
const auto& [type, type_labels] :
labels_) {
838 if (ImGui::TreeNode(type.c_str())) {
839 ImGui::Text(
"Labels: %zu", type_labels.size());
840 for (
const auto& [key, value] : type_labels) {
841 ImGui::Text(
"%s = %s", key.c_str(), value.c_str());
851 const std::string& newValue) {
856 const std::string& key,
857 const std::string& defaultValue) {
859 if (ImGui::Selectable(absl::StrFormat(
"%s: %s", key.c_str(),
GetLabel(type, key).c_str()).c_str(), selected)) {
865 auto type_it =
labels_.find(type);
866 if (type_it ==
labels_.end())
return "";
868 auto label_it = type_it->second.find(key);
869 if (label_it == type_it->second.end())
return "";
871 return label_it->second;
875 const std::string& defaultValue) {
876 auto existing =
GetLabel(type, key);
877 if (!existing.empty())
return existing;
879 labels_[type][key] = defaultValue;
893 LOG_DEBUG(
"Project",
"Initialized embedded labels:");
907 return absl::OkStatus();
908 }
catch (
const std::exception& e) {
909 return absl::InternalError(
910 absl::StrCat(
"Failed to initialize embedded labels: ", e.what()));
915 const std::string& default_value)
const {
919 auto label_it = type_it->second.find(std::to_string(
id));
920 if (label_it != type_it->second.end()) {
921 return label_it->second;
930 return default_value.empty()
931 ? resource_type +
"_" + std::to_string(
id)
939#ifdef YAZE_ENABLE_JSON_PROJECT_FORMAT
941absl::Status YazeProject::LoadFromJsonFormat(
const std::string& project_path) {
942 std::ifstream file(project_path);
943 if (!file.is_open()) {
944 return absl::InvalidArgumentError(
945 absl::StrFormat(
"Cannot open JSON project file: %s", project_path));
953 if (j.contains(
"yaze_project")) {
954 auto& proj = j[
"yaze_project"];
956 if (proj.contains(
"name"))
name = proj[
"name"].get<std::string>();
957 if (proj.contains(
"description"))
metadata.
description = proj[
"description"].get<std::string>();
958 if (proj.contains(
"author"))
metadata.
author = proj[
"author"].get<std::string>();
959 if (proj.contains(
"version"))
metadata.
version = proj[
"version"].get<std::string>();
964 if (proj.contains(
"rom_filename"))
rom_filename = proj[
"rom_filename"].get<std::string>();
965 if (proj.contains(
"code_folder"))
code_folder = proj[
"code_folder"].get<std::string>();
966 if (proj.contains(
"assets_folder"))
assets_folder = proj[
"assets_folder"].get<std::string>();
967 if (proj.contains(
"patches_folder"))
patches_folder = proj[
"patches_folder"].get<std::string>();
968 if (proj.contains(
"labels_filename"))
labels_filename = proj[
"labels_filename"].get<std::string>();
969 if (proj.contains(
"symbols_filename"))
symbols_filename = proj[
"symbols_filename"].get<std::string>();
972 if (proj.contains(
"use_embedded_labels")) {
977 if (proj.contains(
"feature_flags")) {
978 auto& flags = proj[
"feature_flags"];
980 if (flags.contains(
"kSaveDungeonMaps"))
982 if (flags.contains(
"kSaveGraphicsSheet"))
987 if (proj.contains(
"workspace_settings")) {
988 auto& ws = proj[
"workspace_settings"];
989 if (ws.contains(
"auto_save_enabled"))
991 if (ws.contains(
"auto_save_interval"))
996 if (proj.contains(
"build_script"))
build_script = proj[
"build_script"].get<std::string>();
997 if (proj.contains(
"output_folder"))
output_folder = proj[
"output_folder"].get<std::string>();
998 if (proj.contains(
"git_repository"))
git_repository = proj[
"git_repository"].get<std::string>();
999 if (proj.contains(
"track_changes"))
track_changes = proj[
"track_changes"].get<bool>();
1002 return absl::OkStatus();
1003 }
catch (
const json::exception& e) {
1004 return absl::InvalidArgumentError(
1005 absl::StrFormat(
"JSON parse error: %s", e.what()));
1009absl::Status YazeProject::SaveToJsonFormat() {
1011 auto& proj = j[
"yaze_project"];
1015 proj[
"name"] =
name;
1049 if (!file.is_open()) {
1050 return absl::InvalidArgumentError(
1051 absl::StrFormat(
"Cannot write JSON project file: %s",
filepath));
1055 return absl::OkStatus();
1063 if (!config_dir.ok()) {
1072 if (!config_dir_status.ok()) {
1073 LOG_ERROR(
"Project",
"Failed to get or create config directory: %s",
1074 config_dir_status.status().ToString().c_str());
1079 std::ofstream file(filepath);
1080 if (!file.is_open()) {
1081 LOG_WARN(
"RecentFilesManager",
"Could not save recent files to %s", filepath.c_str());
1086 file << file_path << std::endl;
1092 std::ifstream file(filepath);
1093 if (!file.is_open()) {
1100 while (std::getline(file, line)) {
1101 if (!line.empty()) {
static std::vector< ProjectTemplate > GetProjectTemplates()
static absl::Status ValidateProjectStructure(const YazeProject &project)
static std::vector< std::string > GetRecommendedFixesForProject(const YazeProject &project)
static absl::StatusOr< YazeProject > CreateFromTemplate(const std::string &template_name, const std::string &project_name, const std::string &base_path)
static absl::Status BackupProject(const YazeProject &project)
static std::vector< std::string > FindProjectsInDirectory(const std::string &directory)
std::string GetFilePath() const
std::vector< std::string > recent_files_
#define ICON_MD_VIDEOGAME_ASSET
#define LOG_DEBUG(category, format,...)
#define LOG_ERROR(category, format,...)
#define LOG_WARN(category, format,...)
float ParseFloat(const std::string &value)
std::vector< std::string > ParseStringList(const std::string &value)
bool ParseBool(const std::string &value)
std::pair< std::string, std::string > ParseKeyValue(const std::string &line)
const std::string kRecentFilesFilename
Main namespace for the application.
bool kApplyZSCustomOverworldASM
bool kLoadCustomOverworld
struct yaze::core::FeatureFlags::Flags::Overworld overworld
void SelectableLabelWithNameEdit(bool selected, const std::string &type, const std::string &key, const std::string &defaultValue)
bool LoadLabels(const std::string &filename)
void DisplayLabels(bool *p_open)
std::string GetLabel(const std::string &type, const std::string &key)
std::unordered_map< std::string, std::unordered_map< std::string, std::string > > labels_
void EditLabel(const std::string &type, const std::string &key, const std::string &newValue)
std::string CreateOrGetLabel(const std::string &type, const std::string &key, const std::string &defaultValue)
float autosave_interval_secs
std::vector< std::string > recent_files
std::map< std::string, std::string > custom_keybindings
std::vector< std::string > saved_layouts
std::string last_layout_preset
std::map< std::string, bool > editor_visibility
std::string gemini_api_key
std::string custom_system_prompt
Modern project structure with comprehensive settings consolidation.
absl::Status Open(const std::string &project_path)
absl::Status SaveToYazeFormat()
absl::Status LoadAllSettings()
std::unordered_map< std::string, std::unordered_map< std::string, std::string > > resource_labels
std::map< std::string, std::string > zscream_mappings
std::vector< std::string > additional_roms
WorkspaceSettings workspace_settings
std::string assets_folder
struct yaze::core::YazeProject::AgentSettings agent_settings
absl::Status Validate() const
std::string symbols_filename
absl::Status LoadFromYazeFormat(const std::string &project_path)
FeatureFlags::Flags feature_flags
std::string GenerateProjectId() const
std::string GetLabel(const std::string &resource_type, int id, const std::string &default_value="") const
std::string GetRelativePath(const std::string &absolute_path) const
std::string labels_filename
std::string output_folder
std::string git_repository
absl::Status RepairProject()
std::string GetDisplayName() const
absl::Status InitializeEmbeddedLabels()
absl::Status ImportFromZScreamFormat(const std::string &project_path)
std::vector< std::string > build_configurations
absl::Status Create(const std::string &project_name, const std::string &base_path)
std::vector< std::string > GetMissingFiles() const
void InitializeDefaults()
std::string GetAbsolutePath(const std::string &relative_path) const
std::string zscream_project_file
absl::Status ResetToDefaults()
absl::Status SaveAllSettings()
std::string rom_backup_folder
absl::Status ImportZScreamProject(const std::string &zscream_project_path)
absl::Status ExportForZScream(const std::string &target_path)
absl::Status SaveAs(const std::string &new_path)
std::string patches_folder
static std::unordered_map< std::string, std::unordered_map< std::string, std::string > > ToResourceLabels()
Convert all labels to a structured map for project embedding.
static std::string GetLabel(const std::string &resource_type, int id, const std::string &default_value="")
Get a label by resource type and ID.