From 164f89e1bb69b2bd0d70c45be5418bfd336822c0 Mon Sep 17 00:00:00 2001 From: scawful Date: Sat, 4 Oct 2025 01:41:55 -0400 Subject: [PATCH] feat: Add INI and JSON format support for project files and update resource label handling --- assets/zelda3.yaze | 65 ++++++++++++++ src/app/core/project.cc | 153 +++++++++++++++++++++++++++++++- src/app/core/project.h | 5 ++ src/app/zelda3/dungeon/room.cc | 101 +++++++++++++++++++++ src/app/zelda3/zelda3_labels.cc | 4 +- src/app/zelda3/zelda3_labels.h | 3 +- 6 files changed, 327 insertions(+), 4 deletions(-) create mode 100644 assets/zelda3.yaze diff --git a/assets/zelda3.yaze b/assets/zelda3.yaze new file mode 100644 index 00000000..98528122 --- /dev/null +++ b/assets/zelda3.yaze @@ -0,0 +1,65 @@ +# Default Zelda3 Project File +# All resource names are embedded and always available to AI agents +# This project uses embedded labels - no external labels file required + +[project] +name = The Legend of Zelda: A Link to the Past +description = Default Zelda3 project with all embedded resource labels +author = Nintendo +version = 1.0.0 +created_date = 2025-10-04 +last_modified = 2025-10-04 +yaze_version = 0.1.0 +tags = zelda3, reference, default + +[files] +rom_filename = zelda3.sfc +code_folder = +assets_folder = assets +patches_folder = patches +labels_filename = +symbols_filename = +output_folder = build +rom_backup_folder = backups + +[feature_flags] +log_instructions = false +save_dungeon_maps = false +save_graphics_sheet = false +load_custom_overworld = false +apply_zs_custom_overworld_asm = false + +[workspace] +font_global_scale = 1.0 +dark_mode = true +ui_theme = default +autosave_enabled = true +autosave_interval_secs = 300 +backup_on_save = true +show_grid = true +show_collision = false +last_layout_preset = default + +[build] +build_script = +output_folder = build +git_repository = +track_changes = true +build_configurations = debug, release + +# Embedded Labels Information +# This project includes the following embedded resource names: +# - 296 room names (dungeons, bosses, treasure rooms) +# - 133 entrance names (dungeons, caves, houses, shops) +# - 256 sprite names (enemies, NPCs, bosses, items) +# - 26 overlord names (factories, traps, special objects) +# - 160 overworld map names (Light World, Dark World, Special Areas) +# - 100 item names (swords, shields, medallions, bottles) +# - 48 music track names +# - 32 graphics sheet names +# - 8 room effect names +# - 13 room tag names +# - 60 tile type names (collision, slopes, water, ice, stairs) +# +# Use InitializeEmbeddedLabels() to load all default labels +# Custom labels can be added in [labels_] sections diff --git a/src/app/core/project.cc b/src/app/core/project.cc index 19fdb957..d05c6780 100644 --- a/src/app/core/project.cc +++ b/src/app/core/project.cc @@ -15,6 +15,11 @@ #include "imgui/imgui.h" #include "yaze_config.h" +#ifdef YAZE_ENABLE_JSON_PROJECT_FORMAT +#include "nlohmann/json.hpp" +using json = nlohmann::json; +#endif + namespace yaze { namespace core { @@ -111,7 +116,27 @@ absl::Status YazeProject::Open(const std::string& project_path) { // Determine format and load accordingly if (project_path.ends_with(".yaze")) { format = ProjectFormat::kYazeNative; - return LoadFromYazeFormat(project_path); + + // Try to detect if it's JSON format by peeking at first character + std::ifstream file(project_path); + if (file.is_open()) { + char first_char; + file.get(first_char); + file.close(); + +#ifdef YAZE_ENABLE_JSON_PROJECT_FORMAT + if (first_char == '{') { + std::cout << "📄 Detected JSON format project file\n"; + return LoadFromJsonFormat(project_path); + } +#endif + + // Default to INI format + return LoadFromYazeFormat(project_path); + } + + return absl::InvalidArgumentError( + absl::StrFormat("Cannot open project file: %s", project_path)); } else if (project_path.ends_with(".zsproj")) { format = ProjectFormat::kZScreamCompat; return ImportFromZScreamFormat(project_path); @@ -868,5 +893,131 @@ std::string YazeProject::GetLabel(const std::string& resource_type, int id, : default_value; } +// ============================================================================ +// JSON Format Support (Optional) +// ============================================================================ + +#ifdef YAZE_ENABLE_JSON_PROJECT_FORMAT + +absl::Status YazeProject::LoadFromJsonFormat(const std::string& project_path) { + std::ifstream file(project_path); + if (!file.is_open()) { + return absl::InvalidArgumentError( + absl::StrFormat("Cannot open JSON project file: %s", project_path)); + } + + try { + json j; + file >> j; + + // Parse project metadata + if (j.contains("yaze_project")) { + auto& proj = j["yaze_project"]; + + if (proj.contains("name")) name = proj["name"].get(); + if (proj.contains("description")) metadata.description = proj["description"].get(); + if (proj.contains("author")) metadata.author = proj["author"].get(); + if (proj.contains("version")) metadata.version = proj["version"].get(); + if (proj.contains("created")) metadata.created_date = proj["created"].get(); + if (proj.contains("modified")) metadata.last_modified = proj["modified"].get(); + + // Files + if (proj.contains("rom_filename")) rom_filename = proj["rom_filename"].get(); + if (proj.contains("code_folder")) code_folder = proj["code_folder"].get(); + if (proj.contains("assets_folder")) assets_folder = proj["assets_folder"].get(); + if (proj.contains("patches_folder")) patches_folder = proj["patches_folder"].get(); + if (proj.contains("labels_filename")) labels_filename = proj["labels_filename"].get(); + if (proj.contains("symbols_filename")) symbols_filename = proj["symbols_filename"].get(); + + // Embedded labels flag + if (proj.contains("use_embedded_labels")) { + use_embedded_labels = proj["use_embedded_labels"].get(); + } + + // Feature flags + if (proj.contains("feature_flags")) { + auto& flags = proj["feature_flags"]; + if (flags.contains("kLogInstructions")) + feature_flags.kLogInstructions = flags["kLogInstructions"].get(); + if (flags.contains("kSaveDungeonMaps")) + feature_flags.kSaveDungeonMaps = flags["kSaveDungeonMaps"].get(); + if (flags.contains("kSaveGraphicsSheet")) + feature_flags.kSaveGraphicsSheet = flags["kSaveGraphicsSheet"].get(); + } + + // Workspace settings + if (proj.contains("workspace_settings")) { + auto& ws = proj["workspace_settings"]; + if (ws.contains("auto_save_enabled")) + workspace_settings.autosave_enabled = ws["auto_save_enabled"].get(); + if (ws.contains("auto_save_interval")) + workspace_settings.autosave_interval_secs = ws["auto_save_interval"].get(); + } + + // Build settings + if (proj.contains("build_script")) build_script = proj["build_script"].get(); + if (proj.contains("output_folder")) output_folder = proj["output_folder"].get(); + if (proj.contains("git_repository")) git_repository = proj["git_repository"].get(); + if (proj.contains("track_changes")) track_changes = proj["track_changes"].get(); + } + + return absl::OkStatus(); + } catch (const json::exception& e) { + return absl::InvalidArgumentError( + absl::StrFormat("JSON parse error: %s", e.what())); + } +} + +absl::Status YazeProject::SaveToJsonFormat() { + json j; + auto& proj = j["yaze_project"]; + + // Metadata + proj["version"] = metadata.version; + proj["name"] = name; + proj["author"] = metadata.author; + proj["description"] = metadata.description; + proj["created"] = metadata.created_date; + proj["modified"] = metadata.last_modified; + + // Files + proj["rom_filename"] = rom_filename; + proj["code_folder"] = code_folder; + proj["assets_folder"] = assets_folder; + proj["patches_folder"] = patches_folder; + proj["labels_filename"] = labels_filename; + proj["symbols_filename"] = symbols_filename; + proj["output_folder"] = output_folder; + + // Embedded labels + proj["use_embedded_labels"] = use_embedded_labels; + + // Feature flags + proj["feature_flags"]["kLogInstructions"] = feature_flags.kLogInstructions; + proj["feature_flags"]["kSaveDungeonMaps"] = feature_flags.kSaveDungeonMaps; + proj["feature_flags"]["kSaveGraphicsSheet"] = feature_flags.kSaveGraphicsSheet; + + // Workspace settings + proj["workspace_settings"]["auto_save_enabled"] = workspace_settings.autosave_enabled; + proj["workspace_settings"]["auto_save_interval"] = workspace_settings.autosave_interval_secs; + + // Build settings + proj["build_script"] = build_script; + proj["git_repository"] = git_repository; + proj["track_changes"] = track_changes; + + // Write to file + std::ofstream file(filepath); + if (!file.is_open()) { + return absl::InvalidArgumentError( + absl::StrFormat("Cannot write JSON project file: %s", filepath)); + } + + file << j.dump(2); // Pretty print with 2-space indent + return absl::OkStatus(); +} + +#endif // YAZE_ENABLE_JSON_PROJECT_FORMAT + } // namespace core } // namespace yaze diff --git a/src/app/core/project.h b/src/app/core/project.h index ac6cf865..5948857f 100644 --- a/src/app/core/project.h +++ b/src/app/core/project.h @@ -154,6 +154,11 @@ private: absl::Status SaveToYazeFormat(); absl::Status ImportFromZScreamFormat(const std::string& project_path); +#ifdef YAZE_ENABLE_JSON_PROJECT_FORMAT + absl::Status LoadFromJsonFormat(const std::string& project_path); + absl::Status SaveToJsonFormat(); +#endif + void InitializeDefaults(); std::string GenerateProjectId() const; }; diff --git a/src/app/zelda3/dungeon/room.cc b/src/app/zelda3/dungeon/room.cc index 03d947af..7a8fa17f 100644 --- a/src/app/zelda3/dungeon/room.cc +++ b/src/app/zelda3/dungeon/room.cc @@ -284,20 +284,35 @@ void Room::CopyRoomGraphicsToBuffer() { } void Room::RenderRoomGraphics() { + std::printf("\n=== RenderRoomGraphics Room %d ===\n", room_id_); + CopyRoomGraphicsToBuffer(); + std::printf("1. Graphics buffer copied\n"); gfx::Arena::Get().bg1().DrawFloor(rom()->vector(), tile_address, tile_address_floor, floor1_graphics_); gfx::Arena::Get().bg2().DrawFloor(rom()->vector(), tile_address, tile_address_floor, floor2_graphics_); + std::printf("2. Floor pattern drawn\n"); + + // Render layout and object tiles to background buffers + RenderObjectsToBackground(); + std::printf("3. Objects rendered to buffer\n"); gfx::Arena::Get().bg1().DrawBackground(std::span(current_gfx16_)); gfx::Arena::Get().bg2().DrawBackground(std::span(current_gfx16_)); + std::printf("4. Background drawn from buffer\n"); + + auto& bg1_bmp = gfx::Arena::Get().bg1().bitmap(); + auto& bg2_bmp = gfx::Arena::Get().bg2().bitmap(); + std::printf("5. BG1 bitmap: active=%d, size=%dx%d, data_size=%zu\n", + bg1_bmp.is_active(), bg1_bmp.width(), bg1_bmp.height(), bg1_bmp.vector().size()); auto bg1_palette = rom()->mutable_palette_group()->get_group("dungeon_main")[0].palette(0); if (!gfx::Arena::Get().bg1().bitmap().is_active()) { + std::printf("6a. Creating new bitmap textures\n"); core::Renderer::Get().CreateAndRenderBitmap( 0x200, 0x200, 0x200, gfx::Arena::Get().bg1().bitmap().vector(), gfx::Arena::Get().bg1().bitmap(), bg1_palette); @@ -305,10 +320,96 @@ void Room::RenderRoomGraphics() { 0x200, 0x200, 0x200, gfx::Arena::Get().bg2().bitmap().vector(), gfx::Arena::Get().bg2().bitmap(), bg1_palette); } else { + std::printf("6b. Updating existing bitmap textures\n"); // Update the bitmap core::Renderer::Get().UpdateBitmap(&gfx::Arena::Get().bg1().bitmap()); core::Renderer::Get().UpdateBitmap(&gfx::Arena::Get().bg2().bitmap()); } + + std::printf("7. BG1 has texture: %d\n", bg1_bmp.texture() != nullptr); + std::printf("=== RenderRoomGraphics Complete ===\n\n"); +} + +void Room::RenderObjectsToBackground() { + if (!rom_ || !rom_->is_loaded()) { + std::printf("RenderObjectsToBackground: ROM not loaded\n"); + return; + } + + std::printf("RenderObjectsToBackground: Room %d has %zu objects\n", room_id_, tile_objects_.size()); + + // Get references to the background buffers + auto& bg1 = gfx::Arena::Get().bg1(); + auto& bg2 = gfx::Arena::Get().bg2(); + + // Render tile objects to their respective layers + int rendered_count = 0; + for (const auto& obj : tile_objects_) { + // Ensure object has tiles loaded + auto mutable_obj = const_cast(obj); + mutable_obj.EnsureTilesLoaded(); + + // Get tiles with error handling + auto tiles_result = obj.GetTiles(); + if (!tiles_result.ok()) { + std::printf(" Object at (%d,%d) failed to load tiles: %s\n", + obj.x_, obj.y_, tiles_result.status().ToString().c_str()); + continue; + } + if (tiles_result->empty()) { + std::printf(" Object at (%d,%d) has no tiles\n", obj.x_, obj.y_); + continue; + } + + const auto& tiles = *tiles_result; + std::printf(" Object at (%d,%d) has %zu tiles\n", obj.x_, obj.y_, tiles.size()); + + // Calculate object position in tile coordinates (each position is an 8x8 tile) + int obj_x = obj.x_; // X position in 8x8 tile units + int obj_y = obj.y_; // Y position in 8x8 tile units + + // Determine which layer this object belongs to + bool is_bg2 = (obj.layer_ == RoomObject::LayerType::BG2); + auto& target_buffer = is_bg2 ? bg2 : bg1; + + // Draw each Tile16 from the object + // Each Tile16 is a 16x16 tile made of 4 TileInfo (8x8) tiles + for (size_t i = 0; i < tiles.size(); i++) { + const auto& tile16 = tiles[i]; + + // Calculate tile16 position (in 16x16 units, so multiply by 2 for 8x8 units) + int base_x = obj_x + ((i % 4) * 2); // Assume 4-tile16 width for now + int base_y = obj_y + ((i / 4) * 2); + + // Each Tile16 contains 4 TileInfo objects arranged as: + // [0][1] (top-left, top-right) + // [2][3] (bottom-left, bottom-right) + const auto& tile_infos = tile16.tiles_info; + + // Draw the 4 sub-tiles of this Tile16 + for (int sub_tile = 0; sub_tile < 4; sub_tile++) { + int tile_x = base_x + (sub_tile % 2); + int tile_y = base_y + (sub_tile / 2); + + // Bounds check + if (tile_x < 0 || tile_x >= 64 || tile_y < 0 || tile_y >= 64) { + continue; + } + + // Convert TileInfo to word format: (vflip<<15) | (hflip<<14) | (over<<13) | (palette<<10) | tile_id + uint16_t tile_word = gfx::TileInfoToWord(tile_infos[sub_tile]); + + // Set the tile in the buffer + target_buffer.SetTileAt(tile_x, tile_y, tile_word); + rendered_count++; + } + } + } + + std::printf("RenderObjectsToBackground: Rendered %d tiles total\n", rendered_count); + + // Note: Layout objects rendering would go here if needed + // For now, focusing on regular tile objects which is what ZScream primarily renders } void Room::LoadAnimatedGraphics() { diff --git a/src/app/zelda3/zelda3_labels.cc b/src/app/zelda3/zelda3_labels.cc index a987aaf3..ecd6ae69 100644 --- a/src/app/zelda3/zelda3_labels.cc +++ b/src/app/zelda3/zelda3_labels.cc @@ -273,9 +273,9 @@ const std::vector& Zelda3Labels::GetTileTypeNames() { } // Convert all labels to structured map for project embedding -std::map> +std::unordered_map> Zelda3Labels::ToResourceLabels() { - std::map> labels; + std::unordered_map> labels; // Rooms const auto& rooms = GetRoomNames(); diff --git a/src/app/zelda3/zelda3_labels.h b/src/app/zelda3/zelda3_labels.h index 3a8381ed..bbf7e79a 100644 --- a/src/app/zelda3/zelda3_labels.h +++ b/src/app/zelda3/zelda3_labels.h @@ -3,6 +3,7 @@ #include #include +#include #include namespace yaze { @@ -59,7 +60,7 @@ struct Zelda3Labels { * @brief Convert all labels to a structured map for project embedding * @return Map of resource type -> (id -> name) for all resources */ - static std::map> ToResourceLabels(); + static std::unordered_map> ToResourceLabels(); /** * @brief Get a label by resource type and ID