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 <iostream>
5#include <sstream>
6
7#include "absl/strings/str_cat.h"
8#include "absl/strings/str_format.h"
9#include "absl/strings/str_join.h"
11
12namespace yaze {
13namespace cli {
14
15namespace {
16std::string EscapeJson(const std::string& input) {
17 std::string escaped;
18 escaped.reserve(input.size());
19 for (char c : input) {
20 switch (c) {
21 case '\\\\':
22 escaped.append("\\\\");
23 break;
24 case '\"':
25 escaped.append("\\\"");
26 break;
27 case '\n':
28 escaped.append("\\n");
29 break;
30 case '\r':
31 escaped.append("\\r");
32 break;
33 case '\t':
34 escaped.append("\\t");
35 break;
36 default:
37 escaped.push_back(c);
38 }
39 }
40 return escaped;
41}
42
43void AppendStringArray(std::ostringstream& out,
44 const std::vector<std::string>& values) {
45 out << "[";
46 for (size_t i = 0; i < values.size(); ++i) {
47 if (i > 0)
48 out << ", ";
49 out << "\"" << EscapeJson(values[i]) << "\"";
50 }
51 out << "]";
52}
53} // namespace
54
56 static CommandRegistry instance;
57 static bool initialized = false;
58 if (!initialized) {
59 instance.RegisterCliCommands();
60 initialized = true;
61 }
62 return instance;
63}
64
66 std::unique_ptr<resources::CommandHandler> handler,
67 const CommandMetadata& metadata) {
68 std::string name = handler->GetName();
69
70 // Store metadata
71 metadata_[name] = metadata;
72
73 // Register aliases
74 for (const auto& alias : metadata.aliases) {
75 aliases_[alias] = name;
76 }
77
78 // Store handler
79 handlers_[name] = std::move(handler);
80}
81
83 std::vector<std::unique_ptr<resources::CommandHandler>> handlers) {
84 for (auto& handler : handlers) {
85 std::string name = handler->GetName();
86
87 if (handlers_.find(name) != handlers_.end()) {
88 continue;
89 }
90
91 // Build metadata from handler
92 auto descriptor = handler->Describe();
93 CommandMetadata metadata;
94 metadata.name = name;
95 metadata.usage = handler->GetUsage();
96 metadata.available_to_agent = true; // Most commands available to agent
97 metadata.requires_rom = handler->RequiresRom();
98 metadata.requires_grpc = false;
99
100 // Categorize and enhance metadata based on command type
101 if (name.find("resource-") == 0) {
102 metadata.category = "resource";
103 metadata.description = "Resource inspection and search";
104 if (name == "resource-list") {
105 metadata.examples = {"z3ed resource-list --type=dungeon --format=json",
106 "z3ed resource-list --type=sprite --format=table"};
107 }
108 } else if (name.find("dungeon-") == 0) {
109 metadata.category = "dungeon";
110 metadata.description = "Dungeon inspection and editing";
111 if (name == "dungeon-describe-room") {
112 metadata.examples = {
113 "z3ed dungeon-describe-room --room=5 --format=json"};
114 } else if (name == "dungeon-place-sprite") {
115 metadata.description =
116 "Place a dungeon sprite (dry-run by default, --write to apply)";
117 metadata.examples = {
118 "z3ed dungeon-place-sprite --room=0x77 --id=0xA3 --x=16 --y=21 "
119 "--subtype=4 --rom=Roms/oos168.sfc --format=json",
120 "z3ed dungeon-place-sprite --room=0x77 --id=0xA3 --x=16 --y=21 "
121 "--subtype=4 --write --rom=/tmp/oos-work.sfc --format=json"};
122 } else if (name == "dungeon-remove-sprite") {
123 metadata.description =
124 "Remove a dungeon sprite by index or exact coordinates";
125 metadata.examples = {
126 "z3ed dungeon-remove-sprite --room=0x77 --index=2 "
127 "--rom=/tmp/oos-work.sfc --format=json",
128 "z3ed dungeon-remove-sprite --room=0x77 --x=16 --y=21 --write "
129 "--rom=/tmp/oos-work.sfc --format=json"};
130 } else if (name == "dungeon-place-object") {
131 metadata.description =
132 "Place a dungeon object (tracks, rails, doors, etc.)";
133 metadata.examples = {
134 "z3ed dungeon-place-object --room=0x98 --id=0x0031 --x=20 --y=20 "
135 "--size=4 --rom=/tmp/oos-work.sfc --format=json",
136 "z3ed dungeon-place-object --room=0x98 --id=0x0031 --x=20 --y=20 "
137 "--size=4 --write --rom=/tmp/oos-work.sfc --format=json"};
138 } else if (name == "dungeon-oracle-preflight") {
139 metadata.description =
140 "Oracle ROM safety preflight: water-fill region/table, collision "
141 "maps, and optional required-room checks";
142 metadata.examples = {
143 "z3ed dungeon-oracle-preflight --rom oos168.sfc --format=json",
144 "z3ed dungeon-oracle-preflight --rom oos168.sfc "
145 "--required-collision-rooms=0x25,0x27 --format=json",
146 "z3ed dungeon-oracle-preflight --rom oos168.sfc "
147 "--report=/tmp/preflight.json"};
148 } else if (name == "dungeon-set-collision-tile") {
149 metadata.description =
150 "Set one or more custom collision tiles in a dungeon room";
151 metadata.examples = {
152 "z3ed dungeon-set-collision-tile --room=0xB8 "
153 "--tiles=\"10,5,0xB7;50,45,0xBA\" --rom=/tmp/oos-work.sfc "
154 "--format=json",
155 "z3ed dungeon-set-collision-tile --room=0xB8 "
156 "--tiles=\"10,5,0xB7;50,45,0xBA\" --write "
157 "--rom=/tmp/oos-work.sfc --format=json"};
158 }
159 } else if (name.find("overworld-") == 0) {
160 metadata.category = "overworld";
161 metadata.description = "Overworld inspection and editing";
162 if (name == "overworld-find-tile") {
163 metadata.examples = {
164 "z3ed overworld-find-tile --tile=0x42 --format=json"};
165 }
166 } else if (name.find("project-bundle-") == 0) {
167 metadata.category = "project";
168 metadata.requires_rom = false;
169 if (name == "project-bundle-verify") {
170 metadata.description = "Project bundle verification";
171 metadata.examples = {
172 "z3ed project-bundle-verify --project MyProject.yazeproj "
173 "--format=json",
174 "z3ed project-bundle-verify --project project.yaze "
175 "--report report.json",
176 "z3ed project-bundle-verify --project MyProject.yazeproj "
177 "--check-rom-hash --format=json"};
178 } else if (name == "project-bundle-pack") {
179 metadata.description = "Pack bundle into zip archive";
180 metadata.examples = {
181 "z3ed project-bundle-pack --project MyProject.yazeproj "
182 "--out MyProject.zip --format=json",
183 "z3ed project-bundle-pack --project MyProject.yazeproj "
184 "--out MyProject.zip --overwrite"};
185 } else if (name == "project-bundle-unpack") {
186 metadata.description = "Unpack zip archive into bundle";
187 metadata.examples = {
188 "z3ed project-bundle-unpack --archive MyProject.zip "
189 "--out ./projects --format=json",
190 "z3ed project-bundle-unpack --archive MyProject.zip "
191 "--out ./projects --overwrite",
192 "z3ed project-bundle-unpack --archive MyProject.zip "
193 "--out ./projects --keep-partial-output",
194 "z3ed project-bundle-unpack --archive MyProject.zip "
195 "--out ./projects --dry-run"};
196 }
197 } else if (name.find("rom-") == 0) {
198 metadata.category = "rom";
199 metadata.description = "ROM inspection and validation";
200 if (name == "rom-info") {
201 metadata.examples = {"z3ed rom-info --rom=zelda3.sfc"};
202 } else if (name == "rom-validate") {
203 metadata.examples = {"z3ed rom-validate --rom=zelda3.sfc"};
204 } else if (name == "rom-doctor") {
205 metadata.examples = {"z3ed rom-doctor --rom=zelda3.sfc --format=json"};
206 } else if (name == "rom-diff") {
207 metadata.examples = {
208 "z3ed rom-diff --rom_a=base.sfc --rom_b=target.sfc"};
209 } else if (name == "rom-compare") {
210 metadata.examples = {
211 "z3ed rom-compare --rom=target.sfc --baseline=vanilla.sfc"};
212 }
213 } else if (name.find("emulator-") == 0) {
214 metadata.category = "emulator";
215 metadata.description = "Emulator control and debugging";
216 metadata.requires_grpc = true;
217 if (name == "emulator-set-breakpoint") {
218 metadata.examples = {
219 "z3ed emulator-set-breakpoint --address=0x83D7 --description='NMI "
220 "handler'"};
221 }
222 } else if (name.find("mesen-") == 0) {
223 metadata.category = "mesen2";
224 metadata.description = "Mesen2 socket automation and introspection";
225 metadata.requires_rom = false;
226 if (name == "mesen-state-capture") {
227 metadata.examples = {
228 "z3ed mesen-state-capture --state=foo.state "
229 "--rom-file=Roms/oos168x.sfc --scenario=d6_room_88",
230 "z3ed mesen-state-capture --state=Roms/SaveStates/d6_room_88.state "
231 "--rom-file=Roms/oos168x.sfc --slot=1 "
232 "--states-dir=~/Library/Application\\ Support/Mesen2/SaveStates"};
233 } else if (name == "mesen-state-hook") {
234 metadata.examples = {
235 "z3ed mesen-state-hook --state=foo.state "
236 "--rom-file=Roms/oos168x.sfc --scenario=d6_room_88",
237 "z3ed mesen-state-hook --state=foo.state "
238 "--rom-file=Roms/oos168x.sfc --format=json"};
239 }
240 } else if (name.find("gui-") == 0) {
241 metadata.category = "gui";
242 metadata.description = "GUI automation";
243 metadata.requires_grpc = true;
244 } else if (name.find("hex-") == 0) {
245 metadata.category = "graphics";
246 metadata.description = "Hex data manipulation";
247 } else if (name.find("palette-") == 0) {
248 metadata.category = "graphics";
249 metadata.description = "Palette operations";
250 } else if (name.find("sprite-") == 0) {
251 metadata.category = "graphics";
252 metadata.description = "Sprite operations";
253 } else if (name.find("message-") == 0 || name.find("dialogue-") == 0) {
254 metadata.category = "game";
255 metadata.description = name.find("message-") == 0 ? "Message inspection"
256 : "Dialogue inspection";
257 } else if (name.find("music-") == 0) {
258 metadata.category = "game";
259 metadata.description = "Music/audio inspection";
260 } else if (name.find("oracle-") == 0) {
261 metadata.category = "oracle";
262 metadata.description = "Oracle-of-Secrets project tooling";
263 if (name == "oracle-menu-index") {
264 metadata.examples = {
265 "z3ed oracle-menu-index --project=/path/to/oracle-of-secrets "
266 "--format=json",
267 "z3ed oracle-menu-index --table=Menu_ItemCursorPositions "
268 "--missing-bins --format=json"};
269 } else if (name == "oracle-menu-set-offset") {
270 metadata.examples = {
271 "z3ed oracle-menu-set-offset "
272 "--asm=Menu/menu_select_item.asm "
273 "--table=Menu_ItemCursorPositions --index=0 --row=7 --col=2",
274 "z3ed oracle-menu-set-offset "
275 "--asm=Menu/menu_select_item.asm "
276 "--table=Menu_ItemCursorPositions --index=0 --row=7 --col=2 "
277 "--write"};
278 } else if (name == "oracle-menu-validate") {
279 metadata.examples = {
280 "z3ed oracle-menu-validate --project=/path/to/oracle-of-secrets",
281 "z3ed oracle-menu-validate --project=/path/to/oracle-of-secrets "
282 "--strict --max-row=31 --max-col=31"};
283 } else if (name == "oracle-smoke-check") {
284 metadata.examples = {
285 "z3ed oracle-smoke-check --rom oos168.sfc --format=json",
286 "z3ed oracle-smoke-check --rom oos168.sfc --strict-readiness "
287 "--format=json",
288 "z3ed oracle-smoke-check --rom oos168.sfc "
289 "--min-d6-track-rooms=4 --format=json",
290 "z3ed oracle-smoke-check --rom oos168.sfc "
291 "--report=/tmp/smoke.json"};
292 }
293 } else if (name == "simple-chat" || name == "chat") {
294 metadata.category = "agent";
295 metadata.description = "AI conversational agent";
296 metadata.available_to_agent = false; // Meta-command
297 metadata.requires_rom = false;
298 metadata.examples = {
299 "z3ed simple-chat --rom=zelda3.sfc",
300 "z3ed simple-chat \"What dungeons exist?\" --rom=zelda3.sfc"};
301 } else if (name.find("tools-") == 0) {
302 metadata.category = "tools";
303 if (name == "tools-list") {
304 metadata.description = "List available test helper tools";
305 metadata.requires_rom = false;
306 metadata.available_to_agent = true;
307 } else if (name == "tools-harness-state") {
308 metadata.description = "Generate WRAM state for test harnesses";
309 metadata.examples = {
310 "z3ed tools-harness-state --rom=zelda3.sfc --output=state.h"};
311 } else if (name == "tools-extract-values") {
312 metadata.description = "Extract vanilla ROM values";
313 metadata.examples = {"z3ed tools-extract-values --rom=zelda3.sfc"};
314 } else if (name == "tools-extract-golden") {
315 metadata.description = "Extract comprehensive golden data for testing";
316 metadata.examples = {
317 "z3ed tools-extract-golden --rom=zelda3.sfc --output=golden.h"};
318 } else if (name == "tools-patch-v3") {
319 metadata.description = "Create v3 ZSCustomOverworld patched ROM";
320 metadata.examples = {
321 "z3ed tools-patch-v3 --rom=zelda3.sfc --output=patched.sfc"};
322 } else {
323 metadata.description = "Test helper tool";
324 }
325 } else if (name.find("test-") == 0) {
326 metadata.category = "test";
327 metadata.description = "Test discovery and execution";
328 if (name == "test-list") {
329 metadata.requires_rom = false;
330 metadata.examples = {"z3ed test-list", "z3ed test-list --format json"};
331 } else if (name == "test-run") {
332 metadata.examples = {"z3ed test-run --label stable",
333 "z3ed test-run --label gui"};
334 } else if (name == "test-status") {
335 metadata.requires_rom = false;
336 metadata.examples = {"z3ed test-status --format json"};
337 }
338 } else {
339 metadata.category = "misc";
340 metadata.description = "Miscellaneous command";
341 }
342
343 // Prefer handler-provided summary if present
344 if (!descriptor.summary.empty() &&
345 descriptor.summary != "Command summary not provided.") {
346 metadata.description = descriptor.summary;
347 }
348
349 // Keep TODO reference if supplied by handler
350 if (!descriptor.todo_reference.empty() &&
351 descriptor.todo_reference != "todo#unassigned") {
352 metadata.todo_reference = descriptor.todo_reference;
353 }
354
355 Register(std::move(handler), metadata);
356 }
357}
358
359resources::CommandHandler* CommandRegistry::Get(const std::string& name) const {
360 // Check direct name
361 auto it = handlers_.find(name);
362 if (it != handlers_.end()) {
363 return it->second.get();
364 }
365
366 // Check aliases
367 auto alias_it = aliases_.find(name);
368 if (alias_it != aliases_.end()) {
369 auto handler_it = handlers_.find(alias_it->second);
370 if (handler_it != handlers_.end()) {
371 return handler_it->second.get();
372 }
373 }
374
375 return nullptr;
376}
377
379 const std::string& name) const {
380 // Resolve alias first
381 std::string canonical_name = name;
382 auto alias_it = aliases_.find(name);
383 if (alias_it != aliases_.end()) {
384 canonical_name = alias_it->second;
385 }
386
387 auto it = metadata_.find(canonical_name);
388 return (it != metadata_.end()) ? &it->second : nullptr;
389}
390
392 const std::string& category) const {
393 std::vector<std::string> result;
394 for (const auto& [name, metadata] : metadata_) {
395 if (metadata.category == category) {
396 result.push_back(name);
397 }
398 }
399 return result;
400}
401
402std::vector<std::string> CommandRegistry::GetCategories() const {
403 std::vector<std::string> categories;
404 for (const auto& [_, metadata] : metadata_) {
405 if (std::find(categories.begin(), categories.end(), metadata.category) ==
406 categories.end()) {
407 categories.push_back(metadata.category);
408 }
409 }
410 return categories;
411}
412
413std::vector<std::string> CommandRegistry::GetAgentCommands() const {
414 std::vector<std::string> result;
415 for (const auto& [name, metadata] : metadata_) {
416 if (metadata.available_to_agent) {
417 result.push_back(name);
418 }
419 }
420 return result;
421}
422
424 std::ostringstream out;
425 out << "{\n \"commands\": [\n";
426
427 bool first = true;
428 for (const auto& [_, metadata] : metadata_) {
429 if (!first)
430 out << ",\n";
431 first = false;
432
433 out << " {\n";
434 out << " \"name\": \"" << EscapeJson(metadata.name) << "\",\n";
435 out << " \"category\": \"" << EscapeJson(metadata.category) << "\",\n";
436 out << " \"description\": \"" << EscapeJson(metadata.description)
437 << "\",\n";
438 out << " \"usage\": \"" << EscapeJson(metadata.usage) << "\",\n";
439 out << " \"available_to_agent\": "
440 << (metadata.available_to_agent ? "true" : "false") << ",\n";
441 out << " \"requires_rom\": "
442 << (metadata.requires_rom ? "true" : "false") << ",\n";
443 out << " \"requires_grpc\": "
444 << (metadata.requires_grpc ? "true" : "false") << ",\n";
445 if (!metadata.todo_reference.empty()) {
446 out << " \"todo_reference\": \""
447 << EscapeJson(metadata.todo_reference) << "\",\n";
448 } else {
449 out << " \"todo_reference\": \"\",\n";
450 }
451 out << " \"aliases\": ";
452 AppendStringArray(out, metadata.aliases);
453 out << ",\n";
454 out << " \"examples\": ";
455 AppendStringArray(out, metadata.examples);
456 out << "\n";
457 out << " }";
458 }
459
460 out << "\n ]\n}";
461 return out.str();
462}
463
464std::string CommandRegistry::GenerateHelp(const std::string& name) const {
465 auto* metadata = GetMetadata(name);
466 if (!metadata) {
467 return absl::StrFormat("Command '%s' not found", name);
468 }
469
470 std::ostringstream help;
471 help << "\n\033[1;36m" << metadata->name << "\033[0m - "
472 << metadata->description << "\n\n";
473 help << "\033[1;33mUsage:\033[0m\n";
474 help << " " << metadata->usage << "\n\n";
475
476 if (!metadata->examples.empty()) {
477 help << "\033[1;33mExamples:\033[0m\n";
478 for (const auto& example : metadata->examples) {
479 help << " " << example << "\n";
480 }
481 help << "\n";
482 }
483
484 if (metadata->requires_rom) {
485 help << "\033[1;33mRequires:\033[0m ROM file (--rom=<path>)\n";
486 }
487 if (metadata->requires_grpc) {
488 help << "\033[1;33mRequires:\033[0m YAZE running with gRPC enabled\n";
489 }
490
491 if (!metadata->aliases.empty()) {
492 help << "\n\033[1;33mAliases:\033[0m "
493 << absl::StrJoin(metadata->aliases, ", ") << "\n";
494 }
495
496 return help.str();
497}
498
500 const std::string& category) const {
501 auto commands = GetCommandsInCategory(category);
502 if (commands.empty()) {
503 return absl::StrFormat("No commands in category '%s'", category);
504 }
505
506 std::ostringstream help;
507 help << "\n\033[1;36m" << category << " commands:\033[0m\n\n";
508
509 for (const auto& cmd : commands) {
510 auto* metadata = GetMetadata(cmd);
511 if (metadata) {
512 help << " \033[1;33m" << cmd << "\033[0m\n";
513 help << " " << metadata->description << "\n";
514 if (!metadata->usage.empty()) {
515 help << " Usage: " << metadata->usage << "\n";
516 }
517 help << "\n";
518 }
519 }
520
521 return help.str();
522}
523
525 std::ostringstream help;
526 help << "\n\033[1;36mAll z3ed Commands:\033[0m\n\n";
527
528 auto categories = GetCategories();
529 for (const auto& category : categories) {
530 help << GenerateCategoryHelp(category);
531 }
532
533 return help.str();
534}
535
536absl::Status CommandRegistry::Execute(const std::string& name,
537 const std::vector<std::string>& args,
538 Rom* rom_context,
539 std::string* captured_output) {
540 auto* handler = Get(name);
541 if (!handler) {
542 return absl::NotFoundError(absl::StrFormat("Command '%s' not found", name));
543 }
544
545 const bool command_help_requested =
546 std::find(args.begin(), args.end(), "--help") != args.end() ||
547 std::find(args.begin(), args.end(), "-h") != args.end();
548 if (command_help_requested) {
549 const std::string help = GenerateHelp(name);
550 if (captured_output != nullptr) {
551 *captured_output = help;
552 } else {
553 std::cout << help << "\n";
554 }
555 return absl::OkStatus();
556 }
557
558 absl::Status status = handler->Run(args, rom_context, captured_output);
559
560 // If a command was invoked without its required arguments, surface full
561 // command help in addition to the normal parser error/usage line.
562 if (!status.ok() && status.code() == absl::StatusCode::kInvalidArgument &&
563 args.empty()) {
564 const std::string help = GenerateHelp(name);
565 if (captured_output != nullptr) {
566 if (!captured_output->empty()) {
567 captured_output->append("\n\n");
568 }
569 captured_output->append(help);
570 } else {
571 std::cout << help << "\n";
572 }
573 }
574
575 return status;
576}
577
578bool CommandRegistry::HasCommand(const std::string& name) const {
579 return Get(name) != nullptr;
580}
581
585
586} // namespace cli
587} // 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:28
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.
void RegisterHandlers(std::vector< std::unique_ptr< resources::CommandHandler > > handlers)
Register a set of command handlers (idempotent)
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 > > CreateCliCommandHandlers()
Factory function to create all CLI-level command handlers.