From d519142a8a3ec38d9d2a51feda36e4f0fd2c731c Mon Sep 17 00:00:00 2001 From: TheBrokenRail Date: Thu, 9 May 2024 01:25:53 -0400 Subject: [PATCH] Fancy Info Screen --- launcher/src/client/available-feature-flags | 1 + mods/CMakeLists.txt | 11 +- mods/include/mods/init/init.h | 2 +- mods/src/chat/chat-internal.h | 10 +- mods/src/chat/chat.cpp | 37 +-- mods/src/chat/ui.cpp | 11 +- mods/src/init/init.cpp | 2 +- mods/src/options/info.cpp | 278 +++++++++++++++++++ mods/src/options/options-internal.h | 5 +- mods/src/options/ui.cpp | 79 ++++++ mods/src/sound/sound.cpp | 3 + symbols/src/gui/components/Button.def | 3 +- symbols/src/gui/components/GuiComponent.def | 4 +- symbols/src/gui/components/Touch_TButton.def | 2 +- symbols/src/gui/screens/OptionsScreen.def | 3 + 15 files changed, 391 insertions(+), 60 deletions(-) create mode 100644 mods/src/options/info.cpp diff --git a/launcher/src/client/available-feature-flags b/launcher/src/client/available-feature-flags index 9222b00fc7..3a12eff7dd 100644 --- a/launcher/src/client/available-feature-flags +++ b/launcher/src/client/available-feature-flags @@ -61,3 +61,4 @@ TRUE Add Splashes TRUE Display Date In Select World Screen TRUE Optimized Chunk Sorting TRUE Disable Buggy Held Item Caching +TRUE Add Info Button To Options \ No newline at end of file diff --git a/mods/CMakeLists.txt b/mods/CMakeLists.txt index d2e785da71..a9aa3faee8 100644 --- a/mods/CMakeLists.txt +++ b/mods/CMakeLists.txt @@ -32,12 +32,18 @@ set(SRC # options src/options/options.cpp src/options/ui.cpp + src/options/info.cpp # bucket src/bucket/bucket.cpp # cake src/cake/cake.cpp # home src/home/home.cpp + # touch + src/touch/touch.cpp + # text-input-box + src/text-input-box/TextInputBox.cpp + src/text-input-box/TextInputScreen.cpp # test src/test/test.cpp # init @@ -83,8 +89,6 @@ else() src/input/crafting.cpp # sign src/sign/sign.cpp - # touch - src/touch/touch.cpp # atlas src/atlas/atlas.cpp # title-screen @@ -98,9 +102,6 @@ else() # textures src/textures/textures.cpp src/textures/lava.cpp - # text-input-box - src/text-input-box/TextInputBox.cpp - src/text-input-box/TextInputScreen.cpp # fps src/fps/fps.cpp ) diff --git a/mods/include/mods/init/init.h b/mods/include/mods/init/init.h index 495802925a..0107aec40f 100644 --- a/mods/include/mods/init/init.h +++ b/mods/include/mods/init/init.h @@ -17,12 +17,12 @@ void init_sound(); void init_input(); void init_sign(); void init_camera(); -void init_touch(); void init_atlas(); void init_title_screen(); void init_skin(); void init_fps(); #endif +void init_touch(); void init_textures(); void init_creative(); void init_game_mode(); diff --git a/mods/src/chat/chat-internal.h b/mods/src/chat/chat-internal.h index 657ac5af89..7adb8eaeeb 100644 --- a/mods/src/chat/chat-internal.h +++ b/mods/src/chat/chat-internal.h @@ -2,6 +2,8 @@ #include +#include + // Message Limitations #define MAX_CHAT_MESSAGE_LENGTH 256 @@ -9,11 +11,7 @@ __attribute__((visibility("internal"))) std::string _chat_get_prefix(char *username); // Queue Message For Sending -#ifndef MCPI_SERVER_MODE -__attribute__((visibility("internal"))) void _chat_queue_message(const char *message); -#endif +__attribute__((visibility("internal"))) void _chat_send_message(Minecraft *minecraft, const char *message); // Init Chat UI -#ifndef MCPI_HEADLESS_MODE -__attribute__((visibility("internal"))) void _init_chat_ui(); -#endif +__attribute__((visibility("internal"))) void _init_chat_ui(); \ No newline at end of file diff --git a/mods/src/chat/chat.cpp b/mods/src/chat/chat.cpp index fa8b59cbfb..5f3ecb1051 100644 --- a/mods/src/chat/chat.cpp +++ b/mods/src/chat/chat.cpp @@ -1,21 +1,13 @@ -// Config Needs To Load First -#include - #include #include #include #include +#include #include -#ifndef MCPI_HEADLESS_MODE -#include -#endif #include #include -#ifndef MCPI_HEADLESS_MODE -#include -#endif #include "chat-internal.h" #include @@ -33,7 +25,6 @@ std::string chat_send_api_command(Minecraft *minecraft, std::string str) { } } -#ifndef MCPI_HEADLESS_MODE // Send API Chat Command static void send_api_chat_command(Minecraft *minecraft, char *str) { char *command = nullptr; @@ -41,7 +32,6 @@ static void send_api_chat_command(Minecraft *minecraft, char *str) { chat_send_api_command(minecraft, command); free(command); } -#endif // Send Message To Players std::string _chat_get_prefix(char *username) { @@ -87,25 +77,10 @@ static void ServerSideNetworkHandler_handle_ChatPacket_injection(ServerSideNetwo } } -#ifndef MCPI_HEADLESS_MODE -// Message Queue -static std::vector queue; -// Add To Queue -void _chat_queue_message(const char *message) { - // Add - std::string str = message; - queue.push_back(str); +// Send Message +void _chat_send_message(Minecraft *minecraft, const char *message) { + send_api_chat_command(minecraft, (char *) message); } -// Empty Queue -unsigned int old_chat_counter = 0; -static void send_queued_messages(Minecraft *minecraft) { - // Loop - for (unsigned int i = 0; i < queue.size(); i++) { - send_api_chat_command(minecraft, (char *) queue[i].c_str()); - } - queue.clear(); -} -#endif // Init void init_chat() { @@ -117,12 +92,8 @@ void init_chat() { overwrite_call((void *) 0x6b518, (void *) CommandServer_parse_CommandServer_dispatchPacket_injection); // Re-Broadcast ChatPacket patch_vtable(ServerSideNetworkHandler_handle_ChatPacket, ServerSideNetworkHandler_handle_ChatPacket_injection); -#ifndef MCPI_HEADLESS_MODE - // Send Messages On Input Tick - input_run_on_tick(send_queued_messages); // Init UI _init_chat_ui(); -#endif // Disable Built-In Chat Message Limiting unsigned char message_limit_patch[4] = {0x03, 0x00, 0x53, 0xe1}; // "cmp r4, r4" patch((void *) 0x6b4c0, message_limit_patch); diff --git a/mods/src/chat/ui.cpp b/mods/src/chat/ui.cpp index 1c151bc70e..495b10ffd2 100644 --- a/mods/src/chat/ui.cpp +++ b/mods/src/chat/ui.cpp @@ -1,10 +1,5 @@ -// Config Needs To Load First -#include - -// Chat UI Code Is Useless In Headless Mode -#ifndef MCPI_HEADLESS_MODE - #include "chat-internal.h" +#include #include #include #include @@ -94,7 +89,7 @@ CUSTOM_VTABLE(chat_screen, Screen) { if (get_history().size() == 0 || text != get_history().back()) { get_history().push_back(text); } - _chat_queue_message(text.c_str()); + _chat_send_message(super->minecraft, text.c_str()); } Minecraft_setScreen(super->minecraft, nullptr); } else if (key == 0x26) { @@ -158,5 +153,3 @@ void _init_chat_ui() { } }); } - -#endif diff --git a/mods/src/init/init.cpp b/mods/src/init/init.cpp index 2d2979411f..31c169431f 100644 --- a/mods/src/init/init.cpp +++ b/mods/src/init/init.cpp @@ -20,12 +20,12 @@ __attribute__((constructor)) static void init(int argc, char *argv[]) { init_input(); init_sign(); init_camera(); - init_touch(); init_atlas(); init_title_screen(); init_skin(); init_fps(); #endif + init_touch(); init_textures(); init_creative(); init_game_mode(); diff --git a/mods/src/options/info.cpp b/mods/src/options/info.cpp new file mode 100644 index 0000000000..fed413d1cf --- /dev/null +++ b/mods/src/options/info.cpp @@ -0,0 +1,278 @@ +#include +#include +#include + +#include + +#include "options-internal.h" + +// Button IDs +#define DISCORD_ID 0 +#define BACK_ID 1 +#define INFO_ID_START 2 + +// Constants +static int line_button_width = 80; +static int line_button_height = 24; +static int padding = 4; +static int line_height = 8; +static int bottom_padding = padding; +static int inner_padding = padding; +static int title_padding = 8; +static int info_text_y_offset = (line_button_height - line_height) / 2; +static int content_y_offset_top = (title_padding * 2) + line_height; +static int content_y_offset_bottom = (bottom_padding * 2) + line_button_height; + +// Extra Version Info +static std::string extra_version_info = +#ifdef MCPI_IS_APPIMAGE_BUILD + "AppImage" +#elif defined(MCPI_IS_FLATPAK_BUILD) + "Flatpak" +#else + "" +#endif + ; +static std::string extra_version_info_full = !extra_version_info.empty() ? (" (" + extra_version_info + ")") : ""; + +// Profile Directory +static std::string profile_directory_suffix = +#ifdef MCPI_IS_FLATPAK_BUILD + "/.var/app/" MCPI_APP_ID "/.minecraft-pi" +#else + "/.minecraft-pi" +#endif + ; +static std::string get_profile_directory_url() { + const char *home = getenv("HOME"); + if (home == nullptr) { + IMPOSSIBLE(); + } + return std::string("file://") + home + profile_directory_suffix; +} + +// Info Data +#define MORE_INFO_TEXT "More Info" +struct info_line { + std::string (*get_text)(); + std::string button_url; + std::string button_text; +}; +std::string info_sound_data_state = "N/A"; +static info_line info[] = { + { + .get_text = []() { + return std::string("Version: v") + reborn_get_version() + extra_version_info_full; + }, + .button_url = "https://gitea.thebrokenrail.com/minecraft-pi-reborn/minecraft-pi-reborn/src/branch/master/docs/CHANGELOG.md", + .button_text = MORE_INFO_TEXT + }, + { + .get_text = []() { + return std::string("Profile Directory"); + }, + .button_url = get_profile_directory_url(), + .button_text = "Open" + }, + { + .get_text = []() { + return std::string("Sound Data: ") + info_sound_data_state; + }, + .button_url = "https://gitea.thebrokenrail.com/minecraft-pi-reborn/minecraft-pi-reborn/src/branch/master/docs/SOUND.md", + .button_text = MORE_INFO_TEXT + }, +}; +#define info_size int(sizeof(info) / sizeof(info_line)) + +// Positioned Info +struct info_pos { + int x; + int y; +}; +struct info_line_position { + info_pos text; + info_pos button; +}; +static info_line_position positioned_info[info_size]; +static int content_height = 0; +static void position_info(Font *font, int width, int height) { + // First Stage (Find Max Text Width) + int info_text_width = 0; + for (int i = 0; i < info_size; i++) { + std::string text = info[i].get_text(); + int text_width = font->width(&text); + if (text_width > info_text_width) { + info_text_width = text_width; + } + } + + // Second Stage (Initial Positioning) + int y = 0; + for (int i = 0; i < info_size; i++) { + // Padding + if (i != 0) { + y += padding; + } + // Y + positioned_info[i].button.y = y; + positioned_info[i].text.y = y + info_text_y_offset; + // X + positioned_info[i].button.x = info_text_width + padding; + positioned_info[i].text.x = 0; + // Advance + y += line_button_height; + } + + // Third Stage (Centering) + int info_height = y; + int info_width = info_text_width + padding + line_button_width; + content_height = height - content_y_offset_top - content_y_offset_bottom; + int info_y_offset = ((content_height - info_height) / 2) + content_y_offset_top; + int info_x_offset = (width - info_width) / 2; + for (int i = 0; i < info_size; i++) { + positioned_info[i].button.x += info_x_offset; + positioned_info[i].button.y += info_y_offset; + positioned_info[i].text.x += info_x_offset; + positioned_info[i].text.y += info_y_offset; + } +} + +// Open URL +static void open_url(const std::string &url) { + int return_code; + const char *command[] = {"xdg-open", url.c_str(), nullptr}; + char *output = run_command(command, &return_code, nullptr); + if (output != nullptr) { + free(output); + } + if (!is_exit_status_success(return_code)) { + WARN("Unable To Open URL: %s", url.c_str()); + } +} + +// Render Fancy Background +static void render_background(Minecraft *minecraft, int x, int y, int width, int height) { + // https://github.com/ReMinecraftPE/mcpe/blob/f0d65eaecec1b3fe9c2f2b251e114a890c54ab77/source/client/gui/components/RolledSelectionList.cpp#L169-L179 + std::string texture = "gui/background.png"; + minecraft->textures->loadAndBindTexture(&texture); + Tesselator *t = &Tesselator_instance; + t->begin(7); + t->color(32, 32, 32, 255); + float x1 = x; + float x2 = x + width; + float y1 = y; + float y2 = y + height; + t->vertexUV(x1, y2, 0.0f, x1 / 32.0f, y2 / 32.0f); + t->vertexUV(x2, y2, 0.0f, x2 / 32.0f, y2 / 32.0f); + t->vertexUV(x2, y1, 0.0f, x2 / 32.0f, y1 / 32.0f); + t->vertexUV(x1, y1, 0.0f, x1 / 32.0f, y1 / 32.0f); + t->draw(); +} + +// Create VTable +CUSTOM_VTABLE(info_screen, Screen) { + // Buttons + static Button *discord; + static Button *back; + static Button *info_buttons[info_size]; + // Init + vtable->init = [](Screen *self) { + // Info + for (int i = 0; i < info_size; i++) { + Button *button = touch_create_button(INFO_ID_START + i, info[i].button_text); + self->rendered_buttons.push_back(button); + self->selectable_buttons.push_back(button); + info_buttons[i] = button; + } + // Discord Button + discord = touch_create_button(DISCORD_ID, "Discord"); + self->rendered_buttons.push_back(discord); + self->selectable_buttons.push_back(discord); + // Back Button + back = touch_create_button(BACK_ID, "Back"); + self->rendered_buttons.push_back(back); + self->selectable_buttons.push_back(back); + }; + // Handle Back + vtable->handleBackEvent = [](Screen *self, bool do_nothing) { + if (!do_nothing) { + OptionsScreen *screen = alloc_OptionsScreen(); + ALLOC_CHECK(screen); + screen->constructor(); + self->minecraft->setScreen((Screen *) screen); + } + return true; + }; + // Rendering + static Screen_render_t original_render = vtable->render; + vtable->render = [](Screen *self, int x, int y, float param_1) { + // Background + self->vtable->renderBackground(self); + // Gradient + render_background(self->minecraft, 0, content_y_offset_top, self->width, content_height); + // Call Original Method + original_render(self, x, y, param_1); + // Title + std::string title = "Reborn Information"; + self->drawCenteredString(self->font, &title, self->width / 2, title_padding, 0xffffffff); + // Info Text + for (int i = 0; i < info_size; i++) { + std::string text = info[i].get_text(); + self->drawString(self->font, &text, positioned_info[i].text.x, positioned_info[i].text.y, 0xffffffff); + } + }; + // Positioning + vtable->setupPositions = [](Screen *self) { + // Height/Width + int width = 120; + discord->width = back->width = width; + discord->height = back->height = line_button_height; + // X/Y + discord->y = back->y = self->height - bottom_padding - line_button_height; + discord->x = (self->width / 2) - inner_padding - width; + back->x = (self->width / 2) + inner_padding; + // Info + position_info(self->font, self->width, self->height); + for (int i = 0; i < info_size; i++) { + Button *button = info_buttons[i]; + button->width = line_button_width; + button->height = line_button_height; + button->x = positioned_info[i].button.x; + button->y = positioned_info[i].button.y; + } + }; + // Cleanup + vtable->removed = [](Screen *self) { + for (Button *button : self->rendered_buttons) { + button->destructor_deleting(); + } + }; + // Handle Button Click + vtable->buttonClicked = [](Screen *self, Button *button) { + if (button->id == BACK_ID) { + // Back + self->handleBackEvent(false); + } else if (button->id == DISCORD_ID) { + // Open Discord Invite + open_url("https://discord.gg/mcpi-revival-740287937727561779"); + } else if (button->id >= INFO_ID_START) { + // Open Info URL + int i = button->id - INFO_ID_START; + open_url(info[i].button_url); + } + }; +} + +// Create Screen +Screen *_create_options_info_screen() { + // Allocate + Screen *screen = alloc_Screen(); + ALLOC_CHECK(screen); + screen->constructor(); + + // Set VTable + screen->vtable = get_info_screen_vtable(); + + // Return + return screen; +} \ No newline at end of file diff --git a/mods/src/options/options-internal.h b/mods/src/options/options-internal.h index 4a7211899b..07a4903af4 100644 --- a/mods/src/options/options-internal.h +++ b/mods/src/options/options-internal.h @@ -1,4 +1,7 @@ #pragma once +#include + __attribute__((visibility("internal"))) void _init_options_ui(); -__attribute__((visibility("internal"))) extern Options *stored_options; \ No newline at end of file +__attribute__((visibility("internal"))) extern Options *stored_options; +__attribute__((visibility("internal"))) Screen *_create_options_info_screen(); \ No newline at end of file diff --git a/mods/src/options/ui.cpp b/mods/src/options/ui.cpp index e2f281da32..e8918771ca 100644 --- a/mods/src/options/ui.cpp +++ b/mods/src/options/ui.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -89,6 +90,72 @@ static void OptionButton_toggle_Options_save_injection(Options *self) { Options_save(self); } +// Add "Reborn" Info Button +#define INFO_BUTTON_ID 99 +static void OptionsScreen_init_injection(OptionsScreen_init_t original, OptionsScreen *self) { + // Call Original Method + original(self); + + // Add Button + Touch_TButton *button = alloc_Touch_TButton(); + ALLOC_CHECK(button); + std::string name = "Reborn"; + button->constructor(INFO_BUTTON_ID, &name); + self->rendered_buttons.push_back((Button *) button); + self->selectable_buttons.push_back((Button *) button); +} +static void OptionsScreen_setupPositions_injection(OptionsScreen_setupPositions_t original, OptionsScreen *self) { + // Call Original Method + original(self); + + // Find Button + Button *prevButton = nullptr; + Button *button = nullptr; + for (Button *x : self->selectable_buttons) { + if (x->id == INFO_BUTTON_ID) { + button = x; + break; + } + prevButton = x; + } + if (button == nullptr || prevButton == nullptr) { + IMPOSSIBLE(); + } + + // Setup Button + button->width = prevButton->width; + button->height = prevButton->height; + button->x = prevButton->x; + button->y = prevButton->y + prevButton->height; +} +static void OptionsScreen_buttonClicked_injection(OptionsScreen_buttonClicked_t original, OptionsScreen *self, Button *button) { + // Check ID + if (button->id == INFO_BUTTON_ID) { + // Show Screen + self->minecraft->setScreen(_create_options_info_screen()); + } else { + // Call Original Method + original(self, button); + } +} +static void OptionsScreen_removed_injection(OptionsScreen_removed_t original, OptionsScreen *self) { + // Delete Button + Button *button = nullptr; + for (Button *x : self->selectable_buttons) { + if (x->id == INFO_BUTTON_ID) { + button = x; + break; + } + } + if (button == nullptr) { + IMPOSSIBLE(); + } + button->destructor_deleting(); + + // Call Original Method + original(self); +} + // Init void _init_options_ui() { // Fix Options Screen @@ -115,4 +182,16 @@ void _init_options_ui() { // Fix Difficulty When Toggling overwrite_call((void *) 0x1cd00, (void *) OptionButton_toggle_Options_save_injection); } + + // Info Button + if (feature_has("Add Info Button To Options", server_disabled)) { + // Add Button + overwrite_virtual_calls(OptionsScreen_init, OptionsScreen_init_injection); + // Position Button + overwrite_virtual_calls(OptionsScreen_setupPositions, OptionsScreen_setupPositions_injection); + // Handle Click + overwrite_virtual_calls(OptionsScreen_buttonClicked, OptionsScreen_buttonClicked_injection); + // Cleanup + overwrite_virtual_calls(OptionsScreen_removed, OptionsScreen_removed_injection); + } } \ No newline at end of file diff --git a/mods/src/sound/sound.cpp b/mods/src/sound/sound.cpp index 9043374d34..5d59c37103 100644 --- a/mods/src/sound/sound.cpp +++ b/mods/src/sound/sound.cpp @@ -11,6 +11,7 @@ // Resolve Source File Path #define SOURCE_FILE_BASE "data/libminecraftpe.so" +extern std::string info_sound_data_state; std::string _sound_get_source_file() { static bool source_loaded = false; static std::string source; @@ -38,9 +39,11 @@ std::string _sound_get_source_file() { // Fail WARN("Audio Source File Doesn't Exist: " SOURCE_FILE_BASE " (See: https://gitea.thebrokenrail.com/minecraft-pi-reborn/minecraft-pi-reborn/src/branch/master/docs/SOUND.md)"); source.assign(""); + info_sound_data_state = "Missing"; } else { // Set source.assign(path); + info_sound_data_state = "Loaded"; } // Free diff --git a/symbols/src/gui/components/Button.def b/symbols/src/gui/components/Button.def index 1b3d078b62..2d1f2774ba 100644 --- a/symbols/src/gui/components/Button.def +++ b/symbols/src/gui/components/Button.def @@ -1,7 +1,7 @@ extends GuiComponent; size 0x28; -constructor (int param_1, std::string *text) = 0x1bc54; +constructor (int id, std::string *text) = 0x1bc54; method int hovered(Minecraft *minecraft, int click_x, int click_y) = 0x1be2c; @@ -10,3 +10,4 @@ property int height = 0x18; property int x = 0xc; property int y = 0x10; property std::string text = 0x1c; +property int id = 0x20; \ No newline at end of file diff --git a/symbols/src/gui/components/GuiComponent.def b/symbols/src/gui/components/GuiComponent.def index 4a14dc11d7..f5f183766d 100644 --- a/symbols/src/gui/components/GuiComponent.def +++ b/symbols/src/gui/components/GuiComponent.def @@ -4,7 +4,7 @@ constructor () = 0x28204; vtable 0x1039b0; method void blit(int x_dest, int y_dest, int x_src, int y_src, int width_dest, int height_dest, int width_src, int height_src) = 0x282a4; -method void drawCenteredString(Font *font, std::string *text, int x, int y, int color) = 0x2821c; -method void drawString(Font *font, std::string *text, int x, int y, int color) = 0x28284; +method void drawCenteredString(Font *font, std::string *text, int x, int y, uint color) = 0x2821c; +method void drawString(Font *font, std::string *text, int x, int y, uint color) = 0x28284; method void fill(int x1, int y1, int x2, int y2, uint color) = 0x285f0; method void fillGradient(int x1, int y1, int x2, int y2, int color1, int color2) = 0x287c0; \ No newline at end of file diff --git a/symbols/src/gui/components/Touch_TButton.def b/symbols/src/gui/components/Touch_TButton.def index fc73c64be9..12ef89c636 100644 --- a/symbols/src/gui/components/Touch_TButton.def +++ b/symbols/src/gui/components/Touch_TButton.def @@ -1,4 +1,4 @@ extends Button; size 0x28; -constructor (int param_1, std::string *text) = 0x1c168; +constructor (int id, std::string *text) = 0x1c168; diff --git a/symbols/src/gui/screens/OptionsScreen.def b/symbols/src/gui/screens/OptionsScreen.def index 990bf44131..30128680a2 100644 --- a/symbols/src/gui/screens/OptionsScreen.def +++ b/symbols/src/gui/screens/OptionsScreen.def @@ -1,3 +1,6 @@ extends Screen; vtable 0x104978; + +size 0x70; +constructor () = 0x35488; \ No newline at end of file