yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
z3ed Command Abstraction Layer Guide

Created: October 11, 2025
Status: Implementation Complete

Overview

This guide documents the new command abstraction layer for z3ed CLI commands. The abstraction layer eliminates ~500+ lines of duplicated code across tool commands and provides a consistent, maintainable architecture for future command development.

Problem Statement

Before Abstraction

The original tool_commands.cc (1549 lines) had severe code duplication:

  1. ROM Loading: Every command had 20-30 lines of identical ROM loading logic
  2. Argument Parsing: Each command manually parsed --format, --rom, --type, etc.
  3. Output Formatting: JSON vs text formatting was duplicated across every command
  4. Label Initialization: Resource label loading was repeated in every handler
  5. Error Handling: Inconsistent error messages and validation patterns

Code Duplication Example

// Repeated in EVERY command (30+ times):
Rom rom_storage;
Rom* rom = nullptr;
if (rom_context != nullptr && rom_context->is_loaded()) {
rom = rom_context;
} else {
auto rom_or = LoadRomFromFlag();
if (!rom_or.ok()) {
return rom_or.status();
}
rom_storage = std::move(rom_or.value());
rom = &rom_storage;
}
// Initialize labels (repeated in every command that needs labels)
if (rom->resource_label()) {
if (!rom->resource_label()->labels_loaded_) {
core::YazeProject project;
project.use_embedded_labels = true;
auto labels_status = project.InitializeEmbeddedLabels();
// ... more boilerplate ...
}
}
// Manual argument parsing (repeated everywhere)
std::string format = "json";
for (size_t i = 0; i < arg_vec.size(); ++i) {
const std::string& token = arg_vec[i];
if (token == "--format") {
if (i + 1 >= arg_vec.size()) {
return absl::InvalidArgumentError("--format requires a value.");
}
format = arg_vec[++i];
} else if (absl::StartsWith(token, "--format=")) {
format = token.substr(9);
}
}
// Manual output formatting (repeated everywhere)
if (format == "json") {
std::cout << "{\n";
std::cout << " \"field\": \"value\",\n";
std::cout << "}\n";
} else {
std::cout << "Field: value\n";
}

Solution Architecture

Three-Layer Abstraction

  1. CommandContext - ROM loading, context management
  2. ArgumentParser - Unified argument parsing
  3. OutputFormatter - Consistent output formatting
  4. CommandHandler (Optional) - Base class for structured commands

File Structure

src/cli/service/resources/
├── command_context.h # Context management
├── command_context.cc
├── command_handler.h # Base handler class
├── command_handler.cc
└── (existing files...)
src/cli/handlers/agent/
├── tool_commands.cc # Original (to be refactored)
├── tool_commands_refactored.cc # Example refactored commands
└── (other handlers...)

Core Components

1. CommandContext

Encapsulates ROM loading and common context:

// Create context
CommandContext::Config config;
config.external_rom_context = rom_context; // Optional: use existing ROM
config.rom_path = "/path/to/rom.sfc"; // Optional: override ROM path
config.use_mock_rom = false; // Optional: use mock for testing
config.format = "json";
CommandContext context(config);
// Get ROM (auto-loads if needed)
ASSIGN_OR_RETURN(Rom* rom, context.GetRom());
// Ensure labels loaded
RETURN_IF_ERROR(context.EnsureLabelsLoaded(rom));
#define RETURN_IF_ERROR(expression)
Definition macro.h:53
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:61

Benefits:

  • Single location for ROM loading logic
  • Automatic error handling
  • Mock ROM support for testing
  • Label management abstraction

2. ArgumentParser

Unified argument parsing with type safety:

