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.
This commit is contained in:
scawful
2025-10-03 09:35:49 -04:00
parent 3473d37be4
commit b89dcca93f
8 changed files with 933 additions and 51 deletions

View File

@@ -253,33 +253,31 @@ unset GEMINI_API_KEY
## 🚀 Next Steps ## 🚀 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 ```bash
ollama serve ollama serve
ollama pull qwen2.5-coder:7b ./build-grpc-test/bin/z3ed agent plan \
./build-grpc-test/bin/z3ed agent plan --prompt "test" --prompt "Create a 3x3 water pond at 15, 10"
# Verify proposal generation
# Verify tile16 changes are correct
``` ```
2. **Test Gemini Integration** (30 min) 4. **Add Visual Diff Implementation** (2-3 hours)
```bash - Render tile16 bitmaps from overworld
export GEMINI_API_KEY="your-key" - Create side-by-side comparison images
./build-grpc-test/bin/z3ed agent plan --prompt "test" - Highlight changed tiles
```
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
### Short-Term (This Week) ### Short-Term (This Week)
@@ -351,13 +349,17 @@ unset GEMINI_API_KEY
## 📝 Files Summary ## 📝 Files Summary
### Created/Modified in This Session ### Created/Modified Recently
- ✅ `src/cli/handlers/agent/test_common.{h,cc}` (NEW) - ✅ `src/cli/handlers/agent/test_common.{h,cc}` (NEW)
- ✅ `src/cli/handlers/agent/test_commands.cc` (REBUILT) - ✅ `src/cli/handlers/agent/test_commands.cc` (REBUILT)
- ✅ `src/cli/z3ed.cmake` (UPDATED) - ✅ `src/cli/z3ed.cmake` (UPDATED)
- ✅ `src/cli/service/gemini_ai_service.cc` (FIXED includes) - ✅ `src/cli/service/gemini_ai_service.cc` (FIXED includes)
- ✅ `docs/z3ed/BUILD-FIX-COMPLETED.md` (NEW) - ✅ `src/cli/service/tile16_proposal_generator.{h,cc}` (NEW - Oct 3) ✨
- ✅ `docs/z3ed/AGENTIC-PLAN-STATUS.md` (NEW - this file) - ✅ `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) ### Previously Implemented (Phase 1-4)
- ✅ `src/cli/service/ollama_ai_service.{h,cc}` - ✅ `src/cli/service/ollama_ai_service.{h,cc}`

View File

@@ -287,6 +287,15 @@ class Overworld {
return map_tiles_.special_world[y][x]; 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 map_tiles() const { return map_tiles_; }
auto mutable_map_tiles() { return &map_tiles_; } auto mutable_map_tiles() { return &map_tiles_; }
auto all_items() const { return all_items_; } auto all_items() const { return all_items_; }

View File

@@ -23,13 +23,19 @@ absl::Status OverworldGetTile::Run(const std::vector<std::string>& arg_vec) {
return absl::InvalidArgumentError("ROM file must be provided via --rom flag."); 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()) { if (!rom_.is_loaded()) {
return absl::AbortedError("Failed to load ROM."); return absl::AbortedError("Failed to load ROM.");
} }
zelda3::Overworld overworld(&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); uint16_t tile = overworld.GetTile(x, y);
@@ -54,21 +60,40 @@ absl::Status OverworldSetTile::Run(const std::vector<std::string>& arg_vec) {
return absl::InvalidArgumentError("ROM file must be provided via --rom flag."); 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()) { if (!rom_.is_loaded()) {
return absl::AbortedError("Failed to load ROM."); return absl::AbortedError("Failed to load ROM.");
} }
zelda3::Overworld overworld(&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 // Set the world based on map_id
// overworld.SetTile(x, y, tile_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<uint16_t>(tile_id));
std::cout << "Set tile at (" << x << ", " << y << ") on map " << map_id << " to: 0x" << std::hex << tile_id << std::endl; // Save the ROM
std::cout << "(Not actually implemented yet)" << std::endl; 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(); return absl::OkStatus();
} }

View File

@@ -0,0 +1,262 @@
#include "cli/service/resource_context_builder.h"
#include <sstream>
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<std::string> 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<std::map<std::string, std::string>>
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<std::string, std::string> 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<std::string> 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

View File

@@ -0,0 +1,163 @@
#ifndef YAZE_CLI_SERVICE_RESOURCE_CONTEXT_BUILDER_H_
#define YAZE_CLI_SERVICE_RESOURCE_CONTEXT_BUILDER_H_
#include <map>
#include <string>
#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<std::string> 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<std::map<std::string, std::string>> 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<std::string> 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_

View File

@@ -0,0 +1,263 @@
#include "cli/service/tile16_proposal_generator.h"
#include <sstream>
#include <fstream>
#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> 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<std::chrono::milliseconds>(
now.time_since_epoch()).count();
std::ostringstream oss;
oss << "proposal_" << ms;
return oss.str();
}
absl::StatusOr<Tile16Change> Tile16ProposalGenerator::ParseSetTileCommand(
const std::string& command,
Rom* rom) {
// Expected format: "overworld set-tile --map 0 --x 10 --y 20 --tile 0x02E"
std::vector<std::string> 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<uint16_t>(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<Tile16Proposal> Tile16ProposalGenerator::GenerateFromCommands(
const std::string& prompt,
const std::vector<std::string>& 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<gfx::Bitmap> 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<Tile16Proposal> 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

View File

@@ -0,0 +1,147 @@
#ifndef YAZE_CLI_SERVICE_TILE16_PROPOSAL_GENERATOR_H_
#define YAZE_CLI_SERVICE_TILE16_PROPOSAL_GENERATOR_H_
#include <chrono>
#include <string>
#include <vector>
#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<Tile16Change> 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<Tile16Proposal> 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<Tile16Proposal> GenerateFromCommands(
const std::string& prompt,
const std::vector<std::string>& 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<gfx::Bitmap> 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<Tile16Proposal> 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<Tile16Change> 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_

View File

@@ -62,6 +62,10 @@ add_executable(
cli/service/test_suite_writer.cc cli/service/test_suite_writer.cc
cli/service/test_suite_writer.h cli/service/test_suite_writer.h
cli/service/gemini_ai_service.cc 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/rom.cc
app/core/project.cc app/core/project.cc
app/core/asar_wrapper.cc app/core/asar_wrapper.cc
@@ -84,26 +88,33 @@ if(YAZE_WITH_JSON)
endif() 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) # SSL is only enabled when building with gRPC+JSON (the full agent/testing suite)
if(YAZE_WITH_SSL OR YAZE_WITH_JSON) # This ensures Windows builds without these dependencies still work
# Find OpenSSL on the system if(YAZE_WITH_GRPC AND YAZE_WITH_JSON)
find_package(OpenSSL REQUIRED) find_package(OpenSSL)
# Define the SSL support macro for httplib if(OpenSSL_FOUND)
target_compile_definitions(z3ed PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT) # Define the SSL support macro for httplib
target_compile_definitions(z3ed PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT)
# Link OpenSSL libraries # Link OpenSSL libraries
target_link_libraries(z3ed PRIVATE OpenSSL::SSL OpenSSL::Crypto) target_link_libraries(z3ed PRIVATE OpenSSL::SSL OpenSSL::Crypto)
# On macOS, also enable Keychain cert support # On macOS, also enable Keychain cert support
if(APPLE) if(APPLE)
target_compile_definitions(z3ed PRIVATE CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) target_compile_definitions(z3ed PRIVATE CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN)
target_link_libraries(z3ed PRIVATE "-framework CoreFoundation" "-framework Security") 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() endif()
else()
message(STATUS "✓ SSL/HTTPS support enabled for z3ed") message(STATUS "Building z3ed without gRPC/JSON - AI agent features disabled")
endif() endif()
target_include_directories( target_include_directories(