yaze 0.2.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
tui.cc
Go to the documentation of this file.
1#include "tui.h"
2
3#include <ftxui/component/component.hpp>
4#include <ftxui/component/screen_interactive.hpp>
5#include <ftxui/dom/elements.hpp>
6#include <ftxui/screen/screen.hpp>
7
8#include "absl/strings/str_cat.h"
9#include "util/bps.h"
10
11namespace yaze {
12namespace cli {
13
14using namespace ftxui;
15
16namespace {
17void SwitchComponents(ftxui::ScreenInteractive &screen, LayoutID layout) {
18 screen.ExitLoopClosure()();
19 screen.Clear();
20 app_context.current_layout = layout;
21
22 // Clear the buffer
23 // std::cout << "\033[2J\033[1;1H";
24}
25
26bool HandleInput(ftxui::ScreenInteractive &screen, ftxui::Event &event,
27 int &selected) {
28 if (event == Event::ArrowDown || event == Event::Character('j')) {
29 selected++;
30 return true;
31 }
32 if (event == Event::ArrowUp || event == Event::Character('k')) {
33 if (selected != 0) selected--;
34 return true;
35 }
36 if (event == Event::Character('q')) {
38 return true;
39 }
40 return false;
41}
42
43void ReturnIfRomNotLoaded(ftxui::ScreenInteractive &screen) {
44 if (!app_context.rom.is_loaded()) {
45 app_context.error_message = "No ROM loaded.";
47 }
48}
49
50void ApplyBpsPatchComponent(ftxui::ScreenInteractive &screen) {
51 // Text inputs for user to enter file paths (or any relevant data).
52 static std::string patch_file;
53 static std::string base_file;
54
55 auto patch_file_input = Input(&patch_file, "Patch file path");
56 auto base_file_input = Input(&base_file, "Base file path");
57
58 // Button to apply the patch.
59 auto apply_button = Button("Apply Patch", [&] {
60 std::vector<uint8_t> source = app_context.rom.vector();
61 // auto source_contents = core::LoadFile(base_file);
62 // std::copy(source_contents.begin(), source_contents.end(),
63 // std::back_inserter(source));
64 std::vector<uint8_t> patch;
65 auto patch_contents = core::LoadFile(patch_file);
66 std::copy(patch_contents.begin(), patch_contents.end(),
67 std::back_inserter(patch));
68 std::vector<uint8_t> patched;
69
70 try {
71 util::ApplyBpsPatch(source, patch, patched);
72 } catch (const std::runtime_error &e) {
73 app_context.error_message = e.what();
75 return;
76 }
77
78 // Write the patched data to a new file.
79 // Find the . in the base file name and insert _patched before it.
80 auto dot_pos = base_file.find_last_of('.');
81 auto patched_file = base_file.substr(0, dot_pos) + "_patched" +
82 base_file.substr(dot_pos, base_file.size() - dot_pos);
83 std::ofstream file(patched_file, std::ios::binary);
84 if (!file.is_open()) {
85 app_context.error_message = "Could not open file for writing.";
87 return;
88 }
89
90 file.write(reinterpret_cast<const char *>(patched.data()), patched.size());
91
92 // If the patch was applied successfully, return to the main menu.
94 });
95
96 // Button to return to main menu without applying.
97 auto return_button = Button("Back to Main Menu", [&] {
99 });
100
101 // Layout components vertically.
102 auto container = Container::Vertical({
103 patch_file_input,
104 base_file_input,
105 apply_button,
106 return_button,
107 });
108
109 auto renderer = Renderer(container, [&] {
110 return vbox({text("Apply BPS Patch") | center, separator(),
111 text("Enter Patch File:"), patch_file_input->Render(),
112 text("Enter Base File:"), base_file_input->Render(),
113 separator(),
114 hbox({
115 apply_button->Render() | center,
116 separator(),
117 return_button->Render() | center,
118 }) | center}) |
119 center;
120 });
121
122 screen.Loop(renderer);
123}
124
125void GenerateSaveFileComponent(ftxui::ScreenInteractive &screen) {
126 // Produce a list of ftxui::Checkbox for items and values to set
127 // Link to the past items include Bow, Boomerang, etc.
128
129 const static std::vector<std::string> items = {"Bow",
130 "Boomerang",
131 "Hookshot",
132 "Bombs",
133 "Magic Powder",
134 "Fire Rod",
135 "Ice Rod",
136 "Lantern",
137 "Hammer",
138 "Shovel",
139 "Flute",
140 "Bug Net",
141 "Book of Mudora",
142 "Cane of Somaria",
143 "Cane of Byrna",
144 "Magic Cape",
145 "Magic Mirror",
146 "Pegasus Boots",
147 "Flippers",
148 "Moon Pearl",
149 "Bottle 1",
150 "Bottle 2",
151 "Bottle 3",
152 "Bottle 4"};
153
154 constexpr size_t kNumItems = 28;
155 std::array<bool, kNumItems> values = {};
156 auto checkboxes = Container::Vertical({});
157 for (size_t i = 0; i < items.size(); i += 4) {
158 auto row = Container::Horizontal({});
159 for (size_t j = 0; j < 4 && (i + j) < items.size(); ++j) {
160 row->Add(
161 Checkbox(absl::StrCat(items[i + j], " ").data(), &values[i + j]));
162 }
163 checkboxes->Add(row);
164 }
165
166 // border container for sword, shield, armor with radioboxes
167 // to select the current item
168 // sword, shield, armor
169
170 static int sword = 0;
171 static int shield = 0;
172 static int armor = 0;
173
174 const std::vector<std::string> sword_items = {"Fighter", "Master", "Tempered",
175 "Golden"};
176 const std::vector<std::string> shield_items = {"Small", "Fire", "Mirror"};
177 const std::vector<std::string> armor_items = {"Green", "Blue", "Red"};
178
179 auto sword_radiobox = Radiobox(&sword_items, &sword);
180 auto shield_radiobox = Radiobox(&shield_items, &shield);
181 auto armor_radiobox = Radiobox(&armor_items, &armor);
182 auto equipment_container = Container::Vertical({
183 sword_radiobox,
184 shield_radiobox,
185 armor_radiobox,
186 });
187
188 auto save_button = Button("Generate Save File", [&] {
189 // Generate the save file here.
190 // You can use the values vector to determine which items are checked.
191 // After generating the save file, you could either stay here or return to
192 // the main menu.
193 });
194
195 auto back_button =
196 Button("Back", [&] { SwitchComponents(screen, LayoutID::kMainMenu); });
197
198 auto container = Container::Vertical({
199 checkboxes,
200 equipment_container,
201 save_button,
202 back_button,
203 });
204
205 auto renderer = Renderer(container, [&] {
206 return vbox({text("Generate Save File") | center, separator(),
207 text("Select items to include in the save file:"),
208 checkboxes->Render(), separator(),
209 equipment_container->Render(), separator(),
210 hbox({
211 save_button->Render() | center,
212 separator(),
213 back_button->Render() | center,
214 }) | center}) |
215 center;
216 });
217
218 screen.Loop(renderer);
219}
220
221void LoadRomComponent(ftxui::ScreenInteractive &screen) {
222 static std::string rom_file;
223 auto rom_file_input = Input(&rom_file, "ROM file path");
224
225 auto load_button = Button("Load ROM", [&] {
226 // Load the ROM file here.
227 auto rom_status = app_context.rom.LoadFromFile(rom_file);
228 if (!rom_status.ok()) {
229 app_context.error_message = rom_status.message();
231 return;
232 }
233 // If the ROM is loaded successfully, switch to the main menu.
235 });
236
237 auto back_button =
238 Button("Back", [&] { SwitchComponents(screen, LayoutID::kMainMenu); });
239
240 auto container = Container::Vertical({
241 rom_file_input,
242 load_button,
243 back_button,
244 });
245
246 auto renderer = Renderer(container, [&] {
247 return vbox({text("Load ROM") | center, separator(),
248 text("Enter ROM File:"), rom_file_input->Render(), separator(),
249 hbox({
250 load_button->Render() | center,
251 separator(),
252 back_button->Render() | center,
253 }) | center}) |
254 center;
255 });
256
257 screen.Loop(renderer);
258}
259
260Element ColorBox(const Color &color) {
261 return ftxui::text(" ") | ftxui::bgcolor(color);
262}
263
264void PaletteEditorComponent(ftxui::ScreenInteractive &screen) {
265 ReturnIfRomNotLoaded(screen);
266
267 auto back_button =
268 Button("Back", [&] { SwitchComponents(screen, LayoutID::kMainMenu); });
269
270 static auto palette_groups = app_context.rom.palette_group();
271 static std::vector<gfx::PaletteGroup> ftx_palettes = {
272 palette_groups.swords,
273 palette_groups.shields,
274 palette_groups.armors,
275 palette_groups.overworld_main,
276 palette_groups.overworld_aux,
277 palette_groups.global_sprites,
278 palette_groups.sprites_aux1,
279 palette_groups.sprites_aux2,
280 palette_groups.sprites_aux3,
281 palette_groups.dungeon_main,
282 palette_groups.overworld_mini_map,
283 palette_groups.grass,
284 palette_groups.object_3d,
285 };
286
287 // Create a list of palette groups to pick from
288 static int selected_palette_group = 0;
289 static std::vector<std::string> palette_group_names;
290 if (palette_group_names.empty()) {
291 for (size_t i = 0; i < 14; ++i) {
292 palette_group_names.push_back(gfx::kPaletteCategoryNames[i].data());
293 }
294 }
295
296 static bool show_palette_editor = false;
297 static std::vector<std::vector<Element>> palette_elements;
298
299 const auto load_palettes_from_current_group = [&]() {
300 auto palette_group = ftx_palettes[selected_palette_group];
301 palette_elements.clear();
302 // Create a list of colors to display in the palette editor.
303 for (size_t i = 0; i < palette_group.size(); ++i) {
304 palette_elements.push_back(std::vector<Element>());
305 for (size_t j = 0; j < palette_group[i].size(); ++j) {
306 auto color = palette_group[i][j];
307 palette_elements[i].push_back(
308 ColorBox(Color::RGB(color.rgb().x, color.rgb().y, color.rgb().z)));
309 }
310 }
311 };
312
313 if (show_palette_editor) {
314 if (palette_elements.empty()) {
315 load_palettes_from_current_group();
316 }
317
318 auto palette_grid = Container::Vertical({});
319 for (const auto &element : palette_elements) {
320 auto row = Container::Horizontal({});
321 for (const auto &color : element) {
322 row->Add(Renderer([color] { return color; }));
323 }
324 palette_grid->Add(row);
325 }
326
327 // Create a button to save the changes to the palette.
328 auto save_button = Button("Save Changes", [&] {
329 // Save the changes to the palette here.
330 // You can use the current_palette vector to determine the new colors.
331 // After saving the changes, you could either stay here or return to the
332 // main menu.
333 });
334
335 auto back_button = Button("Back", [&] {
336 show_palette_editor = false;
337 screen.ExitLoopClosure()();
338 });
339
340 auto palette_editor_container = Container::Vertical({
341 palette_grid,
342 save_button,
343 back_button,
344 });
345
346 auto palette_editor_renderer = Renderer(palette_editor_container, [&] {
347 return vbox({text(gfx::kPaletteCategoryNames[selected_palette_group]
348 .data()) |
349 center,
350 separator(), palette_grid->Render(), separator(),
351 hbox({
352 save_button->Render() | center,
353 separator(),
354 back_button->Render() | center,
355 }) | center}) |
356 center;
357 });
358 screen.Loop(palette_editor_renderer);
359 } else {
360 auto palette_list = Menu(&palette_group_names, &selected_palette_group);
361 palette_list = CatchEvent(palette_list, [&](Event event) {
362 if (event == Event::Return) {
363 // Load the selected palette group into the palette editor.
364 // This will be a separate component.
365 show_palette_editor = true;
366 screen.ExitLoopClosure()();
367 load_palettes_from_current_group();
368 return true;
369 }
370 return false;
371 });
372
373 auto container = Container::Vertical({
374 palette_list,
375 back_button,
376 });
377 auto renderer = Renderer(container, [&] {
378 return vbox({text("Palette Editor") | center, separator(),
379 palette_list->Render(), separator(),
380 back_button->Render() | center}) |
381 center;
382 });
383 screen.Loop(renderer);
384 }
385}
386
387void HelpComponent(ftxui::ScreenInteractive &screen) {
388 auto help_text = vbox({
389 text("z3ed") | bold | color(Color::Yellow),
390 text("by scawful") | color(Color::Magenta),
391 text("The Legend of Zelda: A Link to the Past Hacking Tool") |
392 color(Color::Red),
393 separator(),
394 hbox({
395 text("Command") | bold | underlined,
396 filler(),
397 text("Arg") | bold | underlined,
398 filler(),
399 text("Params") | bold | underlined,
400 }),
401 separator(),
402 hbox({
403 text("Apply BPS Patch"),
404 filler(),
405 text("-a"),
406 filler(),
407 text("<rom_file> <bps_file>"),
408 }),
409 hbox({
410 text("Create BPS Patch"),
411 filler(),
412 text("-c"),
413 filler(),
414 text("<bps_file> <src_file> <modified_file>"),
415 }),
416 separator(),
417 hbox({
418 text("Open ROM"),
419 filler(),
420 text("-o"),
421 filler(),
422 text("<rom_file>"),
423 }),
424 hbox({
425 text("Backup ROM"),
426 filler(),
427 text("-b"),
428 filler(),
429 text("<rom_file> <optional:new_file>"),
430 }),
431 hbox({
432 text("Expand ROM"),
433 filler(),
434 text("-x"),
435 filler(),
436 text("<rom_file> <file_size>"),
437 }),
438 separator(),
439 hbox({
440 text("Transfer Tile16"),
441 filler(),
442 text("-t"),
443 filler(),
444 text("<src_rom> <dest_rom> <tile32_id_list:csv>"),
445 }),
446 separator(),
447 hbox({
448 text("Export Graphics"),
449 filler(),
450 text("-e"),
451 filler(),
452 text("<rom_file> <bin_file>"),
453 }),
454 hbox({
455 text("Import Graphics"),
456 filler(),
457 text("-i"),
458 filler(),
459 text("<bin_file> <rom_file>"),
460 }),
461 separator(),
462 hbox({
463 text("SNES to PC Address"),
464 filler(),
465 text("-s"),
466 filler(),
467 text("<address>"),
468 }),
469 hbox({
470 text("PC to SNES Address"),
471 filler(),
472 text("-p"),
473 filler(),
474 text("<address>"),
475 }),
476 });
477
478 auto help_text_component = Renderer([&] { return help_text; });
479
480 auto back_button =
481 Button("Back", [&] { SwitchComponents(screen, LayoutID::kMainMenu); });
482
483 auto container = Container::Vertical({
484 help_text_component,
485 back_button,
486 });
487
488 auto renderer = Renderer(container, [&] {
489 return vbox({
490 help_text_component->Render() | center,
491 separator(),
492 back_button->Render() | center,
493 }) |
494 border;
495 });
496
497 screen.Loop(renderer);
498}
499
500void MainMenuComponent(ftxui::ScreenInteractive &screen) {
501 // Tracks which menu item is selected.
502 static int selected = 0;
503 MenuOption option;
504 option.focused_entry = &selected;
505 auto menu = Menu(&kMainMenuEntries, &selected, option);
506 menu = CatchEvent(
507 menu, [&](Event event) { return HandleInput(screen, event, selected); });
508
509 std::string rom_information = "ROM not loaded";
510 if (app_context.rom.is_loaded()) {
511 rom_information = app_context.rom.title();
512 }
513
514 auto title = border(hbox({
515 text("z3ed") | bold | color(Color::Blue1),
516 separator(),
517 text("v0.1.0") | bold | color(Color::Green1),
518 separator(),
519 text(rom_information) | bold | color(Color::Red1),
520 }));
521
522 auto renderer = Renderer(menu, [&] {
523 return vbox({
524 separator(),
525 title | center,
526 separator(),
527 menu->Render() | center,
528 });
529 });
530
531 // Catch events like pressing Enter to switch layout or pressing 'q' to exit.
532 auto main_component = CatchEvent(renderer, [&](Event event) {
533 if (event == Event::Return) {
534 switch ((MainMenuEntry)selected) {
537 return true;
540 return true;
543 return true;
546 return true;
549 return true;
552 return true;
553 }
554 }
555
556 if (event == Event::Character('q')) {
558 return true;
559 }
560 return false;
561 });
562
563 screen.Loop(main_component);
564}
565
566} // namespace
567
568void ShowMain() {
569 auto screen = ScreenInteractive::TerminalOutput();
570 while (true) {
571 switch (app_context.current_layout) {
572 case LayoutID::kMainMenu: {
573 MainMenuComponent(screen);
574 } break;
575 case LayoutID::kLoadRom: {
576 LoadRomComponent(screen);
577 } break;
579 ApplyBpsPatchComponent(screen);
580 } break;
582 GenerateSaveFileComponent(screen);
583 } break;
585 PaletteEditorComponent(screen);
586 } break;
587 case LayoutID::kHelp: {
588 HelpComponent(screen);
589 } break;
590 case LayoutID::kError: {
591 // Display error message and return to main menu.
592 auto error_button = Button("Back to Main Menu", [&] {
593 app_context.error_message.clear();
594 SwitchComponents(screen, LayoutID::kMainMenu);
595 });
596
597 auto error_renderer = Renderer(error_button, [&] {
598 return vbox({text("Error") | center, separator(),
599 text(app_context.error_message), separator(),
600 error_button->Render() | center}) |
601 center;
602 });
603
604 screen.Loop(error_renderer);
605 } break;
606 case LayoutID::kExit:
607 default:
608 return; // Exit the application.
609 }
610 }
611}
612
613} // namespace cli
614} // namespace yaze
The Renderer class represents the renderer for the Yaze application.
Definition renderer.h:24
void PaletteEditorComponent(ftxui::ScreenInteractive &screen)
Definition tui.cc:264
bool HandleInput(ftxui::ScreenInteractive &screen, ftxui::Event &event, int &selected)
Definition tui.cc:26
void LoadRomComponent(ftxui::ScreenInteractive &screen)
Definition tui.cc:221
Element ColorBox(const Color &color)
Definition tui.cc:260
void ApplyBpsPatchComponent(ftxui::ScreenInteractive &screen)
Definition tui.cc:50
void MainMenuComponent(ftxui::ScreenInteractive &screen)
Definition tui.cc:500
void GenerateSaveFileComponent(ftxui::ScreenInteractive &screen)
Definition tui.cc:125
void HelpComponent(ftxui::ScreenInteractive &screen)
Definition tui.cc:387
void ReturnIfRomNotLoaded(ftxui::ScreenInteractive &screen)
Definition tui.cc:43
void SwitchComponents(ftxui::ScreenInteractive &screen, LayoutID layout)
Definition tui.cc:17
Namespace for the command line interface.
Definition compress.cc:4
const std::vector< std::string > kMainMenuEntries
Definition tui.h:15
MainMenuEntry
Definition tui.h:24
void ShowMain()
Definition tui.cc:568
LayoutID
Definition tui.h:33
@ kGenerateSaveFile
Definition tui.h:36
@ kMainMenu
Definition tui.h:40
@ kError
Definition tui.h:41
@ kPaletteEditor
Definition tui.h:37
@ kHelp
Definition tui.h:38
@ kExit
Definition tui.h:39
@ kLoadRom
Definition tui.h:34
@ kApplyBpsPatch
Definition tui.h:35
std::string LoadFile(const std::string &filename)
void ApplyBpsPatch(const std::vector< uint8_t > &source, const std::vector< uint8_t > &patch, std::vector< uint8_t > &target)
Definition bps.cc:136
Main namespace for the application.
Definition controller.cc:18