From b89dcca93f3ccd9286767fc8c4fefbce5dca4e44 Mon Sep 17 00:00:00 2001 From: scawful Date: Fri, 3 Oct 2025 09:35:49 -0400 Subject: [PATCH] Refactor Agent Commands and Enhance Resource Context Management - Updated the immediate action plan to focus on integrating `Tile16ProposalGenerator` and `ResourceContextBuilder` into agent commands, improving command handling and proposal generation. - Implemented the `SetTile` method in the `Overworld` class to facilitate tile modifications based on the current world context. - Enhanced error handling in command execution to ensure robust feedback during ROM operations. - Created new files for `Tile16ProposalGenerator` and `ResourceContextBuilder`, enabling structured management of tile changes and resource labels for AI prompts. This commit advances the functionality of the z3ed system, laying the groundwork for more sophisticated AI-driven editing capabilities. --- docs/z3ed/AGENTIC-PLAN-STATUS.md | 52 ++-- src/app/zelda3/overworld/overworld.h | 9 + src/cli/handlers/overworld.cc | 43 ++- src/cli/service/resource_context_builder.cc | 262 ++++++++++++++++++ src/cli/service/resource_context_builder.h | 163 ++++++++++++ src/cli/service/tile16_proposal_generator.cc | 263 +++++++++++++++++++ src/cli/service/tile16_proposal_generator.h | 147 +++++++++++ src/cli/z3ed.cmake | 45 ++-- 8 files changed, 933 insertions(+), 51 deletions(-) create mode 100644 src/cli/service/resource_context_builder.cc create mode 100644 src/cli/service/resource_context_builder.h create mode 100644 src/cli/service/tile16_proposal_generator.cc create mode 100644 src/cli/service/tile16_proposal_generator.h diff --git a/docs/z3ed/AGENTIC-PLAN-STATUS.md b/docs/z3ed/AGENTIC-PLAN-STATUS.md index 5d699910..4b244a20 100644 --- a/docs/z3ed/AGENTIC-PLAN-STATUS.md +++ b/docs/z3ed/AGENTIC-PLAN-STATUS.md @@ -253,33 +253,31 @@ unset GEMINI_API_KEY ## 🚀 Next Steps -### Immediate Actions (Today) +### Immediate Actions (Next Session) -1. **Test Ollama Integration** (30 min) +1. **Integrate Tile16ProposalGenerator into Agent Commands** (2 hours) + - Modify `HandlePlanCommand()` to use generator + - Modify `HandleRunCommand()` to apply proposals + - Add `HandleAcceptCommand()` for accepting proposals + +2. **Integrate ResourceContextBuilder into PromptBuilder** (1 hour) + - Update `BuildContextualPrompt()` to inject labels + - Test with actual labels file from user project + +3. **Test End-to-End Workflow** (1 hour) ```bash ollama serve - ollama pull qwen2.5-coder:7b - ./build-grpc-test/bin/z3ed agent plan --prompt "test" + ./build-grpc-test/bin/z3ed agent plan \ + --prompt "Create a 3x3 water pond at 15, 10" + + # Verify proposal generation + # Verify tile16 changes are correct ``` -2. **Test Gemini Integration** (30 min) - ```bash - export GEMINI_API_KEY="your-key" - ./build-grpc-test/bin/z3ed agent plan --prompt "test" - ``` - -3. **Run End-to-End Test** (1 hour) - ```bash - ./build-grpc-test/bin/z3ed agent run \ - --prompt "Change palette 0 color 5 to red" \ - --rom assets/zelda3.sfc \ - --sandbox - ``` - -4. **Document Results** (30 min) - - Create `TESTING-RESULTS.md` with actual outputs - - Update `GEMINI-TESTING-STATUS.md` with validation - - Mark Phase 2 & 4 as validated in checklists +4. **Add Visual Diff Implementation** (2-3 hours) + - Render tile16 bitmaps from overworld + - Create side-by-side comparison images + - Highlight changed tiles ### Short-Term (This Week) @@ -351,13 +349,17 @@ unset GEMINI_API_KEY ## 📝 Files Summary -### Created/Modified in This Session +### Created/Modified Recently - ✅ `src/cli/handlers/agent/test_common.{h,cc}` (NEW) - ✅ `src/cli/handlers/agent/test_commands.cc` (REBUILT) - ✅ `src/cli/z3ed.cmake` (UPDATED) - ✅ `src/cli/service/gemini_ai_service.cc` (FIXED includes) -- ✅ `docs/z3ed/BUILD-FIX-COMPLETED.md` (NEW) -- ✅ `docs/z3ed/AGENTIC-PLAN-STATUS.md` (NEW - this file) +- ✅ `src/cli/service/tile16_proposal_generator.{h,cc}` (NEW - Oct 3) ✨ +- ✅ `src/cli/service/resource_context_builder.{h,cc}` (NEW - Oct 3) ✨ +- ✅ `src/app/zelda3/overworld/overworld.h` (UPDATED - SetTile method) ✨ +- ✅ `src/cli/handlers/overworld.cc` (UPDATED - SetTile implementation) ✨ +- ✅ `docs/z3ed/IMPLEMENTATION-SESSION-OCT3-CONTINUED.md` (NEW) ✨ +- ✅ `docs/z3ed/AGENTIC-PLAN-STATUS.md` (UPDATED - this file) ### Previously Implemented (Phase 1-4) - ✅ `src/cli/service/ollama_ai_service.{h,cc}` diff --git a/src/app/zelda3/overworld/overworld.h b/src/app/zelda3/overworld/overworld.h index d4207553..c1897595 100644 --- a/src/app/zelda3/overworld/overworld.h +++ b/src/app/zelda3/overworld/overworld.h @@ -287,6 +287,15 @@ class Overworld { return map_tiles_.special_world[y][x]; } } + void SetTile(int x, int y, uint16_t tile_id) { + if (current_world_ == 0) { + map_tiles_.light_world[y][x] = tile_id; + } else if (current_world_ == 1) { + map_tiles_.dark_world[y][x] = tile_id; + } else { + map_tiles_.special_world[y][x] = tile_id; + } + } auto map_tiles() const { return map_tiles_; } auto mutable_map_tiles() { return &map_tiles_; } auto all_items() const { return all_items_; } diff --git a/src/cli/handlers/overworld.cc b/src/cli/handlers/overworld.cc index 9fcaf613..286ae0f8 100644 --- a/src/cli/handlers/overworld.cc +++ b/src/cli/handlers/overworld.cc @@ -23,13 +23,19 @@ absl::Status OverworldGetTile::Run(const std::vector& arg_vec) { return absl::InvalidArgumentError("ROM file must be provided via --rom flag."); } - rom_.LoadFromFile(rom_file); + auto load_status = rom_.LoadFromFile(rom_file); + if (!load_status.ok()) { + return load_status; + } if (!rom_.is_loaded()) { return absl::AbortedError("Failed to load ROM."); } zelda3::Overworld overworld(&rom_); - overworld.Load(&rom_); + auto ow_status = overworld.Load(&rom_); + if (!ow_status.ok()) { + return ow_status; + } uint16_t tile = overworld.GetTile(x, y); @@ -54,21 +60,40 @@ absl::Status OverworldSetTile::Run(const std::vector& arg_vec) { return absl::InvalidArgumentError("ROM file must be provided via --rom flag."); } - rom_.LoadFromFile(rom_file); + auto load_status = rom_.LoadFromFile(rom_file); + if (!load_status.ok()) { + return load_status; + } if (!rom_.is_loaded()) { return absl::AbortedError("Failed to load ROM."); } zelda3::Overworld overworld(&rom_); - overworld.Load(&rom_); + auto status = overworld.Load(&rom_); + if (!status.ok()) { + return status; + } - // TODO: Implement the actual set_tile method in Overworld class - // overworld.SetTile(x, y, tile_id); + // Set the world based on map_id + if (map_id < 0x40) { + overworld.set_current_world(0); // Light World + } else if (map_id < 0x80) { + overworld.set_current_world(1); // Dark World + } else { + overworld.set_current_world(2); // Special World + } - // rom_.SaveToFile({.filename = rom_file}); + // Set the tile + overworld.SetTile(x, y, static_cast(tile_id)); - std::cout << "Set tile at (" << x << ", " << y << ") on map " << map_id << " to: 0x" << std::hex << tile_id << std::endl; - std::cout << "(Not actually implemented yet)" << std::endl; + // Save the ROM + auto save_status = rom_.SaveToFile({.filename = rom_file}); + if (!save_status.ok()) { + return save_status; + } + + std::cout << "✅ Set tile at (" << x << ", " << y << ") on map " << map_id + << " to: 0x" << std::hex << tile_id << std::dec << std::endl; return absl::OkStatus(); } diff --git a/src/cli/service/resource_context_builder.cc b/src/cli/service/resource_context_builder.cc new file mode 100644 index 00000000..866b38b6 --- /dev/null +++ b/src/cli/service/resource_context_builder.cc @@ -0,0 +1,262 @@ +#include "cli/service/resource_context_builder.h" + +#include + +namespace yaze { +namespace cli { + +std::string ResourceContextBuilder::GetCommonTile16Reference() { + std::ostringstream oss; + oss << "Common Tile16s:\n"; + oss << " - 0x020: Grass\n"; + oss << " - 0x022: Dirt\n"; + oss << " - 0x02E: Tree\n"; + oss << " - 0x003: Bush\n"; + oss << " - 0x004: Rock\n"; + oss << " - 0x021: Flower\n"; + oss << " - 0x023: Sand\n"; + oss << " - 0x14C: Water (top edge)\n"; + oss << " - 0x14D: Water (middle)\n"; + oss << " - 0x14E: Water (bottom edge)\n"; + return oss.str(); +} + +std::string ResourceContextBuilder::ExtractOverworldLabels() { + if (!rom_ || !rom_->is_loaded()) { + return "Overworld Maps: (ROM not loaded)\n"; + } + + auto* label_mgr = rom_->resource_label(); + if (!label_mgr || !label_mgr->labels_loaded_) { + return "Overworld Maps: (No labels file loaded)\n"; + } + + std::ostringstream oss; + oss << "Overworld Maps:\n"; + + // Check if "overworld" labels exist + auto it = label_mgr->labels_.find("overworld"); + if (it != label_mgr->labels_.end()) { + for (const auto& [key, value] : it->second) { + oss << " - " << key << ": \"" << value << "\"\n"; + } + } else { + // Provide defaults + oss << " - 0: \"Light World\"\n"; + oss << " - 1: \"Dark World\"\n"; + oss << " - 3: \"Desert\"\n"; + } + + return oss.str(); +} + +std::string ResourceContextBuilder::ExtractDungeonLabels() { + if (!rom_ || !rom_->is_loaded()) { + return "Dungeons: (ROM not loaded)\n"; + } + + auto* label_mgr = rom_->resource_label(); + if (!label_mgr || !label_mgr->labels_loaded_) { + return "Dungeons: (No labels file loaded)\n"; + } + + std::ostringstream oss; + oss << "Dungeons:\n"; + + // Check if "dungeon" labels exist + auto it = label_mgr->labels_.find("dungeon"); + if (it != label_mgr->labels_.end()) { + for (const auto& [key, value] : it->second) { + oss << " - " << key << ": \"" << value << "\"\n"; + } + } else { + // Provide vanilla defaults + oss << " - 0x00: \"Hyrule Castle\"\n"; + oss << " - 0x02: \"Eastern Palace\"\n"; + oss << " - 0x04: \"Desert Palace\"\n"; + oss << " - 0x06: \"Tower of Hera\"\n"; + } + + return oss.str(); +} + +std::string ResourceContextBuilder::ExtractEntranceLabels() { + if (!rom_ || !rom_->is_loaded()) { + return "Entrances: (ROM not loaded)\n"; + } + + auto* label_mgr = rom_->resource_label(); + if (!label_mgr || !label_mgr->labels_loaded_) { + return "Entrances: (No labels file loaded)\n"; + } + + std::ostringstream oss; + oss << "Entrances:\n"; + + // Check if "entrance" labels exist + auto it = label_mgr->labels_.find("entrance"); + if (it != label_mgr->labels_.end()) { + for (const auto& [key, value] : it->second) { + oss << " - " << key << ": \"" << value << "\"\n"; + } + } else { + // Provide vanilla defaults + oss << " - 0x00: \"Link's House\"\n"; + oss << " - 0x01: \"Sanctuary\"\n"; + } + + return oss.str(); +} + +std::string ResourceContextBuilder::ExtractRoomLabels() { + if (!rom_ || !rom_->is_loaded()) { + return "Rooms: (ROM not loaded)\n"; + } + + auto* label_mgr = rom_->resource_label(); + if (!label_mgr || !label_mgr->labels_loaded_) { + return "Rooms: (No labels file loaded)\n"; + } + + std::ostringstream oss; + oss << "Rooms:\n"; + + // Check if "room" labels exist + auto it = label_mgr->labels_.find("room"); + if (it != label_mgr->labels_.end()) { + for (const auto& [key, value] : it->second) { + oss << " - " << key << ": \"" << value << "\"\n"; + } + } else { + oss << " (No room labels defined)\n"; + } + + return oss.str(); +} + +std::string ResourceContextBuilder::ExtractSpriteLabels() { + if (!rom_ || !rom_->is_loaded()) { + return "Sprites: (ROM not loaded)\n"; + } + + auto* label_mgr = rom_->resource_label(); + if (!label_mgr || !label_mgr->labels_loaded_) { + return "Sprites: (No labels file loaded)\n"; + } + + std::ostringstream oss; + oss << "Sprites:\n"; + + // Check if "sprite" labels exist + auto it = label_mgr->labels_.find("sprite"); + if (it != label_mgr->labels_.end()) { + for (const auto& [key, value] : it->second) { + oss << " - " << key << ": \"" << value << "\"\n"; + } + } else { + // Provide vanilla defaults + oss << " - 0x00: \"Soldier\"\n"; + oss << " - 0x01: \"Octorok\"\n"; + } + + return oss.str(); +} + +absl::StatusOr ResourceContextBuilder::BuildResourceContext() { + if (!rom_) { + return absl::InvalidArgumentError("ROM pointer is null"); + } + + std::ostringstream context; + + context << "=== AVAILABLE RESOURCES ===\n\n"; + + // Add overworld maps + context << ExtractOverworldLabels() << "\n"; + + // Add dungeons + context << ExtractDungeonLabels() << "\n"; + + // Add entrances + context << ExtractEntranceLabels() << "\n"; + + // Add rooms (if any) + context << ExtractRoomLabels() << "\n"; + + // Add sprites + context << ExtractSpriteLabels() << "\n"; + + // Add common tile16 reference + context << GetCommonTile16Reference() << "\n"; + + context << "=== INSTRUCTIONS ===\n"; + context << "1. Use the resource labels when they're available\n"; + context << "2. If a user refers to a custom name, check the labels above\n"; + context << "3. Always provide tile16 IDs as hex values (0x###)\n"; + context << "4. Explain which resources you're using in your reasoning\n"; + + return context.str(); +} + +absl::StatusOr> +ResourceContextBuilder::GetLabels(const std::string& category) { + if (!rom_ || !rom_->is_loaded()) { + return absl::FailedPreconditionError("ROM not loaded"); + } + + auto* label_mgr = rom_->resource_label(); + if (!label_mgr || !label_mgr->labels_loaded_) { + return absl::FailedPreconditionError("No labels file loaded"); + } + + std::map result; + + auto it = label_mgr->labels_.find(category); + if (it != label_mgr->labels_.end()) { + for (const auto& [key, value] : it->second) { + result[key] = value; + } + } + + return result; +} + +absl::StatusOr ResourceContextBuilder::ExportToJson() { + if (!rom_ || !rom_->is_loaded()) { + return absl::InvalidArgumentError("ROM not loaded"); + } + + auto* label_mgr = rom_->resource_label(); + if (!label_mgr || !label_mgr->labels_loaded_) { + return absl::InvalidArgumentError("No labels file loaded"); + } + + std::ostringstream json; + json << "{\n"; + + bool first_category = true; + for (const auto& [category, labels] : label_mgr->labels_) { + if (!first_category) json << ",\n"; + first_category = false; + + json << " \"" << category << "\": {\n"; + + bool first_label = true; + for (const auto& [key, value] : labels) { + if (!first_label) json << ",\n"; + first_label = false; + + json << " \"" << key << "\": \"" << value << "\""; + } + + json << "\n }"; + } + + json << "\n}\n"; + + return json.str(); +} + +} // namespace cli +} // namespace yaze + diff --git a/src/cli/service/resource_context_builder.h b/src/cli/service/resource_context_builder.h new file mode 100644 index 00000000..ceb0fd3a --- /dev/null +++ b/src/cli/service/resource_context_builder.h @@ -0,0 +1,163 @@ +#ifndef YAZE_CLI_SERVICE_RESOURCE_CONTEXT_BUILDER_H_ +#define YAZE_CLI_SERVICE_RESOURCE_CONTEXT_BUILDER_H_ + +#include +#include + +#include "absl/status/statusor.h" +#include "app/rom.h" + +namespace yaze { +namespace cli { + +/** + * @brief Builds contextual information from ROM resources for AI prompts. + * + * This class extracts user-defined labels from the ROM's ResourceLabelManager + * and formats them into human-readable context that can be injected into + * AI prompts. This enables AI to use meaningful names like "eastern_palace" + * instead of opaque IDs like "0x02". + * + * Example usage: + * ResourceContextBuilder builder(rom); + * std::string context = builder.BuildResourceContext().value(); + * // Context contains formatted labels for all resource types + */ +class ResourceContextBuilder { + public: + explicit ResourceContextBuilder(Rom* rom) : rom_(rom) {} + + /** + * @brief Build a complete resource context string for AI prompts. + * + * Extracts all ResourceLabels from the ROM and formats them into + * a structured text format suitable for AI consumption. + * + * Example output: + * ``` + * === AVAILABLE RESOURCES === + * + * Overworld Maps: + * - 0: "Light World" (user: "hyrule_overworld") + * - 1: "Dark World" (user: "dark_world") + * + * Dungeons: + * - 0x00: "Hyrule Castle" (user: "castle") + * - 0x02: "Eastern Palace" (user: "east_palace") + * + * Common Tile16s: + * - 0x020: Grass + * - 0x02E: Tree + * - 0x14C: Water (top) + * ``` + * + * @return Formatted resource context string + */ + absl::StatusOr BuildResourceContext(); + + /** + * @brief Get labels for a specific resource category. + * + * @param category Resource type ("overworld", "dungeon", "entrance", etc.) + * @return Map of ID -> label for that category + */ + absl::StatusOr> GetLabels( + const std::string& category); + + /** + * @brief Export all labels to JSON format. + * + * Creates a structured JSON representation of all resources + * for potential use by AI services. + * + * @return JSON string with all resource labels + */ + absl::StatusOr ExportToJson(); + + private: + Rom* rom_; + + /** + * @brief Extract overworld map labels. + * + * Returns formatted string like: + * ``` + * Overworld Maps: + * - 0: "Light World" (user: "hyrule_overworld") + * - 1: "Dark World" (user: "dark_world") + * ``` + */ + std::string ExtractOverworldLabels(); + + /** + * @brief Extract dungeon labels. + * + * Returns formatted string like: + * ``` + * Dungeons: + * - 0x00: "Hyrule Castle" (user: "castle") + * - 0x02: "Eastern Palace" (user: "east_palace") + * ``` + */ + std::string ExtractDungeonLabels(); + + /** + * @brief Extract entrance labels. + * + * Returns formatted string like: + * ``` + * Entrances: + * - 0x00: "Link's House" (user: "starting_house") + * - 0x01: "Sanctuary" (user: "church") + * ``` + */ + std::string ExtractEntranceLabels(); + + /** + * @brief Extract room labels. + * + * Returns formatted string like: + * ``` + * Rooms: + * - 0x00_0x10: "Eastern Palace Boss Room" + * - 0x04_0x05: "Desert Palace Treasure Room" + * ``` + */ + std::string ExtractRoomLabels(); + + /** + * @brief Extract sprite labels. + * + * Returns formatted string like: + * ``` + * Sprites: + * - 0x00: "Soldier" (user: "green_soldier") + * - 0x01: "Octorok" (user: "red_octorok") + * ``` + */ + std::string ExtractSpriteLabels(); + + /** + * @brief Add common tile16 reference for AI. + * + * Provides a quick reference of common tile16 IDs that AI + * can use without needing to search through the entire tileset. + * + * Returns formatted string like: + * ``` + * Common Tile16s: + * - 0x020: Grass + * - 0x022: Dirt + * - 0x02E: Tree + * - 0x14C: Water (top edge) + * - 0x14D: Water (middle) + * ``` + */ + std::string GetCommonTile16Reference(); +}; + +} // namespace cli +} // namespace yaze + +#endif // YAZE_CLI_SERVICE_RESOURCE_CONTEXT_BUILDER_H_ + diff --git a/src/cli/service/tile16_proposal_generator.cc b/src/cli/service/tile16_proposal_generator.cc new file mode 100644 index 00000000..3d9ee8ab --- /dev/null +++ b/src/cli/service/tile16_proposal_generator.cc @@ -0,0 +1,263 @@ +#include "cli/service/tile16_proposal_generator.h" + +#include +#include + +#include "absl/strings/match.h" +#include "absl/strings/str_split.h" +#include "absl/strings/str_cat.h" +#include "app/zelda3/overworld/overworld.h" + +namespace yaze { +namespace cli { + +std::string Tile16Change::ToString() const { + std::ostringstream oss; + oss << "Map " << map_id << " @ (" << x << "," << y << "): " + << "0x" << std::hex << old_tile << " → 0x" << new_tile; + return oss.str(); +} + +std::string Tile16Proposal::ToJson() const { + std::ostringstream json; + json << "{\n"; + json << " \"id\": \"" << id << "\",\n"; + json << " \"prompt\": \"" << prompt << "\",\n"; + json << " \"ai_service\": \"" << ai_service << "\",\n"; + json << " \"reasoning\": \"" << reasoning << "\",\n"; + json << " \"status\": "; + + switch (status) { + case Status::PENDING: json << "\"pending\""; break; + case Status::ACCEPTED: json << "\"accepted\""; break; + case Status::REJECTED: json << "\"rejected\""; break; + case Status::APPLIED: json << "\"applied\""; break; + } + json << ",\n"; + + json << " \"changes\": [\n"; + for (size_t i = 0; i < changes.size(); ++i) { + const auto& change = changes[i]; + json << " {\n"; + json << " \"map_id\": " << change.map_id << ",\n"; + json << " \"x\": " << change.x << ",\n"; + json << " \"y\": " << change.y << ",\n"; + json << " \"old_tile\": \"0x" << std::hex << change.old_tile << "\",\n"; + json << " \"new_tile\": \"0x" << std::hex << change.new_tile << "\"\n"; + json << " }"; + if (i < changes.size() - 1) json << ","; + json << "\n"; + } + json << " ]\n"; + json << "}\n"; + + return json.str(); +} + +absl::StatusOr Tile16Proposal::FromJson(const std::string& /* json */) { + // TODO: Implement JSON parsing using nlohmann/json when available + return absl::UnimplementedError("JSON parsing not yet implemented"); +} + +std::string Tile16ProposalGenerator::GenerateProposalId() const { + // Generate a simple timestamp-based ID + auto now = std::chrono::system_clock::now(); + auto ms = std::chrono::duration_cast( + now.time_since_epoch()).count(); + + std::ostringstream oss; + oss << "proposal_" << ms; + return oss.str(); +} + +absl::StatusOr Tile16ProposalGenerator::ParseSetTileCommand( + const std::string& command, + Rom* rom) { + + // Expected format: "overworld set-tile --map 0 --x 10 --y 20 --tile 0x02E" + std::vector parts = absl::StrSplit(command, ' '); + + if (parts.size() < 10) { + return absl::InvalidArgumentError( + absl::StrCat("Invalid command format: ", command)); + } + + if (parts[0] != "overworld" || parts[1] != "set-tile") { + return absl::InvalidArgumentError( + absl::StrCat("Not a set-tile command: ", command)); + } + + Tile16Change change; + + // Parse arguments + for (size_t i = 2; i < parts.size(); i += 2) { + if (i + 1 >= parts.size()) break; + + const std::string& flag = parts[i]; + const std::string& value = parts[i + 1]; + + if (flag == "--map") { + change.map_id = std::stoi(value); + } else if (flag == "--x") { + change.x = std::stoi(value); + } else if (flag == "--y") { + change.y = std::stoi(value); + } else if (flag == "--tile") { + // Parse as hex (both 0x prefix and plain hex) + change.new_tile = static_cast(std::stoi(value, nullptr, 16)); + } + } + + // Load the ROM to get the old tile value + if (rom && rom->is_loaded()) { + zelda3::Overworld overworld(rom); + auto status = overworld.Load(rom); + if (!status.ok()) { + return status; + } + + // Set the correct world based on map_id + if (change.map_id < 0x40) { + overworld.set_current_world(0); // Light World + } else if (change.map_id < 0x80) { + overworld.set_current_world(1); // Dark World + } else { + overworld.set_current_world(2); // Special World + } + + change.old_tile = overworld.GetTile(change.x, change.y); + } else { + change.old_tile = 0x0000; // Unknown + } + + return change; +} + +absl::StatusOr Tile16ProposalGenerator::GenerateFromCommands( + const std::string& prompt, + const std::vector& commands, + const std::string& ai_service, + Rom* rom) { + + Tile16Proposal proposal; + proposal.id = GenerateProposalId(); + proposal.prompt = prompt; + proposal.ai_service = ai_service; + proposal.created_at = std::chrono::system_clock::now(); + proposal.status = Tile16Proposal::Status::PENDING; + + // Parse each command + for (const auto& command : commands) { + // Skip empty commands or comments + if (command.empty() || command[0] == '#') { + continue; + } + + // Check if it's a set-tile command + if (absl::StrContains(command, "overworld set-tile")) { + auto change_or = ParseSetTileCommand(command, rom); + if (change_or.ok()) { + proposal.changes.push_back(change_or.value()); + } else { + return change_or.status(); + } + } + // TODO: Add support for other command types (set-area, replace-tile, etc.) + } + + if (proposal.changes.empty()) { + return absl::InvalidArgumentError( + "No valid tile16 changes found in commands"); + } + + proposal.reasoning = absl::StrCat( + "Generated ", proposal.changes.size(), " tile16 changes from prompt"); + + return proposal; +} + +absl::Status Tile16ProposalGenerator::ApplyProposal( + const Tile16Proposal& proposal, + Rom* rom) { + + if (!rom || !rom->is_loaded()) { + return absl::FailedPreconditionError("ROM not loaded"); + } + + zelda3::Overworld overworld(rom); + auto status = overworld.Load(rom); + if (!status.ok()) { + return status; + } + + // Apply each change + for (const auto& change : proposal.changes) { + // Set the correct world + if (change.map_id < 0x40) { + overworld.set_current_world(0); // Light World + } else if (change.map_id < 0x80) { + overworld.set_current_world(1); // Dark World + } else { + overworld.set_current_world(2); // Special World + } + + // Apply the tile change + overworld.SetTile(change.x, change.y, change.new_tile); + } + + // Note: We don't save to disk here - that's the caller's responsibility + // This allows for sandbox testing before committing + + return absl::OkStatus(); +} + +absl::StatusOr Tile16ProposalGenerator::GenerateDiff( + const Tile16Proposal& /* proposal */, + Rom* /* before_rom */, + Rom* /* after_rom */) { + + // TODO: Implement visual diff generation + // This would: + // 1. Load overworld from both ROMs + // 2. Render the affected regions + // 3. Create side-by-side or overlay comparison + // 4. Highlight changed tiles + + return absl::UnimplementedError("Visual diff generation not yet implemented"); +} + +absl::Status Tile16ProposalGenerator::SaveProposal( + const Tile16Proposal& proposal, + const std::string& path) { + + std::ofstream file(path); + if (!file.is_open()) { + return absl::InvalidArgumentError( + absl::StrCat("Failed to open file for writing: ", path)); + } + + file << proposal.ToJson(); + file.close(); + + return absl::OkStatus(); +} + +absl::StatusOr Tile16ProposalGenerator::LoadProposal( + const std::string& path) { + + std::ifstream file(path); + if (!file.is_open()) { + return absl::InvalidArgumentError( + absl::StrCat("Failed to open file for reading: ", path)); + } + + std::stringstream buffer; + buffer << file.rdbuf(); + file.close(); + + return Tile16Proposal::FromJson(buffer.str()); +} + +} // namespace cli +} // namespace yaze + diff --git a/src/cli/service/tile16_proposal_generator.h b/src/cli/service/tile16_proposal_generator.h new file mode 100644 index 00000000..64b2034a --- /dev/null +++ b/src/cli/service/tile16_proposal_generator.h @@ -0,0 +1,147 @@ +#ifndef YAZE_CLI_SERVICE_TILE16_PROPOSAL_GENERATOR_H_ +#define YAZE_CLI_SERVICE_TILE16_PROPOSAL_GENERATOR_H_ + +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "app/gfx/bitmap.h" +#include "app/rom.h" + +namespace yaze { +namespace cli { + +/** + * @brief Represents a single tile16 change in a proposal. + */ +struct Tile16Change { + int map_id; // Overworld map ID + int x; // Tile16 X coordinate + int y; // Tile16 Y coordinate + uint16_t old_tile; // Original tile16 ID (for rollback) + uint16_t new_tile; // New tile16 ID to apply + + std::string ToString() const; +}; + +/** + * @brief Represents a proposal for tile16 edits on the overworld. + * + * This is the core data structure for the accept/reject workflow. + * AI generates proposals, which are then applied to a sandbox ROM + * for preview before being committed to the main ROM. + */ +struct Tile16Proposal { + std::string id; // Unique proposal ID (UUID-like) + std::string prompt; // Original user prompt + std::vector changes; // List of tile changes + std::string reasoning; // AI's explanation + std::string ai_service; // "gemini", "ollama", "mock" + std::chrono::system_clock::time_point created_at; // Timestamp + + // Proposal state + enum class Status { + PENDING, // Generated but not reviewed + ACCEPTED, // User accepted, changes applied + REJECTED, // User rejected, changes discarded + APPLIED // Successfully applied to ROM + }; + Status status = Status::PENDING; + + std::string ToJson() const; + static absl::StatusOr FromJson(const std::string& json); +}; + +/** + * @brief Generates and manages tile16 editing proposals. + * + * This class bridges the AI service with the overworld editing system, + * providing a safe sandbox workflow for reviewing and applying changes. + */ +class Tile16ProposalGenerator { + public: + Tile16ProposalGenerator() = default; + + /** + * @brief Generate a tile16 proposal from an AI-generated command list. + * + * @param prompt The original user prompt + * @param commands List of commands from AI (e.g., "overworld set-tile ...") + * @param ai_service Name of the AI service used + * @param rom Reference ROM for validation + * @return Tile16Proposal with parsed changes + */ + absl::StatusOr GenerateFromCommands( + const std::string& prompt, + const std::vector& commands, + const std::string& ai_service, + Rom* rom); + + /** + * @brief Apply a proposal to a ROM (typically a sandbox). + * + * This modifies the ROM in memory but doesn't save to disk. + * Used for preview and testing. + * + * @param proposal The proposal to apply + * @param rom The ROM to modify + * @return Status indicating success or failure + */ + absl::Status ApplyProposal(const Tile16Proposal& proposal, Rom* rom); + + /** + * @brief Generate a visual diff bitmap for a proposal. + * + * Creates a side-by-side or overlay comparison of before/after state. + * + * @param proposal The proposal to visualize + * @param before_rom ROM in original state + * @param after_rom ROM with proposal applied + * @return Bitmap showing the visual difference + */ + absl::StatusOr GenerateDiff( + const Tile16Proposal& proposal, + Rom* before_rom, + Rom* after_rom); + + /** + * @brief Save a proposal to a JSON file for later review. + * + * @param proposal The proposal to save + * @param path File path to save to + * @return Status indicating success or failure + */ + absl::Status SaveProposal(const Tile16Proposal& proposal, + const std::string& path); + + /** + * @brief Load a proposal from a JSON file. + * + * @param path File path to load from + * @return The loaded proposal or error + */ + absl::StatusOr LoadProposal(const std::string& path); + + private: + /** + * @brief Parse a single "overworld set-tile" command into a Tile16Change. + * + * Expected format: "overworld set-tile --map 0 --x 10 --y 20 --tile 0x02E" + */ + absl::StatusOr ParseSetTileCommand( + const std::string& command, + Rom* rom); + + /** + * @brief Generate a unique proposal ID. + */ + std::string GenerateProposalId() const; +}; + +} // namespace cli +} // namespace yaze + +#endif // YAZE_CLI_SERVICE_TILE16_PROPOSAL_GENERATOR_H_ + diff --git a/src/cli/z3ed.cmake b/src/cli/z3ed.cmake index 195a7f75..7218d5ed 100644 --- a/src/cli/z3ed.cmake +++ b/src/cli/z3ed.cmake @@ -62,6 +62,10 @@ add_executable( cli/service/test_suite_writer.cc cli/service/test_suite_writer.h cli/service/gemini_ai_service.cc + cli/service/tile16_proposal_generator.h + cli/service/tile16_proposal_generator.cc + cli/service/resource_context_builder.h + cli/service/resource_context_builder.cc app/rom.cc app/core/project.cc app/core/asar_wrapper.cc @@ -84,26 +88,33 @@ if(YAZE_WITH_JSON) endif() # ============================================================================ -# SSL/HTTPS Support (Required for Gemini API and future collaborative features) +# SSL/HTTPS Support (Optional - Required for Gemini API and collaborative features) # ============================================================================ -option(YAZE_WITH_SSL "Build with OpenSSL support for HTTPS" ON) -if(YAZE_WITH_SSL OR YAZE_WITH_JSON) - # Find OpenSSL on the system - find_package(OpenSSL REQUIRED) +# SSL is only enabled when building with gRPC+JSON (the full agent/testing suite) +# This ensures Windows builds without these dependencies still work +if(YAZE_WITH_GRPC AND YAZE_WITH_JSON) + find_package(OpenSSL) - # Define the SSL support macro for httplib - target_compile_definitions(z3ed PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT) - - # Link OpenSSL libraries - target_link_libraries(z3ed PRIVATE OpenSSL::SSL OpenSSL::Crypto) - - # On macOS, also enable Keychain cert support - if(APPLE) - target_compile_definitions(z3ed PRIVATE CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) - target_link_libraries(z3ed PRIVATE "-framework CoreFoundation" "-framework Security") + if(OpenSSL_FOUND) + # Define the SSL support macro for httplib + target_compile_definitions(z3ed PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT) + + # Link OpenSSL libraries + target_link_libraries(z3ed PRIVATE OpenSSL::SSL OpenSSL::Crypto) + + # On macOS, also enable Keychain cert support + if(APPLE) + target_compile_definitions(z3ed PRIVATE CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) + target_link_libraries(z3ed PRIVATE "-framework CoreFoundation" "-framework Security") + endif() + + message(STATUS "✓ SSL/HTTPS support enabled for z3ed (required for Gemini API)") + else() + message(WARNING "OpenSSL not found - Gemini API will not work (Ollama will still function)") + message(STATUS " Install OpenSSL to enable Gemini: brew install openssl (macOS) or apt-get install libssl-dev (Linux)") endif() - - message(STATUS "✓ SSL/HTTPS support enabled for z3ed") +else() + message(STATUS "Building z3ed without gRPC/JSON - AI agent features disabled") endif() target_include_directories(