yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
cli_main.cc
Go to the documentation of this file.
1#include <cstdio>
2#include <cstdlib>
3#include <iostream>
4#include <iterator>
5#include <optional>
6#include <sstream>
7#include <string>
8#include <vector>
9
10#include "absl/flags/declare.h"
11#include "absl/flags/flag.h"
12#include "absl/status/status.h"
13#include "absl/status/statusor.h"
14#include "absl/strings/str_format.h"
15#include "absl/strings/string_view.h"
16#include "cli/cli.h"
17#include "cli/global_flags.h"
19#include "cli/util/ansi_style.h"
20#include "cli/z3ed_ascii_logo.h"
21#include "util/platform_paths.h"
22#include "yaze_config.h"
23
24#ifdef YAZE_ENABLE_AGENT_CLI
26#endif
27
28#ifdef YAZE_HTTP_API_ENABLED
29#include <chrono>
30#include <csignal>
31#include <memory>
32#include <thread>
33
36#include "rom/rom.h"
37#include "util/macro.h"
38#include "zelda3/game_data.h"
39#endif
40
41ABSL_DECLARE_FLAG(bool, quiet);
42ABSL_DECLARE_FLAG(std::string, rom);
43
44#ifdef YAZE_HTTP_API_ENABLED
45ABSL_FLAG(int, http_port, 0, "HTTP API server port (0 = disabled)");
46ABSL_FLAG(std::string, http_host, "localhost",
47 "Address the HTTP API binds; localhost or 127.0.0.1 accepts "
48 "connections from this machine only");
49ABSL_FLAG(bool, http_allow_commands, false,
50 "Serve POST /api/v1/command/execute, which runs any z3ed command, "
51 "including commands that write files");
52#endif
53
54namespace {
55
56using yaze::cli::Cli;
60
61int ReportFailure(const absl::Status& status, const AnsiStyle& style) {
62 std::cerr << "\n"
63 << style.Failure() << "Error:" << style.Reset() << " "
64 << status.message() << "\n";
65 std::cerr << "Use --help for usage information.\n";
66 return EXIT_FAILURE;
67}
68
69int ExitCode(const absl::Status& status, const AnsiStyle& style) {
70 return status.ok() ? EXIT_SUCCESS : ReportFailure(status, style);
71}
72
73void PrintVersion(const AnsiStyle& style) {
74 std::cout << yaze::cli::RenderLogo(style) << "\n";
75 std::cout << absl::StrFormat(" Version %d.%d.%d\n", YAZE_VERSION_MAJOR,
76 YAZE_VERSION_MINOR, YAZE_VERSION_PATCH);
77 std::cout << " Yet Another Zelda3 Editor - Command Line Interface\n";
78 std::cout << " https://github.com/scawful/yaze\n\n";
79}
80
82 Cli::Options options;
83 options.style = style;
84#ifdef YAZE_ENABLE_AGENT_CLI
86#endif
87#ifdef YAZE_HTTP_API_ENABLED
88 options.http_api_available = true;
89#endif
90 return options;
91}
92
93// The command line help was requested for, or an empty list for the general
94// overview. std::nullopt when no help was requested.
95std::optional<std::vector<std::string>> HelpRequest(
96 const GlobalOptions& options) {
97 if (options.help_target.has_value()) {
98 return std::vector<std::string>{*options.help_target};
99 }
100 if (!options.show_help) {
101 return std::nullopt;
102 }
103 if (options.positional.empty() || options.positional[0] == "help") {
104 return std::vector<std::string>{};
105 }
106 return options.positional;
107}
108
110 const std::string saved_rom = absl::GetFlag(FLAGS_rom);
111 const std::vector<std::string> args = {"--rom=self-test.sfc", "rom-info",
112 "--format=json"};
113 absl::StatusOr<GlobalOptions> options = yaze::cli::ParseGlobalFlags(args);
114 const bool works = options.ok() &&
115 absl::GetFlag(FLAGS_rom) == "self-test.sfc" &&
116 options->positional ==
117 std::vector<std::string>{"rom-info", "--format=json"};
118 absl::SetFlag(&FLAGS_rom, saved_rom);
119 return works;
120}
121
123 std::ostringstream out;
124 Cli(registry, Cli::Options{}, out).PrintOverview();
125 return !out.str().empty();
126}
127
128int RunSelfTest(const AnsiStyle& style) {
129 CommandRegistry& registry = CommandRegistry::Instance();
130 struct Check {
131 const char* name;
132 bool passed;
133 };
134 // Asset checks must pass from portable, bundle and FHS package layouts,
135 // without depending on the source checkout.
136 const Check checks[] = {
137 {"Command registry populated", registry.Count() > 0},
138 {"Global flag parsing", GlobalFlagParsingWorks()},
139 {"Help overview renders", OverviewRenders(registry)},
140 {"Agent catalogue asset available",
141 yaze::util::PlatformPaths::FindAsset("agent/prompt_catalogue.yaml")
142 .ok()},
143 {"Overworld patch asset available",
145 "patches/Overworld/TailMapExpansion.asm")
146 .ok()},
147 };
148
149 std::cout << "\n"
150 << style.Heading() << "=== z3ed Self-Test ===" << style.Reset()
151 << "\n\n";
152 int failed = 0;
153 for (const Check& check : checks) {
154 if (check.passed) {
155 std::cout << " " << style.Success() << "✓" << style.Reset();
156 } else {
157 std::cout << " " << style.Failure() << "✗" << style.Reset();
158 ++failed;
159 }
160 std::cout << " " << check.name << "\n";
161 }
162#ifdef YAZE_HTTP_API_ENABLED
163 std::cout << " HTTP API: compiled in\n";
164#else
165 std::cout << " HTTP API: not compiled in\n";
166#endif
167
168 const int passed = static_cast<int>(std::size(checks)) - failed;
169 std::cout << "\n"
170 << style.Heading() << "=== Results ===" << style.Reset() << "\n";
171 std::cout << " Passed: " << style.Success() << passed << style.Reset()
172 << "\n";
173 std::cout << " Failed: " << style.Failure() << failed << style.Reset()
174 << "\n";
175
176 if (failed == 0) {
177 std::cout << "\n"
178 << style.Success() << "All self-tests passed!" << style.Reset()
179 << "\n\n";
180 return EXIT_SUCCESS;
181 }
182 std::cout << "\n"
183 << style.Failure() << "Some self-tests failed." << style.Reset()
184 << "\n\n";
185 return EXIT_FAILURE;
186}
187
188#ifdef YAZE_HTTP_API_ENABLED
189volatile std::sig_atomic_t g_shutdown_requested = 0;
190
191void RequestShutdown(int) {
192 g_shutdown_requested = 1;
193}
194
195void WaitForShutdownSignal() {
196 std::signal(SIGINT, RequestShutdown);
197 std::signal(SIGTERM, RequestShutdown);
198 while (g_shutdown_requested == 0) {
199 std::this_thread::sleep_for(std::chrono::milliseconds(200));
200 }
201}
202
203bool IsLoopbackHost(absl::string_view host) {
204 return host == "localhost" || host == "127.0.0.1";
205}
206
207// Owns the HTTP API server and the ROM-backed services its endpoints read.
208class HttpApiSession {
209 public:
210 absl::Status Start(int port, const std::string& host, bool allow_commands) {
211 if (port <= 0 || port > 65535) {
212 return absl::InvalidArgumentError(
213 absl::StrFormat("--http-port must be 1-65535, got %d", port));
214 }
215 if (host.empty()) {
216 return absl::InvalidArgumentError("--http-host must not be empty");
217 }
218 server_ = std::make_unique<yaze::cli::api::HttpServer>();
219 if (render_service_) {
220 server_->SetRenderServiceSource(
221 [service = render_service_.get()] { return service; });
222 }
223 server_->SetCommandExecutionEnabled(allow_commands);
224 const bool loopback = IsLoopbackHost(host);
225 if (!loopback) {
226 server_->SetBindAddress(host);
227 }
228 absl::Status status = server_->Start(port, loopback);
229 if (!status.ok()) {
230 server_.reset();
231 }
232 return status;
233 }
234
235 // Serves headless dungeon renders for `rom_path`. Call before Start().
236 absl::Status LoadRenderService(const std::string& rom_path) {
237 auto rom = std::make_unique<yaze::Rom>();
238 RETURN_IF_ERROR(rom->LoadFromFile(rom_path));
239 auto game_data = std::make_unique<yaze::zelda3::GameData>();
241 rom_ = std::move(rom);
242 game_data_ = std::move(game_data);
243 render_service_ = std::make_unique<yaze::app::service::RenderService>(
244 rom_.get(), game_data_.get());
245 return absl::OkStatus();
246 }
247
248 bool IsRunning() const { return server_ && server_->IsRunning(); }
249
250 private:
251 std::unique_ptr<yaze::Rom> rom_;
252 std::unique_ptr<yaze::zelda3::GameData> game_data_;
253 std::unique_ptr<yaze::app::service::RenderService> render_service_;
254 // Declared last so it is destroyed first: the server thread must stop
255 // before the services it reads are freed.
256 std::unique_ptr<yaze::cli::api::HttpServer> server_;
257};
258#endif
259
260} // namespace
261
262int main(int argc, char* argv[]) {
263 const AnsiStyle out_style = AnsiStyle::ForStream(stdout);
264 const AnsiStyle err_style = AnsiStyle::ForStream(stderr);
265
266 std::vector<std::string> args;
267 if (argc > 1) {
268 args.assign(argv + 1, argv + argc);
269 }
270 absl::StatusOr<GlobalOptions> parsed = yaze::cli::ParseGlobalFlags(args);
271 if (!parsed.ok()) {
272 return ReportFailure(parsed.status(), err_style);
273 }
274 const GlobalOptions& options = *parsed;
275
276 if (options.show_version) {
277 PrintVersion(out_style);
278 return EXIT_SUCCESS;
279 }
280 if (options.self_test) {
281 return RunSelfTest(out_style);
282 }
283
284 CommandRegistry& registry = CommandRegistry::Instance();
285#ifdef YAZE_ENABLE_AGENT_CLI
287#endif
288
289 if (options.export_schemas) {
290 std::cout << registry.ExportFunctionSchemas() << "\n";
291 return EXIT_SUCCESS;
292 }
293
294 const Cli cli(registry, MakeCliOptions(out_style), std::cout);
295
296 if (std::optional<std::vector<std::string>> words = HelpRequest(options)) {
297 return ExitCode(cli.PrintScopedHelp(*words), err_style);
298 }
299 if (options.list_commands) {
300 cli.PrintCommandSummary();
301 return EXIT_SUCCESS;
302 }
303
304#ifdef YAZE_HTTP_API_ENABLED
305 HttpApiSession http_api;
306 if (const int port = absl::GetFlag(FLAGS_http_port); port != 0) {
307 const bool quiet = absl::GetFlag(FLAGS_quiet);
308 const std::string rom_path = absl::GetFlag(FLAGS_rom);
309 if (!rom_path.empty()) {
310 absl::Status status = http_api.LoadRenderService(rom_path);
311 if (!status.ok() && !quiet) {
312 std::cerr << err_style.Highlight() << "Warning:" << err_style.Reset()
313 << " HTTP render endpoints disabled: " << status.message()
314 << "\n";
315 }
316 }
317
318 const std::string host = absl::GetFlag(FLAGS_http_host);
319 const bool allow_commands = absl::GetFlag(FLAGS_http_allow_commands);
320 absl::Status status = http_api.Start(port, host, allow_commands);
321 if (!status.ok()) {
322 std::cerr << "\n"
323 << err_style.Failure() << "Warning:" << err_style.Reset()
324 << " Failed to start HTTP API server: " << status.message()
325 << "\nContinuing without HTTP API...\n\n";
326 } else {
327 if (!IsLoopbackHost(host)) {
328 std::cerr << err_style.Highlight() << "Warning:" << err_style.Reset()
329 << " HTTP API bound to " << host
330 << "; other machines can reach it.\n";
331 }
332 if (!quiet) {
333 std::cout << out_style.Success() << "✓" << out_style.Reset()
334 << " HTTP API server started on " << host << ":" << port
335 << "\n";
336 std::cout << " Health check: http://" << host << ":" << port
337 << "/api/v1/health\n";
338 std::cout << " Models list: http://" << host << ":" << port
339 << "/api/v1/models\n";
340 std::cout << " Command execution: "
341 << (allow_commands
342 ? "enabled"
343 : "disabled (pass --http-allow-commands to enable)")
344 << "\n\n";
345 }
346 }
347
348 if (options.positional.empty() && http_api.IsRunning()) {
349 if (!quiet) {
350 std::cout << "HTTP API server running. Press Ctrl+C to exit.\n";
351 }
352 WaitForShutdownSignal();
353 return EXIT_SUCCESS;
354 }
355 }
356#endif
357
358 return ExitCode(cli.Run(options.positional), err_style);
359}
absl::Status LoadFromFile(const std::string &filename, const LoadOptions &options=LoadOptions::Defaults())
Definition rom.cc:632
Single source of truth for all z3ed commands.
size_t Count() const
Get total command count.
constexpr absl::string_view Heading() const
Definition ansi_style.h:22
constexpr absl::string_view Failure() const
Definition ansi_style.h:27
constexpr absl::string_view Success() const
Definition ansi_style.h:26
constexpr absl::string_view Reset() const
Definition ansi_style.h:28
static absl::StatusOr< std::filesystem::path > FindAsset(const std::string &relative_path)
Find an asset file in multiple standard locations.
int main(int argc, char *argv[])
Definition cli_main.cc:262
ABSL_DECLARE_FLAG(bool, quiet)
ABSL_FLAG(std::string, emu_rom, "", "Path to the ROM file to load.")
int RunSelfTest(const AnsiStyle &style)
Definition cli_main.cc:128
int ReportFailure(const absl::Status &status, const AnsiStyle &style)
Definition cli_main.cc:61
void PrintVersion(const AnsiStyle &style)
Definition cli_main.cc:73
int ExitCode(const absl::Status &status, const AnsiStyle &style)
Definition cli_main.cc:69
bool OverviewRenders(CommandRegistry &registry)
Definition cli_main.cc:122
std::optional< std::vector< std::string > > HelpRequest(const GlobalOptions &options)
Definition cli_main.cc:95
Cli::Options MakeCliOptions(const AnsiStyle &style)
Definition cli_main.cc:81
absl::Status HandleAgentCommand(const std::vector< std::string > &arg_vec)
Unified agent command handler using CommandRegistry.
Definition agent.cc:113
std::string RenderLogo(const util::AnsiStyle &style)
absl::StatusOr< GlobalOptions > ParseGlobalFlags(absl::Span< const std::string > args)
Rom * rom()
Get the current ROM instance.
::yaze::zelda3::GameData * game_data()
Get the current game data instance.
absl::Status LoadGameData(Rom &rom, GameData &data, const LoadOptions &options)
Loads all Zelda3-specific game data from a generic ROM.
Definition game_data.cc:123
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
bool http_api_available
Definition cli.h:28
util::AnsiStyle style
Definition cli.h:29
AgentHandler agent_handler
Definition cli.h:27
std::vector< std::string > positional
std::optional< std::string > help_target