yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
wasm_terminal_bridge.cc
Go to the documentation of this file.
1
10#ifdef __EMSCRIPTEN__
11
12#include <cstdint>
13#include <fstream>
14#include <memory>
15#include <optional>
16#include <set>
17#include <string>
18#include <vector>
19
20#include <emscripten.h>
21#include <emscripten/bind.h>
22
23#include "absl/status/status.h"
24#include "absl/strings/ascii.h"
25#include "absl/strings/match.h"
26#include "absl/strings/numbers.h"
27#include "absl/strings/str_cat.h"
28#include "absl/strings/str_join.h"
29#include "absl/strings/str_split.h"
30#include "absl/strings/string_view.h"
31#include "absl/types/span.h"
40#include "cli/util/ansi_style.h"
41#include "nlohmann/json.hpp"
42#include "rom/rom.h"
43
44namespace yaze::app {
45extern editor::EditorManager* GetGlobalEditorManager();
46}
47
48namespace {
49
50constexpr char kTerminalUploadPath[] = "/.yaze/roms/terminal_upload.sfc";
51// xterm.js renders ANSI sequences, so registry help keeps its colors here.
52constexpr yaze::cli::util::AnsiStyle kTerminalStyle{true};
53
54struct BridgeState {
55 std::unique_ptr<yaze::cli::BrowserAIService> ai_service;
56 std::string last_output;
57 std::string provider = yaze::cli::kProviderGemini;
58 std::string model = "gemini-2.5-flash";
59 std::string api_base;
60 std::string api_key;
61 bool initialized = false;
62
63 void Initialize() {
64 if (!initialized) {
66 initialized = true;
67 }
68 }
69
70 void SetupAIService() {
71 yaze::cli::BrowserAIConfig config;
72 config.provider = provider.empty() ? yaze::cli::kProviderGemini : provider;
73 config.api_key = api_key;
74 config.model = model;
75 config.api_base = api_base;
76 config.verbose = false;
77 auto http_client = std::make_unique<yaze::net::EmscriptenHttpClient>();
78 ai_service = std::make_unique<yaze::cli::BrowserAIService>(
79 config, std::move(http_client));
80 if (yaze::Rom* rom = GetActiveRom()) {
81 ai_service->SetRomContext(rom);
82 }
83 }
84
85 void ConfigureAI(const char* provider_value, const char* model_value,
86 const char* api_base_value, const char* api_key_value) {
87 if (provider_value) {
88 provider = provider_value;
89 }
90 if (model_value) {
91 model = model_value;
92 }
93 if (api_base_value) {
94 api_base = api_base_value;
95 }
96 if (api_key_value) {
97 api_key = api_key_value;
98 }
99 SetupAIService();
100 }
101
102 // The ROM open in the editor, which the terminal operates on.
103 yaze::Rom* GetActiveRom() const {
104 auto* manager = yaze::app::GetGlobalEditorManager();
105 return manager ? manager->GetCurrentRom() : nullptr;
106 }
107
108 yaze::Rom* GetLoadedRom() const {
109 yaze::Rom* rom = GetActiveRom();
110 return rom && rom->is_loaded() ? rom : nullptr;
111 }
112
113 const char* Return(std::string output) {
114 last_output = std::move(output);
115 return last_output.c_str();
116 }
117};
118
119BridgeState g_bridge;
120
121} // namespace
122
123namespace yaze {
124namespace cli {
125
126BrowserAIService* GetGlobalBrowserAIService() {
127 return g_bridge.ai_service.get();
128}
129
131 return g_bridge.GetActiveRom();
132}
133
134} // namespace cli
135} // namespace yaze
136
137namespace {
138
139using Args = std::vector<std::string>;
140
141EM_JS(void, z3ed_print_to_terminal, (const char* text), {
142 if (window.z3edTerminal) {
143 window.z3edTerminal.print(UTF8ToString(text));
144 } else {
145 console.log(UTF8ToString(text));
146 }
147});
148
149EM_JS(void, z3ed_error_to_terminal, (const char* text), {
150 if (window.z3edTerminal) {
151 window.z3edTerminal.printError(UTF8ToString(text));
152 } else {
153 console.error(UTF8ToString(text));
154 }
155});
156
157// Whitespace tokenization; quoted arguments are not supported.
158Args ParseCommand(absl::string_view command) {
159 return absl::StrSplit(command, absl::ByAnyChar(" \t"), absl::SkipEmpty());
160}
161
162std::string ErrorJson(absl::string_view message) {
163 return nlohmann::json{{"error", message}}.dump();
164}
165
166std::optional<int> ParseHexId(absl::string_view text) {
167 int value = 0;
168 if (!absl::SimpleHexAtoi(text, &value)) {
169 return std::nullopt;
170 }
171 return value;
172}
173
174std::string ExecuteRegistryCommand(const std::string& name, const Args& args) {
175 std::string output;
176 absl::Status status = yaze::cli::CommandRegistry::Instance().Execute(
177 name, args, g_bridge.GetActiveRom(), &output);
178 if (!status.ok()) {
179 return absl::StrCat("Command failed: ", status.message());
180 }
181 return output.empty() ? "Command executed successfully" : output;
182}
183
184std::string HandleHelp(const Args& args) {
185 auto& registry = yaze::cli::CommandRegistry::Instance();
186 if (args.size() < 2) {
187 return registry.GenerateCompleteHelp(kTerminalStyle);
188 }
189 if (registry.HasCommand(args[1])) {
190 return registry.GenerateHelp(args[1], kTerminalStyle);
191 }
192 return registry.GenerateCategoryHelp(args[1], kTerminalStyle);
193}
194
195std::string HandleRom(const Args& args) {
196 if (args.size() < 2) {
197 return absl::StrCat("Usage: rom load <path> | rom info\n",
199 }
200 const std::string& subcommand = args[1];
201 if (subcommand == "load") {
202 if (args.size() < 3) {
203 return "Usage: rom load <path>";
204 }
205 yaze::app::wasm::TriggerRomLoad(args[2]);
206 return absl::StrCat("Requesting load for: ", args[2],
207 ". Check application log.");
208 }
209 if (subcommand == "info") {
210 yaze::Rom* rom = g_bridge.GetLoadedRom();
211 if (rom == nullptr) {
212 return "No ROM loaded.";
213 }
214 return absl::StrCat("ROM Info:\n Size: ", rom->size(),
215 " bytes\n Title: ", rom->title(), "\n");
216 }
217
218 auto invocation =
219 yaze::cli::ResolveRomSubcommand(absl::MakeConstSpan(args).subspan(1));
220 if (!invocation.ok()) {
221 return std::string(invocation.status().message());
222 }
223 return ExecuteRegistryCommand(invocation->command, invocation->args);
224}
225
226std::string HandleAi(const Args& args) {
227 if (!g_bridge.ai_service) {
228 g_bridge.SetupAIService();
229 }
230 if (!g_bridge.ai_service) {
231 return "AI service unavailable";
232 }
233 if (args.size() < 2) {
234 return "AI command requires a prompt. Usage: ai <prompt>";
235 }
236
237 const std::string prompt = absl::StrJoin(args.begin() + 1, args.end(), " ");
238 auto response = g_bridge.ai_service->GenerateResponse(prompt);
239 if (!response.ok()) {
240 return absl::StrCat("AI error: ", response.status().message());
241 }
242 return response->text_response;
243}
244
245std::string HandleEditor(const Args& args) {
246 auto* editor_manager = yaze::app::GetGlobalEditorManager();
247 if (!editor_manager) {
248 return "Error: Editor manager not available";
249 }
250
251 if (args.size() > 2 && args[1] == "switch") {
252 const std::string target = absl::AsciiStrToLower(args[2]);
253 for (size_t i = 0; i < yaze::editor::kEditorNames.size(); ++i) {
254 const std::string name =
255 absl::AsciiStrToLower(yaze::editor::kEditorNames[i]);
256 if (name == target) {
257 editor_manager->SwitchToEditor(
258 static_cast<yaze::editor::EditorType>(i));
259 return absl::StrCat("Switched to ", yaze::editor::kEditorNames[i]);
260 }
261 }
262 return absl::StrCat("Unknown editor: ", args[2]);
263 }
264
265 if (args.size() > 3 && args[1] == "card") {
266 auto* current_editor = editor_manager->GetCurrentEditor();
267 if (current_editor &&
269 return "Panel visibility is now controlled via Layout Designer. Use "
270 "'editor layout' commands.";
271 }
272 return "Panel control not supported for current editor";
273 }
274
275 if (args.size() > 2 && args[1] == "debug" && args[2] == "toggle") {
276 return "Debug controls are now accessed via the Layout Designer. "
277 "Use the canvas debug panel or layout commands.";
278 }
279
280 return "Usage: editor switch <name>";
281}
282
283std::string HandleDungeon(const Args& args) {
284 auto* editor_manager = yaze::app::GetGlobalEditorManager();
285 if (!editor_manager) {
286 return "Error: Editor manager not available";
287 }
288
289 auto* current_editor = editor_manager->GetCurrentEditor();
290 if (!current_editor ||
292 return "Error: Dungeon editor is not active. Use 'editor switch dungeon' "
293 "first.";
294 }
295 auto* dungeon_editor =
297
298 if (args.size() > 2 && args[1] == "room") {
299 std::optional<int> room_id = ParseHexId(args[2]);
300 if (!room_id) {
301 return "Invalid room ID (hex required)";
302 }
303 dungeon_editor->FocusRoom(*room_id);
304 return absl::StrCat("Focused room ", args[2]);
305 }
306
307 if (args.size() > 2 && args[1] == "select_object") {
308 std::optional<int> object_id = ParseHexId(args[2]);
309 if (!object_id) {
310 return "Invalid object ID (hex required)";
311 }
312 dungeon_editor->SelectObject(*object_id);
313 return absl::StrCat("Selected object ", args[2]);
314 }
315
316 if (args.size() > 2 && args[1] == "agent_mode") {
317 const bool enabled = args[2] == "on" || args[2] == "true";
318 dungeon_editor->SetAgentMode(enabled);
319 return absl::StrCat("Agent mode ", enabled ? "enabled" : "disabled");
320 }
321
322 return "Usage: dungeon room <hex> | select_object <hex> | agent_mode "
323 "<on|off>";
324}
325
326using BuiltinHandler = std::string (*)(const Args&);
327
328struct BuiltinCommand {
329 absl::string_view name;
330 BuiltinHandler handler;
331};
332
333// Commands handled by the bridge itself rather than the command registry.
334constexpr BuiltinCommand kBuiltinCommands[] = {
335 {"help", &HandleHelp}, {"rom", &HandleRom}, {"ai", &HandleAi},
336 {"editor", &HandleEditor}, {"dungeon", &HandleDungeon},
337};
338
339std::string ProcessCommandInternal(absl::string_view command_str) {
340 const Args args = ParseCommand(command_str);
341 if (args.empty()) {
342 return "No command provided";
343 }
344
345 for (const BuiltinCommand& builtin : kBuiltinCommands) {
346 if (args[0] == builtin.name) {
347 return builtin.handler(args);
348 }
349 }
350
351 auto& registry = yaze::cli::CommandRegistry::Instance();
352 if (registry.HasCommand(args[0])) {
353 return ExecuteRegistryCommand(args[0], Args(args.begin() + 1, args.end()));
354 }
355 if (!registry.GetCommandsInCategory(args[0]).empty()) {
356 return registry.GenerateCategoryHelp(args[0], kTerminalStyle);
357 }
358
359 return absl::StrCat("Unknown command: ", args[0],
360 "\nType 'help' for available commands");
361}
362
363std::vector<std::string> GetCompletionsInternal(absl::string_view partial) {
364 const Args words = ParseCommand(partial);
365 const bool completing_first_word =
366 words.empty() || (words.size() == 1 && !absl::EndsWith(partial, " "));
367
368 std::set<std::string> completions;
369 if (completing_first_word) {
370 const absl::string_view prefix =
371 words.empty() ? absl::string_view() : words[0];
372 auto add = [&](absl::string_view candidate) {
373 if (absl::StartsWith(candidate, prefix)) {
374 completions.emplace(candidate);
375 }
376 };
377 auto& registry = yaze::cli::CommandRegistry::Instance();
378 for (const BuiltinCommand& builtin : kBuiltinCommands) {
379 add(builtin.name);
380 }
381 for (const std::string& category : registry.GetCategories()) {
382 add(category);
383 for (const std::string& command :
384 registry.GetCommandsInCategory(category)) {
385 add(command);
386 }
387 }
388 } else if (words[0] == "rom") {
389 const absl::string_view prefix =
390 words.size() > 1 ? absl::string_view(words[1]) : absl::string_view();
391 std::vector<absl::string_view> subcommands = {"load", "info"};
392 for (absl::string_view name : yaze::cli::RomSubcommandNames()) {
393 subcommands.push_back(name);
394 }
395 for (absl::string_view subcommand : subcommands) {
396 if (absl::StartsWith(subcommand, prefix)) {
397 completions.insert(absl::StrCat("rom ", subcommand));
398 }
399 }
400 }
401
402 return {completions.begin(), completions.end()};
403}
404
405} // namespace
406
407extern "C" {
408
414EMSCRIPTEN_KEEPALIVE
415const char* Z3edProcessCommand(const char* command) {
416 g_bridge.Initialize();
417
418 if (!command) {
419 return g_bridge.Return("Invalid command");
420 }
421
422 const char* output = g_bridge.Return(ProcessCommandInternal(command));
423 z3ed_print_to_terminal(output);
424 return output;
425}
426
432EMSCRIPTEN_KEEPALIVE
433const char* Z3edGetCompletions(const char* partial) {
434 g_bridge.Initialize();
435
436 if (!partial) {
437 return g_bridge.Return("[]");
438 }
439 return g_bridge.Return(
440 nlohmann::json(GetCompletionsInternal(partial)).dump());
441}
442
447EMSCRIPTEN_KEEPALIVE
448void Z3edSetApiKey(const char* api_key) {
449 g_bridge.ConfigureAI(nullptr, nullptr, nullptr, api_key ? api_key : "");
450}
451
459EMSCRIPTEN_KEEPALIVE
460void Z3edConfigureAI(const char* provider, const char* model,
461 const char* api_base, const char* api_key) {
462 g_bridge.Initialize();
463 g_bridge.ConfigureAI(provider ? provider : yaze::cli::kProviderGemini,
464 model ? model : "", api_base ? api_base : "",
465 api_key ? api_key : "");
466}
467
472EMSCRIPTEN_KEEPALIVE
473int Z3edIsReady() {
474 g_bridge.Initialize();
475 return g_bridge.initialized ? 1 : 0;
476}
477
484EMSCRIPTEN_KEEPALIVE
485int Z3edLoadRomData(const uint8_t* data, size_t size) {
486 if (!data || size == 0) {
487 z3ed_error_to_terminal("Invalid ROM data");
488 return 0;
489 }
490
491 {
492 std::ofstream file(kTerminalUploadPath, std::ios::binary);
493 if (!file || !file.write(reinterpret_cast<const char*>(data),
494 static_cast<std::streamsize>(size))) {
495 z3ed_error_to_terminal("Failed to write to VFS");
496 return 0;
497 }
498 }
499
500 yaze::app::wasm::TriggerRomLoad(kTerminalUploadPath);
501
502 z3ed_print_to_terminal("ROM uploaded to VFS. Loading...");
503 return 1;
504}
505
510EMSCRIPTEN_KEEPALIVE
511const char* Z3edGetRomInfo() {
512 yaze::Rom* rom = g_bridge.GetLoadedRom();
513 if (rom == nullptr) {
514 return g_bridge.Return(ErrorJson("No ROM loaded"));
515 }
516
517 return g_bridge.Return(nlohmann::json{
518 {"loaded", true},
519 {"size", rom->size()},
520 {"title", rom->title()},
521 }
522 .dump());
523}
524
530EMSCRIPTEN_KEEPALIVE
531const char* Z3edQueryResource(const char* query) {
532 g_bridge.Initialize();
533
534 if (!query) {
535 return g_bridge.Return(ErrorJson("Invalid query"));
536 }
537
538 yaze::Rom* rom = g_bridge.GetLoadedRom();
539 if (rom == nullptr) {
540 return g_bridge.Return(ErrorJson("No ROM loaded"));
541 }
542
543 constexpr char kResourceSearchCommand[] = "resource-search";
544 auto& registry = yaze::cli::CommandRegistry::Instance();
545 if (!registry.HasCommand(kResourceSearchCommand)) {
546 return g_bridge.Return(ErrorJson("Resource query failed"));
547 }
548
549 const Args args = {"--query", query, "--format", "json"};
550 std::string output;
551 absl::Status status =
552 registry.Execute(kResourceSearchCommand, args, rom, &output);
553 if (!status.ok()) {
554 return g_bridge.Return(ErrorJson("Resource query failed"));
555 }
556 if (output.empty()) {
557 return g_bridge.Return(nlohmann::json{
558 {"status", "success"},
559 {"message", "Query executed but no output returned."},
560 }
561 .dump());
562 }
563 return g_bridge.Return(std::move(output));
564}
565
566} // extern "C"
567
568EMSCRIPTEN_BINDINGS(z3ed_terminal) {
569 emscripten::function("processCommand", &Z3edProcessCommand,
570 emscripten::allow_raw_pointers());
571 emscripten::function("getCompletions", &Z3edGetCompletions,
572 emscripten::allow_raw_pointers());
573 emscripten::function("setApiKey", &Z3edSetApiKey,
574 emscripten::allow_raw_pointers());
575 emscripten::function("configureAI", &Z3edConfigureAI,
576 emscripten::allow_raw_pointers());
577 emscripten::function("isReady", &Z3edIsReady);
578 emscripten::function("getRomInfo", &Z3edGetRomInfo,
579 emscripten::allow_raw_pointers());
580 emscripten::function("queryResource", &Z3edQueryResource,
581 emscripten::allow_raw_pointers());
582}
583
584#endif // __EMSCRIPTEN__
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:52
auto size() const
Definition rom.h:212
bool is_loaded() const
Definition rom.h:199
auto title() const
Definition rom.h:211
static CommandRegistry & Instance()
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.
DungeonEditorV2 - Simplified dungeon editor using component delegation.
EditorType type() const
Definition editor.h:355
EM_JS(void, CallJsAiDriver,(const char *history_json), { if(window.yaze &&window.yaze.ai &&window.yaze.ai.processAgentRequest) { window.yaze.ai.processAgentRequest(UTF8ToString(history_json));} else { console.error("AI Driver not found in window.yaze.ai.processAgentRequest");} })
yaze::editor::EditorManager * GetGlobalEditorManager()
Definition main.cc:138
constexpr char kProviderGemini[]
Definition provider_ids.h:9
absl::StatusOr< CommandInvocation > ResolveRomSubcommand(absl::Span< const std::string > args)
std::string RomSubcommandUsage()
Rom * GetGlobalRom()
BrowserAIService * GetGlobalBrowserAIService()
std::vector< absl::string_view > RomSubcommandNames()
Rom * rom()
Get the current ROM instance.
Editor * current_editor()
Get the currently active editor.
void add(const std::string &id, const std::string &key, const std::string &desc)
constexpr std::array< const char *, 15 > kEditorNames
Definition editor.h:232
const char * Z3edProcessCommand(const char *command)
EMSCRIPTEN_BINDINGS(yaze_debug_inspector)