ArgumentParser parser(arg_vec);
// String arguments
auto type = parser.GetString("type"); // Returns std::optional<string>
auto format = parser.GetString("format").value_or("json");
// Integer arguments (supports hex with 0x prefix)
ASSIGN_OR_RETURN(int room_id, parser.GetInt("room"));
// Hex-only arguments
ASSIGN_OR_RETURN(int tile_id, parser.GetHex("tile"));
// Flags
if (parser.HasFlag("verbose")) {
// ...
}
// Validation
RETURN_IF_ERROR(parser.RequireArgs({"type", "query"}));

Benefits:

  • Consistent argument parsing across all commands
  • Type-safe with proper error handling
  • Supports both --arg=value and --arg value forms
  • Built-in hex parsing for ROM addresses

3. OutputFormatter

Consistent JSON/text output:

ASSIGN_OR_RETURN(auto formatter, OutputFormatter::FromString("json"));
formatter.BeginObject("Room Information");
formatter.AddField("room_id", "0x12");
formatter.AddHexField("address", 0x1234, 4); // Formats as "0x1234"
formatter.AddField("sprite_count", 5);
formatter.BeginArray("sprites");
formatter.AddArrayItem("Sprite 1");
formatter.AddArrayItem("Sprite 2");
formatter.EndArray();
formatter.EndObject();
formatter.Print();

Output (JSON):

{
"room_id": "0x12",
"address": "0x1234",
"sprite_count": 5,
"sprites": [
"Sprite 1",
"Sprite 2"
]
}

Output (Text):

=== Room Information ===
room_id : 0x12
address : 0x1234
sprite_count : 5
sprites:
- Sprite 1
- Sprite 2

Benefits:

  • No manual JSON escaping
  • Consistent formatting rules
  • Easy to switch between JSON and text
  • Proper indentation handling

4. CommandHandler (Optional Base Class)

For more complex commands, use the base class pattern:

class MyCommandHandler : public CommandHandler {
protected:
std::string GetUsage() const override {
return "agent my-command --required <value> [--format <json|text>]";
}
absl::Status ValidateArgs(const ArgumentParser& parser) override {
return parser.RequireArgs({"required"});
}
absl::Status Execute(Rom* rom, const ArgumentParser& parser,
OutputFormatter& formatter) override {
auto value = parser.GetString("required").value();
// Business logic here
formatter.AddField("result", value);
return absl::OkStatus();
}
bool RequiresLabels() const override { return true; }
};
// Usage:
absl::Status HandleMyCommand(const std::vector<std::string>& args, Rom* rom) {
MyCommandHandler handler;
return handler.Run(args, rom);
}

Benefits:

  • Enforces consistent structure
  • Automatic context setup and teardown
  • Built-in error handling
  • Easy to test individual components

Migration Guide

Step-by-Step Refactoring

Before (80 lines):

absl::Status HandleResourceListCommand(
const std::vector<std::string>& arg_vec, Rom* rom_context) {
std::string type;
std::string format = "table";
// Manual argument parsing (20 lines)
for (size_t i = 0; i < arg_vec.size(); ++i) {
const std::string& token = arg_vec[i];
if (token == "--type") {
if (i + 1 >= arg_vec.size()) {
return absl::InvalidArgumentError("--type requires a value.");
}
type = arg_vec[++i];
} else if (absl::StartsWith(token, "--type=")) {
type = token.substr(7);
}
// ... repeat for --format ...
}
if (type.empty()) {
return absl::InvalidArgumentError("Usage: ...");
}
// ROM loading (30 lines)
Rom rom_storage;
Rom* rom = nullptr;
if (rom_context != nullptr && rom_context->is_loaded()) {
rom = rom_context;
} else {
auto rom_or = LoadRomFromFlag();
if (!rom_or.ok()) {
return rom_or.status();
}
rom_storage = std::move(rom_or.value());
rom = &rom_storage;
}
// Label initialization (15 lines)
if (rom->resource_label()) {
if (!rom->resource_label()->labels_loaded_) {
core::YazeProject project;
project.use_embedded_labels = true;
auto labels_status = project.InitializeEmbeddedLabels();
if (labels_status.ok()) {
rom->resource_label()->labels_ = project.resource_labels;
rom->resource_label()->labels_loaded_ = true;
}
}
}
// Business logic
ResourceContextBuilder context_builder(rom);
auto labels_or = context_builder.GetLabels(type);
if (!labels_or.ok()) {
return labels_or.status();
}
auto labels = std::move(labels_or.value());
// Manual output formatting (15 lines)
if (format == "json") {
std::cout << "{\n";
for (const auto& [key, value] : labels) {
std::cout << " \"" << key << "\": \"" << value << "\",\n";
}
std::cout << "}\n";
} else {
for (const auto& [key, value] : labels) {
std::cout << key << ": " << value << "\n";
}
}
return absl::OkStatus();
}

