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