In-Game Chat

This commit is contained in:
TheBrokenRail 2024-02-01 03:12:24 -05:00
parent d175f692e0
commit 58713976d4
26 changed files with 446 additions and 152 deletions

View File

@ -123,6 +123,8 @@ static SDLKey glfw_key_to_sdl_key(int key) {
return SDLK_RETURN;
case GLFW_KEY_BACKSPACE:
return SDLK_BACKSPACE;
case GLFW_KEY_DELETE:
return SDLK_DELETE;
// Fullscreen
case GLFW_KEY_F11:
return SDLK_F11;

View File

@ -28,6 +28,7 @@ typedef enum {
SDLK_s = 115,
SDLK_t = 116,
SDLK_w = 119,
SDLK_DELETE = 127,
SDLK_UP = 273,
SDLK_DOWN = 274,
SDLK_RIGHT = 275,

View File

@ -15,7 +15,7 @@ set(SRC
src/version/version.cpp
# chat
src/chat/chat.cpp
src/chat/ui.c
src/chat/ui.cpp
# creative
src/creative/creative.cpp
# game-mode
@ -96,6 +96,9 @@ 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
)
endif()

View File

@ -15,7 +15,6 @@ extern "C" {
#ifndef MCPI_SERVER_MODE
void chat_open();
unsigned int chat_get_counter();
#endif
// Override using the HOOK() macro to provide customized chat behavior.

View File

@ -28,6 +28,8 @@ void Level_saveLevelData_injection(Level *level);
// Use this instead of directly calling Gui::addMessage(), it has proper logging!
void misc_add_message(Gui *gui, const char *text);
extern bool is_in_chat;
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,35 @@
#pragma once
#include <symbols/minecraft.h>
struct TextInputBox {
GuiComponent super;
int m_ID;
int m_xPos;
int m_yPos;
int m_width;
int m_height;
std::string m_placeholder;
std::string m_text;
bool m_bFocused;
bool m_bEnabled;
bool m_bCursorOn;
int m_insertHead;
int m_lastFlashed;
Font *m_pFont;
int m_maxLength;
void setSize(int x, int y, int width = 200, int height = 12);
void init(Font *pFont);
void setEnabled(bool bEnabled);
void keyPressed(int key);
void charPressed(int chr);
void render();
void tick();
void setFocused(bool b);
void onClick(int x, int y);
bool clicked(int x, int y);
static TextInputBox create(int id, const std::string &placeholder = "", const std::string &text = "");
};

View File

@ -0,0 +1,12 @@
#pragma once
#include <symbols/minecraft.h>
#include <mods/text-input-box/TextInputBox.h>
struct TextInputScreen {
Screen super;
std::vector<TextInputBox *> m_textInputs;
static void setup(Screen_vtable *vtable);
};

View File

@ -6,9 +6,12 @@
extern "C" {
#endif
__attribute__((visibility("internal"))) extern int _chat_enabled;
#ifndef MCPI_SERVER_MODE
__attribute__((visibility("internal"))) void _chat_queue_message(char *message);
__attribute__((visibility("internal"))) void _chat_queue_message(const char *message);
#endif
#ifndef MCPI_HEADLESS_MODE
__attribute__((visibility("internal"))) void _init_chat_ui();
#endif
#ifdef __cplusplus

View File

@ -5,9 +5,6 @@
#include <cstring>
#include <cstdio>
#include <vector>
#ifndef MCPI_HEADLESS_MODE
#include <pthread.h>
#endif
#include <symbols/minecraft.h>
#ifndef MCPI_HEADLESS_MODE
@ -22,9 +19,6 @@
#include "chat-internal.h"
#include <mods/chat/chat.h>
// Store If Chat is Enabled
int _chat_enabled = 0;
// Message Limitations
#define MAX_CHAT_MESSAGE_LENGTH 512
@ -95,45 +89,27 @@ static void ServerSideNetworkHandler_handle_ChatPacket_injection(ServerSideNetwo
#ifndef MCPI_HEADLESS_MODE
// Message Queue
static pthread_mutex_t queue_mutex = PTHREAD_MUTEX_INITIALIZER;
static std::vector<std::string> queue;
// Add To Queue
void _chat_queue_message(char *message) {
// Lock
pthread_mutex_lock(&queue_mutex);
void _chat_queue_message(const char *message) {
// Add
std::string str;
str.append(message);
std::string str = message;
queue.push_back(str);
// Unlock
pthread_mutex_unlock(&queue_mutex);
}
// Empty Queue
unsigned int old_chat_counter = 0;
static void send_queued_messages(Minecraft *minecraft) {
// Lock
pthread_mutex_lock(&queue_mutex);
// If Message Was Submitted, No Other Chat Windows Are Open, And The Game Is Not Paused, Then Re-Lock Cursor
unsigned int new_chat_counter = chat_get_counter();
if (old_chat_counter > new_chat_counter && new_chat_counter == 0) {
// Unlock UI
media_set_interactable(1);
}
old_chat_counter = new_chat_counter;
// Loop
for (unsigned int i = 0; i < queue.size(); i++) {
send_api_chat_command(minecraft, (char *) queue[i].c_str());
}
queue.clear();
// Unlock
pthread_mutex_unlock(&queue_mutex);
}
#endif
// Init
void init_chat() {
_chat_enabled = feature_has("Implement Chat", server_enabled);
if (_chat_enabled) {
if (feature_has("Implement Chat", server_enabled)) {
// Disable Original ChatPacket Loopback
unsigned char disable_chat_packet_loopback_patch[4] = {0x00, 0xf0, 0x20, 0xe3}; // "nop"
patch((void *) 0x6b490, disable_chat_packet_loopback_patch);
@ -141,9 +117,11 @@ void init_chat() {
overwrite_call((void *) 0x6b518, (void *) CommandServer_parse_CommandServer_dispatchPacket_injection);
// Re-Broadcast ChatPacket
patch_address(ServerSideNetworkHandler_handle_ChatPacket_vtable_addr, (void *) ServerSideNetworkHandler_handle_ChatPacket_injection);
// Send Messages On Input Tick
#ifndef MCPI_HEADLESS_MODE
// Send Messages On Input Tick
input_run_on_tick(send_queued_messages);
// Init UI
_init_chat_ui();
#endif
}
}

View File

@ -1,83 +0,0 @@
// Config Needs To Load First
#include <libreborn/libreborn.h>
// Chat UI Code Is Useless In Headless Mode
#ifndef MCPI_HEADLESS_MODE
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
#include <media-layer/core.h>
#include "chat-internal.h"
#include <mods/chat/chat.h>
// Count Chat Windows
static pthread_mutex_t chat_counter_lock = PTHREAD_MUTEX_INITIALIZER;
static volatile unsigned int chat_counter = 0;
unsigned int chat_get_counter() {
return chat_counter;
}
// Chat Thread
#define DIALOG_TITLE "Chat"
static void *chat_thread(__attribute__((unused)) void *nop) {
// Open
int return_code;
const char *command[] = {
"zenity",
"--title", DIALOG_TITLE,
"--name", MCPI_APP_ID,
"--entry",
"--text", "Enter Chat Message:",
NULL
};
char *output = run_command(command, &return_code, NULL);
// Handle Message
if (output != NULL) {
// Check Return Code
if (is_exit_status_success(return_code)) {
// Remove Ending Newline
int length = strlen(output);
if (output[length - 1] == '\n') {
output[length - 1] = '\0';
}
length = strlen(output);
// Don't Allow Empty Strings
if (length > 0) {
// Submit
char *safe_output = to_cp437(output);
_chat_queue_message(safe_output);
free(safe_output);
}
}
// Free Output
free(output);
}
// Update Counter
pthread_mutex_lock(&chat_counter_lock);
chat_counter--;
pthread_mutex_unlock(&chat_counter_lock);
// Return
return NULL;
}
// Create Chat Thead
void chat_open() {
if (_chat_enabled) {
// Lock UI
media_set_interactable(0);
// Update Counter
pthread_mutex_lock(&chat_counter_lock);
chat_counter++;
pthread_mutex_unlock(&chat_counter_lock);
// Start Thread
pthread_t thread;
pthread_create(&thread, NULL, chat_thread, NULL);
}
}
#endif

98
mods/src/chat/ui.cpp Normal file
View File

@ -0,0 +1,98 @@
// Config Needs To Load First
#include <libreborn/libreborn.h>
// Chat UI Code Is Useless In Headless Mode
#ifndef MCPI_HEADLESS_MODE
#include "chat-internal.h"
#include <mods/chat/chat.h>
#include <mods/text-input-box/TextInputScreen.h>
#include <mods/misc/misc.h>
// Structure
struct ChatScreen {
TextInputScreen super;
TextInputBox chat;
};
CUSTOM_VTABLE(chat_screen, Screen) {
TextInputScreen::setup(vtable);
// Init
vtable->init = [](Screen *super) {
Screen_init_non_virtual(super);
ChatScreen *self = (ChatScreen *) super;
self->super.m_textInputs.push_back(&self->chat);
self->chat.init(super->font);
self->chat.setFocused(true);
is_in_chat = true;
};
// Removal
vtable->removed = [](Screen *super) {
Screen_removed_non_virtual(super);
is_in_chat = false;
};
// Rendering
static Screen_render_t original_render = vtable->render;
vtable->render = [](Screen *super, int x, int y, float param_1) {
// Background
super->vtable->renderBackground(super);
// Render Chat
Gui_renderChatMessages(&super->minecraft->gui, super->height, 20, true, super->font);
// Call Original Method
original_render(super, x, y, param_1);
};
// Positioning
vtable->setupPositions = [](Screen *super) {
Screen_setupPositions_non_virtual(super);
ChatScreen *self = (ChatScreen *) super;
int height = 20;
int x = 0;
int y = super->height - height;
int width = super->width;
self->chat.setSize(x, y, width, height);
};
// Key Presses
static Screen_keyPressed_t original_keyPressed = vtable->keyPressed;
vtable->keyPressed = [](Screen *super, int key) {
// Handle Enter
ChatScreen *self = (ChatScreen *) super;
if (key == 0x0d) {
_chat_queue_message(self->chat.m_text.c_str());
Minecraft_setScreen(super->minecraft, NULL);
}
// Call Original Method
original_keyPressed(super, key);
};
}
static Screen *create_chat_screen() {
// Construct
ChatScreen *screen = new ChatScreen;
ALLOC_CHECK(screen);
Screen_constructor(&screen->super.super);
// Set VTable
screen->super.super.vtable = get_chat_screen_vtable();
// Setup
screen->chat = TextInputBox::create(0);
// Return
return (Screen *) screen;
}
// Open Screen
static bool open_chat_screen = false;
void chat_open() {
open_chat_screen = true;
}
// Init
void _init_chat_ui() {
misc_run_on_tick([](Minecraft *minecraft) {
if (open_chat_screen && Minecraft_isLevelGenerated(minecraft) && minecraft->screen == NULL) {
Minecraft_setScreen(minecraft, create_chat_screen());
}
open_chat_screen = false;
});
}
#endif

View File

@ -70,12 +70,7 @@ HOOK(SDL_PollEvent, int, (SDL_Event *event)) {
input_third_person();
handled = 1;
} else if (event->key.keysym.sym == SDLK_t) {
// Only When In-Game With No Other Chat Windows Open
if (SDL_WM_GrabInput(SDL_GRAB_QUERY) == SDL_GRAB_ON && chat_get_counter() == 0) {
// Open Chat
chat_open();
}
// Mark Handled
chat_open();
handled = 1;
} else if (event->key.keysym.sym == SDLK_ESCAPE) {
// Treat Escape As Back Button Press (This Fixes Issues With Signs)

View File

@ -80,7 +80,8 @@ static void _handle_mouse_grab(Minecraft *minecraft) {
// Block UI Interaction When Mouse Is Locked
static bool Gui_tickItemDrop_Minecraft_isCreativeMode_call_injection(Minecraft *minecraft) {
if (!enable_misc || SDL_WM_GrabInput(SDL_GRAB_QUERY) == SDL_GRAB_OFF) {
bool is_in_game = minecraft->screen == NULL || minecraft->screen->vtable == (Screen_vtable *) Touch_IngameBlockSelectionScreen_vtable_base;
if (!enable_misc || (SDL_WM_GrabInput(SDL_GRAB_QUERY) == SDL_GRAB_OFF && is_in_game)) {
// Call Original Method
return creative_is_restricted() && Minecraft_isCreativeMode(minecraft);
} else {

View File

@ -65,6 +65,7 @@ static void Gui_renderBubbles_GuiComponent_blit_injection(Gui *component, int32_
// Additional GUI Rendering
static int hide_chat_messages = 0;
bool is_in_chat = 0;
static int render_selected_item_text = 0;
static void Gui_renderChatMessages_injection(Gui *gui, int32_t y_offset, uint32_t max_messages, bool disable_fading, Font *font) {
// Handle Classic HUD
@ -76,7 +77,7 @@ static void Gui_renderChatMessages_injection(Gui *gui, int32_t y_offset, uint32_
}
// Call Original Method
if (!hide_chat_messages) {
if (!hide_chat_messages && !is_in_chat) {
Gui_renderChatMessages(gui, y_offset, max_messages, disable_fading, font);
}

View File

@ -12,7 +12,13 @@
// Handle Backspace
static int32_t sdl_key_to_minecraft_key_injection(int32_t sdl_key) {
if (sdl_key == SDLK_BACKSPACE) {
return 8;
return 0x8;
} else if (sdl_key == SDLK_DELETE) {
return 0x2e;
} else if (sdl_key == SDLK_LEFT) {
return 0x25;
} else if (sdl_key == SDLK_RIGHT) {
return 0x27;
} else {
// Call Original Method
return Common_sdl_key_to_minecraft_key(sdl_key);
@ -31,37 +37,17 @@ static void LocalPlayer_openTextEdit_injection(LocalPlayer *local_player, TileEn
}
// Store Text Input
std::vector<char> input;
void sign_key_press(char key) {
input.push_back(key);
}
static void clear_input(__attribute__((unused)) Minecraft *minecraft) {
input.clear();
}
// Handle Text Input
static void TextEditScreen_updateEvents_injection(TextEditScreen *screen) {
// Call Original Method
TextEditScreen_updateEvents_non_virtual(screen);
if (!screen->passthrough_input) {
for (char key : input) {
// Handle Normal Key
screen->vtable->keyboardNewChar(screen, key);
}
}
clear_input(NULL);
Keyboard__inputText.push_back(key);
}
// Init
void init_sign() {
if (feature_has("Fix Sign Placement", server_disabled)) {
// Handle Backspace
overwrite_calls((void *) Common_sdl_key_to_minecraft_key, (void *) sdl_key_to_minecraft_key_injection);
// Fix Signs
patch_address(LocalPlayer_openTextEdit_vtable_addr, (void *) LocalPlayer_openTextEdit_injection);
patch_address(TextEditScreen_updateEvents_vtable_addr, (void *) TextEditScreen_updateEvents_injection);
// Clear Input On Input Tick
input_run_on_tick(clear_input);
}
// Handle Backspace
overwrite_calls((void *) Common_sdl_key_to_minecraft_key, (void *) sdl_key_to_minecraft_key_injection);
}

View File

@ -0,0 +1,2 @@
# ``text-input-box`` Mod
This mod implements a GUI component for text input. This is ported from [ReMinecraftPE](https://github.com/ReMinecraftPE/mcpe/blob/d7a8b6baecf8b3b050538abdbc976f690312aa2d/source/client/gui/components/TextInputBox.cpp).

View File

@ -0,0 +1,200 @@
#include <libreborn/libreborn.h>
#include <mods/text-input-box/TextInputBox.h>
TextInputBox TextInputBox::create(int id, const std::string &placeholder, const std::string &text) {
// Construct
TextInputBox self;
GuiComponent_constructor(&self.super);
// Setup
self.m_ID = id;
self.m_xPos = 0;
self.m_yPos = 0;
self.m_width = 0;
self.m_height = 0;
self.m_placeholder = placeholder;
self.m_text = text;
self.m_bFocused = false;
self.m_bEnabled = true;
self.m_bCursorOn = true;
self.m_insertHead = 0;
self.m_lastFlashed = 0;
self.m_pFont = nullptr;
self.m_maxLength = -1;
// Return
return self;
}
void TextInputBox::setSize(int x, int y, int width, int height) {
m_xPos = x;
m_yPos = y;
m_width = width;
m_height = height;
}
void TextInputBox::init(Font *pFont) {
m_pFont = pFont;
}
void TextInputBox::setEnabled(bool bEnabled) {
m_bEnabled = bEnabled;
}
void TextInputBox::keyPressed(int key) {
if (!m_bFocused) {
return;
}
switch (key) {
case 0x8: {
// Backspace
if (m_text.empty()) {
return;
}
if (m_insertHead <= 0) {
return;
}
if (m_insertHead > int(m_text.size())) {
m_insertHead = int(m_text.size());
}
m_text.erase(m_text.begin() + m_insertHead - 1, m_text.begin() + m_insertHead);
m_insertHead--;
break;
}
case 0x2e: {
// Delete
if (m_text.empty()) {
return;
}
if (m_insertHead < 0) {
return;
}
if (m_insertHead >= int(m_text.size())) {
return;
}
m_text.erase(m_text.begin() + m_insertHead, m_text.begin() + m_insertHead + 1);
break;
}
case 0x25: {
// Left
m_insertHead--;
if (m_insertHead < 0) {
m_insertHead = 0;
}
break;
}
case 0x27: {
// Right
m_insertHead++;
if (!m_text.empty()) {
if (m_insertHead > int(m_text.size())) {
m_insertHead = int(m_text.size());
}
} else {
m_insertHead = 0;
}
break;
}
case 0x0d: {
// Enter
m_bFocused = false;
break;
}
}
}
void TextInputBox::tick() {
if (!m_lastFlashed) {
m_lastFlashed = Common_getTimeMs();
}
if (m_bFocused) {
if (Common_getTimeMs() > m_lastFlashed + 500) {
m_lastFlashed += 500;
m_bCursorOn ^= 1;
}
} else {
m_bCursorOn = false;
}
}
void TextInputBox::setFocused(bool b) {
if (m_bFocused == b) {
return;
}
m_bFocused = b;
if (b) {
m_lastFlashed = Common_getTimeMs();
m_bCursorOn = true;
m_insertHead = int(m_text.size());
}
}
void TextInputBox::onClick(int x, int y) {
setFocused(clicked(x, y));
}
static int PADDING = 5;
void TextInputBox::charPressed(int k) {
if (!m_bFocused) {
return;
}
// note: the width will increase by the same amount no matter where K is appended
std::string test_str = m_text + char(k);
if (m_maxLength != -1 && int(test_str.length()) > m_maxLength) {
return;
}
int width = Font_width(m_pFont, &test_str);
if (width < (m_width - PADDING)) {
m_text.insert(m_text.begin() + m_insertHead, k);
m_insertHead++;
}
}
void TextInputBox::render() {
GuiComponent_fill(&super, m_xPos, m_yPos, m_xPos + m_width, m_yPos + m_height, 0xFFAAAAAA);
GuiComponent_fill(&super, m_xPos + 1, m_yPos + 1, m_xPos + m_width - 1, m_yPos + m_height - 1, 0xFF000000);
int textYPos = (m_height - 8) / 2;
if (m_text.empty()) {
GuiComponent_drawString(&super, m_pFont, &m_placeholder, m_xPos + PADDING, m_yPos + textYPos, 0x404040);
} else {
GuiComponent_drawString(&super, m_pFont, &m_text, m_xPos + PADDING, m_yPos + textYPos, 0xFFFFFF);
}
if (m_bCursorOn) {
int xPos = 5;
std::string substr = m_text.substr(0, m_insertHead);
xPos += Font_width(m_pFont, &substr);
std::string str = "_";
GuiComponent_drawString(&super, m_pFont, &str, m_xPos + xPos, m_yPos + textYPos + 2, 0xFFFFFF);
}
}
bool TextInputBox::clicked(int xPos, int yPos) {
if (!m_bEnabled) {
return false;
}
if (xPos < m_xPos) {
return false;
}
if (yPos < m_yPos) {
return false;
}
if (xPos >= m_xPos + m_width) {
return false;
}
if (yPos >= m_yPos + m_height) {
return false;
}
return true;
}

View File

@ -0,0 +1,40 @@
#include <libreborn/libreborn.h>
#include <mods/text-input-box/TextInputScreen.h>
// VTable
void TextInputScreen::setup(Screen_vtable *vtable) {
vtable->keyPressed = [](Screen *super2, int key) {
Screen_keyPressed_non_virtual(super2, key);
TextInputScreen *self = (TextInputScreen *) super2;
for (int i = 0; i < int(self->m_textInputs.size()); i++) {
TextInputBox *textInput = self->m_textInputs[i];
textInput->keyPressed(key);
}
};
vtable->keyboardNewChar = [](Screen *super2, char key) {
Screen_keyboardNewChar_non_virtual(super2, key);
TextInputScreen *self = (TextInputScreen *) super2;
for (int i = 0; i < int(self->m_textInputs.size()); i++) {
TextInputBox *textInput = self->m_textInputs[i];
textInput->charPressed(key);
}
};
vtable->mouseClicked = [](Screen *super2, int x, int y, int param_1) {
Screen_mouseClicked_non_virtual(super2, x, y, param_1);
TextInputScreen *self = (TextInputScreen *) super2;
for (int i = 0; i < int(self->m_textInputs.size()); i++) {
TextInputBox *textInput = self->m_textInputs[i];
textInput->onClick(x, y);
}
};
vtable->render = [](Screen *super2, int x, int y, float param_1) {
Screen_render_non_virtual(super2, x, y, param_1);
TextInputScreen *self = (TextInputScreen *) super2;
for (int i = 0; i < int(self->m_textInputs.size()); i++) {
TextInputBox *textInput = self->m_textInputs[i];
textInput->tick();
textInput->render();
}
};
}

View File

@ -106,7 +106,7 @@ set(SRC
src/gui/components/OptionsPane.def
src/gui/components/GuiComponent.def
src/gui/components/Button.def
src/gui/components/Gui.def
src/gui/Gui.def
src/gui/components/IntRectangle.def
src/gui/components/RectangleArea.def
src/gui/components/ScrollingPane.def
@ -145,6 +145,7 @@ set(SRC
src/input/IBuildInput.def
src/input/MouseBuildInput.def
src/input/Mouse.def
src/input/Keyboard.def
src/recipes/FurnaceRecipes.def
src/recipes/Recipes.def
src/recipes/Recipes_Type.def

View File

@ -40,6 +40,7 @@ property HitResult hit_result = 0xc38;
property int progress = 0xc60;
property PerfRenderer *perf_renderer = 0xcbc;
property CommandServer *command_server = 0xcc0;
property Font *font = 0x16c;
// Smooth Lighting
static-property bool useAmbientOcclusion = 0x136b90;

View File

@ -0,0 +1 @@
method int width(std::string *string) = 0x24d4c;

View File

@ -1,3 +1,8 @@
size 0x8;
constructor () = 0x28204;
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 fill(int x1, int y1, int x2, int y2, uint color) = 0x285f0;

View File

@ -1,15 +1,23 @@
extends GuiComponent;
size 0x48;
constructor () = 0x29028;
vtable-size 0x74;
vtable 0x1039d8;
virtual-method void updateEvents() = 0x14;
virtual-method void keyboardNewChar(char key) = 0x70;
virtual-method void keyPressed(int key) = 0x6c;
virtual-method void render(int param_1, int param_2, float param_3) = 0x8;
virtual-method void render(int x, int y, float param_1) = 0x8;
virtual-method bool handleBackEvent(bool param_1) = 0x24;
virtual-method void tick() = 0x28;
virtual-method void buttonClicked(Button *button) = 0x60;
virtual-method void init() = 0xc;
virtual-method void mouseClicked(int x, int y, int param_1) = 0x64;
virtual-method void removed() = 0x2c;
virtual-method void renderBackground() = 0x30;
virtual-method void setupPositions() = 0x10;
property Minecraft *minecraft = 0x14;
property std::vector<Button *> rendered_buttons = 0x18;
@ -17,3 +25,4 @@ property std::vector<Button *> selectable_buttons = 0x30;
property int width = 0x8;
property int height = 0xc;
property bool passthrough_input = 0x10;
property Font *font = 0x40;

View File

@ -1,5 +1,6 @@
extends Screen;
size 0x16c;
constructor () = 0x3afbc;
vtable 0x1053c0;

View File

@ -0,0 +1 @@
static-property std::vector<char> _inputText = 0x1364f0;