After (30 lines):

absl::Status HandleResourceListCommand(
const std::vector<std::string>& arg_vec, Rom* rom_context) {
// Parse arguments
ArgumentParser parser(arg_vec);
auto type = parser.GetString("type");
auto format_str = parser.GetString("format").value_or("table");
if (!type.has_value()) {
return absl::InvalidArgumentError(
"Usage: agent resource-list --type <type> [--format <table|json>]");
}
// Create formatter
ASSIGN_OR_RETURN(auto formatter, OutputFormatter::FromString(format_str));
// Setup context
CommandContext::Config config;
config.external_rom_context = rom_context;
CommandContext context(config);
// Get ROM and labels
ASSIGN_OR_RETURN(Rom* rom, context.GetRom());
RETURN_IF_ERROR(context.EnsureLabelsLoaded(rom));
// Execute business logic
ResourceContextBuilder builder(rom);
ASSIGN_OR_RETURN(auto labels, builder.GetLabels(*type));
// Format output
formatter.BeginObject("Labels");
for (const auto& [key, value] : labels) {
formatter.AddField(key, value);
}
formatter.EndObject();
formatter.Print();
return absl::OkStatus();
}

Savings: 50+ lines eliminated, clearer intent, easier to maintain

Commands to Refactor

Priority order for refactoring (based on duplication level):

  1. High Priority (Heavy duplication):
    • HandleResourceListCommand - Example provided ✓
    • HandleResourceSearchCommand - Example provided ✓
    • HandleDungeonDescribeRoomCommand - 80 lines → ~35 lines
    • HandleOverworldDescribeMapCommand - 100 lines → ~40 lines
    • HandleOverworldListWarpsCommand - 120 lines → ~45 lines
  2. Medium Priority (Moderate duplication):
    • HandleDungeonListSpritesCommand
    • HandleOverworldFindTileCommand
    • HandleOverworldListSpritesCommand
    • HandleOverworldGetEntranceCommand
    • HandleOverworldTileStatsCommand
  3. Low Priority (Simple commands, less duplication):
    • HandleMessageListCommand (delegates to message handler)
    • HandleMessageReadCommand (delegates to message handler)
    • HandleMessageSearchCommand (delegates to message handler)

Estimated Impact

Metric Before After Savings
Lines of code (tool_commands.cc) 1549 ~800 48%
Duplicated ROM loading ~600 lines 0 600 lines
Duplicated arg parsing ~400 lines 0 400 lines
Duplicated formatting ~300 lines 0 300 lines
Total Duplication Removed **~1300 lines**

Testing Strategy

Unit Testing

TEST(CommandContextTest, LoadsRomFromConfig) {
CommandContext::Config config;
config.rom_path = "test.sfc";
CommandContext context(config);
auto rom_or = context.GetRom();
ASSERT_OK(rom_or);
EXPECT_TRUE(rom_or.value()->is_loaded());
}
TEST(ArgumentParserTest, ParsesStringArguments) {
std::vector<std::string> args = {"--type=dungeon", "--format", "json"};
ArgumentParser parser(args);
EXPECT_EQ(parser.GetString("type").value(), "dungeon");
EXPECT_EQ(parser.GetString("format").value(), "json");
}
TEST(OutputFormatterTest, GeneratesValidJson) {
auto formatter = OutputFormatter::FromString("json").value();
formatter.BeginObject("Test");
formatter.AddField("key", "value");
formatter.EndObject();
std::string output = formatter.GetOutput();
EXPECT_THAT(output, HasSubstr("\"key\": \"value\""));
}
TEST(ResourceCatalogTest, SerializeResourceIncludesReturnsArray)
#define ASSERT_OK(expr)
Definition testing.h:12

