yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
command_registry.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <sstream>
5
6#include "absl/strings/str_cat.h"
7#include "absl/strings/str_format.h"
8#include "absl/strings/str_join.h"
10
11namespace yaze {
12namespace cli {
13
14namespace {
15std::string EscapeJson(const std::string& input) {
16 std::string escaped;
17 escaped.reserve(input.size());
18 for (char c : input) {
19 switch (c) {
20 case '\\\\':
21 escaped.append("\\\\");
22 break;
23 case '\"':
24 escaped.append("\\\"");
25 break;
26 case '\n':
27 escaped.append("\\n");
28 break;
29 case '\r':
30 escaped.append("\\r");
31 break;
32 case '\t':
33 escaped.append("\\t");
34 break;
35 default:
36 escaped.push_back(c);
37 }
38 }
39 return escaped;
40}
41
42void AppendStringArray(std::ostringstream& out,
43 const std::vector<std::string>& values) {
44 out << "[";
45 for (size_t i = 0; i < values.size(); ++i) {
46 if (i > 0)
47 out << ", ";
48 out << "\"" << EscapeJson(values[i]) << "\"";
49 }
50 out << "]";
51}
52} // namespace
53
55 static CommandRegistry instance;
56 static bool initialized = false;
57 if (!initialized) {
58 instance.RegisterAllCommands();
59 initialized = true;
60 }
61 return instance;
62}
63
65 std::unique_ptr<resources::CommandHandler> handler,
66 const CommandMetadata& metadata) {
67 std::string name = handler->GetName();
68
69 // Store metadata
70 metadata_[name] = metadata;
71
72 // Register aliases
73 for (const auto& alias : metadata.aliases) {
74 aliases_[alias] = name;
75 }
76
77 // Store handler
78 handlers_[name] = std::move(handler);
79}
80
81resources::CommandHandler* CommandRegistry::Get(const std::string& name) const {
82 // Check direct name
83 auto it = handlers_.find(name);
84 if (it != handlers_.end()) {
85 return it->second.get();
86 }
87
88 // Check aliases
89 auto alias_it = aliases_.find(name);
90 if (alias_it != aliases_.end()) {
91 auto handler_it = handlers_.find(alias_it->second);
92 if (handler_it != handlers_.end()) {
93 return handler_it->second.get();
94 }
95 }
96
97 return nullptr;
98}
99
101 const std::string& name) const {
102 // Resolve alias first
103 std::string canonical_name = name;
104 auto alias_it = aliases_.find(name);
105 if (alias_it != aliases_.end()) {
106 canonical_name = alias_it->second;
107 }
108
109 auto it = metadata_.find(canonical_name);
110 return (it != metadata_.end()) ? &it->second : nullptr;
111}
112
114 const std::string& category) const {
115 std::vector<std::string> result;
116 for (const auto& [name, metadata] : metadata_) {
117 if (metadata.category == category) {
118 result.push_back(name);
119 }
120 }
121 return result;
122}
123
124std::vector<std::string> CommandRegistry::GetCategories() const {
125 std::vector<std::string> categories;
126 for (const auto& [_, metadata] : metadata_) {
127 if (std::find(categories.begin(), categories.end(), metadata.category) ==
128 categories.end()) {
129 categories.push_back(metadata.category);
130 }
131 }
132 return categories;
133}
134
135std::vector<std::string> CommandRegistry::GetAgentCommands() const {
136 std::vector<std::string> result;
137 for (const auto& [name, metadata] : metadata_) {
138 if (metadata.available_to_agent) {
139 result.push_back(name);
140 }
141 }
142 return result;
143}
144
146 std::ostringstream out;
147 out << "{\n \"commands\": [\n";
148
149 bool first = true;
150 for (const auto& [_, metadata] : metadata_) {
151 if (!first)
152 out << ",\n";
153 first = false;
154
155 out << " {\n";
156 out << " \"name\": \"" << EscapeJson(metadata.name) << "\",\n";
157 out << " \"category\": \"" << EscapeJson(metadata.category) << "\",\n";
158 out << " \"description\": \"" << EscapeJson(metadata.description)
159 << "\",\n";
160 out << " \"usage\": \"" << EscapeJson(metadata.usage) << "\",\n";
161 out << " \"available_to_agent\": "
162 << (metadata.available_to_agent ? "true" : "false") << ",\n";
163 out << " \"requires_rom\": "
164 << (metadata.requires_rom ? "true" : "false") << ",\n";
165 out << " \"requires_grpc\": "
166 << (metadata.requires_grpc ? "true" : "false") << ",\n";
167 if (!metadata.todo_reference.empty()) {
168 out << " \"todo_reference\": \""
169 << EscapeJson(metadata.todo_reference) << "\",\n";
170 } else {
171 out << " \"todo_reference\": \"\",\n";
172 }
173 out << " \"aliases\": ";
174 AppendStringArray(out, metadata.aliases);
175 out << ",\n";
176 out << " \"examples\": ";
177 AppendStringArray(out, metadata.examples);
178 out << "\n";
179 out << " }";
180 }
181
182 out << "\n ]\n}";
183 return out.str();
184}
185
186std::string CommandRegistry::GenerateHelp(const std::string& name) const {
187 auto* metadata = GetMetadata(name);
188 if (!metadata) {
189 return absl::StrFormat("Command '%s' not found", name);
190 }
191
192 std::ostringstream help;
193 help << "\n\033[1;36m" << metadata->name << "\033[0m - "
194 << metadata->description << "\n\n";
195 help << "\033[1;33mUsage:\033[0m\n";
196 help << " " << metadata->usage << "\n\n";
197
198 if (!metadata->examples.empty()) {
199 help << "\033[1;33mExamples:\033[0m\n";
200 for (const auto& example : metadata->examples) {
201 help << " " << example << "\n";
202 }
203 help << "\n";
204 }
205
206 if (metadata->requires_rom) {
207 help << "\033[1;33mRequires:\033[0m ROM file (--rom=<path>)\n";
208 }
209 if (metadata->requires_grpc) {
210 help << "\033[1;33mRequires:\033[0m YAZE running with gRPC enabled\n";
211 }
212
213 if (!metadata->aliases.empty()) {
214 help << "\n\033[1;33mAliases:\033[0m "
215 << absl::StrJoin(metadata->aliases, ", ") << "\n";
216 }
217
218 return help.str();
219}
220
222 const std::string& category) const {
223 auto commands = GetCommandsInCategory(category);
224 if (commands.empty()) {
225 return absl::StrFormat("No commands in category '%s'", category);
226 }
227
228 std::ostringstream help;
229 help << "\n\033[1;36m" << category << " commands:\033[0m\n\n";
230
231 for (const auto& cmd : commands) {
232 auto* metadata = GetMetadata(cmd);
233 if (metadata) {
234 help << " \033[1;33m" << cmd << "\033[0m\n";
235 help << " " << metadata->description << "\n";
236 if (!metadata->usage.empty()) {
237 help << " Usage: " << metadata->usage << "\n";
238 }
239 help << "\n";
240 }
241 }
242
243 return help.str();
244}
245
247 std::ostringstream help;
248 help << "\n\033[1;36mAll z3ed Commands:\033[0m\n\n";
249
250 auto categories = GetCategories();
251 for (const auto& category : categories) {
252 help << GenerateCategoryHelp(category);
253 }
254
255 return help.str();
256}
257
258absl::Status CommandRegistry::Execute(const std::string& name,
259 const std::vector<std::string>& args,
260 Rom* rom_context,
261 std::string* captured_output) {
262 auto* handler = Get(name);
263 if (!handler) {
264 return absl::NotFoundError(absl::StrFormat("Command '%s' not found", name));
265 }
266
267 return handler->Run(args, rom_context, captured_output);
268}
269
270bool CommandRegistry::HasCommand(const std::string& name) const {
271 return Get(name) != nullptr;
272}
273
275 // Get all command handlers from factory
276 auto all_handlers = handlers::CreateAllCommandHandlers();
277
278 for (auto& handler : all_handlers) {
279 std::string name = handler->GetName();
280
281 // Build metadata from handler
282 auto descriptor = handler->Describe();
283 CommandMetadata metadata;
284 metadata.name = name;
285 metadata.usage = handler->GetUsage();
286 metadata.available_to_agent = true; // Most commands available to agent
287 metadata.requires_rom = handler->RequiresRom();
288 metadata.requires_grpc = false;
289
290 // Categorize and enhance metadata based on command type
291 if (name.find("resource-") == 0) {
292 metadata.category = "resource";
293 metadata.description = "Resource inspection and search";
294 if (name == "resource-list") {
295 metadata.examples = {"z3ed resource-list --type=dungeon --format=json",
296 "z3ed resource-list --type=sprite --format=table"};
297 }
298 } else if (name.find("dungeon-") == 0) {
299 metadata.category = "dungeon";
300 metadata.description = "Dungeon inspection and editing";
301 if (name == "dungeon-describe-room") {
302 metadata.examples = {
303 "z3ed dungeon-describe-room --room=5 --format=json"};
304 }
305 } else if (name.find("overworld-") == 0) {
306 metadata.category = "overworld";
307 metadata.description = "Overworld inspection and editing";
308 if (name == "overworld-find-tile") {
309 metadata.examples = {
310 "z3ed overworld-find-tile --tile=0x42 --format=json"};
311 }
312 } else if (name.find("rom-") == 0) {
313 metadata.category = "rom";
314 metadata.description = "ROM inspection and validation";
315 if (name == "rom-info") {
316 metadata.examples = {"z3ed rom-info --rom=zelda3.sfc"};
317 } else if (name == "rom-validate") {
318 metadata.examples = {"z3ed rom-validate --rom=zelda3.sfc"};
319 } else if (name == "rom-doctor") {
320 metadata.examples = {"z3ed rom-doctor --rom=zelda3.sfc --format=json"};
321 } else if (name == "rom-diff") {
322 metadata.examples = {
323 "z3ed rom-diff --rom_a=base.sfc --rom_b=target.sfc"};
324 } else if (name == "rom-compare") {
325 metadata.examples = {
326 "z3ed rom-compare --rom=target.sfc --baseline=vanilla.sfc"};
327 }
328 } else if (name.find("emulator-") == 0) {
329 metadata.category = "emulator";
330 metadata.description = "Emulator control and debugging";
331 metadata.requires_grpc = true;
332 if (name == "emulator-set-breakpoint") {
333 metadata.examples = {
334 "z3ed emulator-set-breakpoint --address=0x83D7 --description='NMI "
335 "handler'"};
336 }
337 } else if (name.find("gui-") == 0) {
338 metadata.category = "gui";
339 metadata.description = "GUI automation";
340 metadata.requires_grpc = true;
341 } else if (name.find("hex-") == 0) {
342 metadata.category = "graphics";
343 metadata.description = "Hex data manipulation";
344 } else if (name.find("palette-") == 0) {
345 metadata.category = "graphics";
346 metadata.description = "Palette operations";
347 } else if (name.find("sprite-") == 0) {
348 metadata.category = "graphics";
349 metadata.description = "Sprite operations";
350 } else if (name.find("message-") == 0 || name.find("dialogue-") == 0) {
351 metadata.category = "game";
352 metadata.description = name.find("message-") == 0 ? "Message inspection"
353 : "Dialogue inspection";
354 } else if (name.find("music-") == 0) {
355 metadata.category = "game";
356 metadata.description = "Music/audio inspection";
357 } else if (name == "simple-chat" || name == "chat") {
358 metadata.category = "agent";
359 metadata.description = "AI conversational agent";
360 metadata.available_to_agent = false; // Meta-command
361 metadata.requires_rom = false;
362 metadata.examples = {
363 "z3ed simple-chat --rom=zelda3.sfc",
364 "z3ed simple-chat \"What dungeons exist?\" --rom=zelda3.sfc"};
365 } else if (name.find("tools-") == 0) {
366 metadata.category = "tools";
367 if (name == "tools-list") {
368 metadata.description = "List available test helper tools";
369 metadata.requires_rom = false;
370 metadata.available_to_agent = true;
371 } else if (name == "tools-harness-state") {
372 metadata.description = "Generate WRAM state for test harnesses";
373 metadata.examples = {
374 "z3ed tools-harness-state --rom=zelda3.sfc --output=state.h"};
375 } else if (name == "tools-extract-values") {
376 metadata.description = "Extract vanilla ROM values";
377 metadata.examples = {"z3ed tools-extract-values --rom=zelda3.sfc"};
378 } else if (name == "tools-extract-golden") {
379 metadata.description = "Extract comprehensive golden data for testing";
380 metadata.examples = {
381 "z3ed tools-extract-golden --rom=zelda3.sfc --output=golden.h"};
382 } else if (name == "tools-patch-v3") {
383 metadata.description = "Create v3 ZSCustomOverworld patched ROM";
384 metadata.examples = {
385 "z3ed tools-patch-v3 --rom=zelda3.sfc --output=patched.sfc"};
386 } else {
387 metadata.description = "Test helper tool";
388 }
389 } else if (name.find("test-") == 0) {
390 metadata.category = "test";
391 metadata.description = "Test discovery and execution";
392 if (name == "test-list") {
393 metadata.requires_rom = false;
394 metadata.examples = {"z3ed test-list", "z3ed test-list --format json"};
395 } else if (name == "test-run") {
396 metadata.examples = {"z3ed test-run --label stable",
397 "z3ed test-run --label gui"};
398 } else if (name == "test-status") {
399 metadata.requires_rom = false;
400 metadata.examples = {"z3ed test-status --format json"};
401 }
402 } else {
403 metadata.category = "misc";
404 metadata.description = "Miscellaneous command";
405 }
406
407 // Prefer handler-provided summary if present
408 if (!descriptor.summary.empty() &&
409 descriptor.summary != "Command summary not provided.") {
410 metadata.description = descriptor.summary;
411 }
412
413 // Keep TODO reference if supplied by handler
414 if (!descriptor.todo_reference.empty() &&
415 descriptor.todo_reference != "todo#unassigned") {
416 metadata.todo_reference = descriptor.todo_reference;
417 }
418
419 Register(std::move(handler), metadata);
420 }
421}
422
423} // namespace cli
424} // namespace yaze
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:24
Single source of truth for all z3ed commands.
std::map< std::string, std::string > aliases_
std::vector< std::string > GetCommandsInCategory(const std::string &category) const
Get all commands in a category.
static CommandRegistry & Instance()
std::string GenerateCategoryHelp(const std::string &category) const
Generate category help text.
std::string ExportFunctionSchemas() const
Export function schemas for AI tool calling (JSON)
const CommandMetadata * GetMetadata(const std::string &name) const
Get command metadata.
std::string GenerateCompleteHelp() const
Generate complete help text (all commands)
std::vector< std::string > GetAgentCommands() const
Get all commands available to AI agents.
std::map< std::string, CommandMetadata > metadata_
absl::Status Execute(const std::string &name, const std::vector< std::string > &args, Rom *rom_context=nullptr, std::string *captured_output=nullptr)
Execute a command by name.
std::map< std::string, std::unique_ptr< resources::CommandHandler > > handlers_
resources::CommandHandler * Get(const std::string &name) const
Get a command handler by name or alias.
bool HasCommand(const std::string &name) const
Check if command exists.
std::vector< std::string > GetCategories() const
Get all categories.
void Register(std::unique_ptr< resources::CommandHandler > handler, const CommandMetadata &metadata)
Register a command handler.
std::string GenerateHelp(const std::string &name) const
Generate help text for a command.
Base class for CLI command handlers.
std::string EscapeJson(const std::string &input)
void AppendStringArray(std::ostringstream &out, const std::vector< std::string > &values)
std::vector< std::unique_ptr< resources::CommandHandler > > CreateAllCommandHandlers()
Factory function to create all command handlers (CLI + agent)