Integration Testing

TEST(ResourceListCommandTest, ListsDungeons) {
std::vector<std::string> args = {"--type=dungeon", "--format=json"};
Rom rom;
rom.LoadFromFile("test.sfc");
auto status = HandleResourceListCommand(args, &rom);
EXPECT_OK(status);
}
#define EXPECT_OK(expr)
Definition testing.h:10

Benefits Summary

For Developers

  1. Less Code to Write: New commands take 30-40 lines instead of 80-120
  2. Consistent Patterns: All commands follow the same structure
  3. Better Error Handling: Standardized error messages and validation
  4. Easier Testing: Each component can be tested independently
  5. Self-Documenting: Clear separation of concerns

For Maintainability

  1. Single Source of Truth: ROM loading logic in one place
  2. Easy to Update: Change all commands by updating one class
  3. Consistent Behavior: All commands handle errors the same way
  4. Reduced Bugs: Less duplication = fewer places for bugs

For AI Integration

  1. Predictable Structure: AI can generate commands using templates
  2. Type Safety: ArgumentParser prevents common errors
  3. Consistent Output: AI can reliably parse JSON responses
  4. Easy to Extend: New tool types follow existing patterns

Next Steps

Immediate (Current PR)

  1. ✅ Create abstraction layer (CommandContext, ArgumentParser, OutputFormatter)
  2. ✅ Add CommandHandler base class
  3. ✅ Provide refactored examples
  4. ✅ Update build system
  5. ✅ Document architecture

Phase 2 (Next PR)

  1. Refactor high-priority commands (5 commands)
  2. Add comprehensive unit tests
  3. Update AI tool dispatcher to use new patterns
  4. Create command generator templates for AI

Phase 3 (Future)

  1. Refactor remaining commands
  2. Remove old helper functions
  3. Add performance benchmarks
  4. Create VS Code snippets for command development

Migration Checklist

For each command being refactored:

  • [ ] Replace manual argument parsing with ArgumentParser
  • [ ] Replace ROM loading with CommandContext
  • [ ] Replace label initialization with context.EnsureLabelsLoaded()
  • [ ] Replace manual formatting with OutputFormatter
  • [ ] Update error messages to use GetUsage()
  • [ ] Add unit tests for the command
  • [ ] Update documentation
  • [ ] Test with both JSON and text output
  • [ ] Test with missing/invalid arguments
  • [ ] Test with mock ROM

References

  • Implementation: src/cli/service/resources/command_context.{h,cc}
  • Examples: src/cli/handlers/agent/tool_commands_refactored.cc
  • Base class: src/cli/service/resources/command_handler.{h,cc}
  • Build config: src/cli/agent.cmake

Questions & Answers

Q: Should I refactor all commands at once?
A: No. Refactor in phases to minimize risk. Start with 2-3 commands as proof of concept.

Q: What if my command needs custom argument handling?
A: ArgumentParser is flexible. You can still access raw args or add custom parsing logic.

Q: Can I use both old and new patterns temporarily?
A: Yes. The new abstraction layer works alongside existing code. Migrate gradually.

Q: Will this affect AI tool calling?
A: No breaking changes. The command interfaces remain the same. Internal implementation improves.

Q: How do I test commands with the new abstractions?
A: Use CommandContext with mock ROM, or pass external rom_context in tests.


Last Updated: October 11, 2025
Author: AI Assistant
Review Status: Ready for Implementation