From 2eef4f966f9df2fd673433ddc8a69535d7e99115 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8F=8D=E6=83=9C?= Date: Fri, 25 Sep 2026 08:37:33 -0700 Subject: [PATCH 1/3] Reject partial or overflowing user ids (#168) Co-authored-by: HttpAnimations --- src/jnivm/jnivm.cc | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/jnivm/jnivm.cc b/src/jnivm/jnivm.cc index d9fd7e2..ca54648 100644 --- a/src/jnivm/jnivm.cc +++ b/src/jnivm/jnivm.cc @@ -1581,9 +1581,13 @@ jlong ParseLocalStorageUserIdFromEnv() { if (!value || value[0] == '\0') { return 0; } + errno = 0; char* end = nullptr; long long parsed = std::strtoll(value, &end, 10); - return end == value ? 0 : static_cast(parsed); + if (errno == ERANGE || end == value || *end != '\0') { + return 0; + } + return static_cast(parsed); } jlong CurrentLocalStorageUserLocked() { From f07daccc82ab23ff272b5cb4f7dfd37a6449c633 Mon Sep 17 00:00:00 2001 From: farwhile Date: Fri, 25 Sep 2026 18:39:24 +0300 Subject: [PATCH 2/3] fix(input): map middle click to MouseButton3 (#171) --- include/runtime/roblox_input_router.h | 7 ++--- src/runtime/roblox_input_router.cc | 17 ++++++------ tests/roblox_input_native_adapter_test.cc | 2 +- tests/roblox_input_router_test.cc | 33 ++++++++++++++++++----- 4 files changed, 40 insertions(+), 19 deletions(-) diff --git a/include/runtime/roblox_input_router.h b/include/runtime/roblox_input_router.h index 5b14649..d8c8a9b 100644 --- a/include/runtime/roblox_input_router.h +++ b/include/runtime/roblox_input_router.h @@ -40,9 +40,10 @@ struct AndroidKeyMapping { // through with an ABI-incompatible numeric value. AndroidKeyMapping MapSdlKeyToAndroid(uint32_t sdl_scancode, uint32_t sdl_key = 0); -// Mirrors the APK's MotionEvent.getActionButton() - 1 conversion. SDL button -// numbers are not numerically compatible with Android button bit values. -int32_t MapSdlMouseButtonToAndroid(uint8_t sdl_button); +// Translates SDL button numbers to the nativePassMouseButton argument. +// The first three are Roblox's zero-based MouseButton1/2/3 indices, not Android +// MotionEvent button bits. +int32_t MapSdlMouseButtonToRoblox(uint8_t sdl_button); int32_t MapSdlGamepadButtonToAndroid(uint8_t sdl_button); int32_t RobloxGamepadType(platform::GamepadFamily family); diff --git a/src/runtime/roblox_input_router.cc b/src/runtime/roblox_input_router.cc index 33c1551..7e4254b 100644 --- a/src/runtime/roblox_input_router.cc +++ b/src/runtime/roblox_input_router.cc @@ -274,14 +274,15 @@ AndroidKeyMapping MapSdlKeyToAndroid(uint32_t sdl_scancode, return {}; } -int32_t MapSdlMouseButtonToAndroid(uint8_t sdl_button) { +int32_t MapSdlMouseButtonToRoblox(uint8_t sdl_button) { switch (sdl_button) { case SDL_BUTTON_LEFT: return 0; case SDL_BUTTON_RIGHT: return 1; case SDL_BUTTON_MIDDLE: - return 3; + // MouseButton3 is index 2, not BUTTON_TERTIARY (4) minus one. + return 2; case SDL_BUTTON_X1: return 7; case SDL_BUTTON_X2: @@ -569,11 +570,11 @@ RobloxInputDispatchResult RobloxInputRouter::HandleMouseButtonLocked( RobloxInputEventKind::kMouseButton, Unsupported("native mouse buttons are unavailable")); } - const int32_t android_button = MapSdlMouseButtonToAndroid(event.button); - if (android_button < 0) { + const int32_t roblox_button = MapSdlMouseButtonToRoblox(event.button); + if (roblox_button < 0) { return Result(RobloxInputDispatchState::kIgnoredUnsupported, RobloxInputEventKind::kMouseButton, - Unsupported("SDL mouse button has no Android mapping")); + Unsupported("SDL mouse button has no Roblox mapping")); } if (event.x > 0.0f || event.y > 0.0f || (mouse_x_ == 0.0f && mouse_y_ == 0.0f)) { @@ -596,12 +597,12 @@ RobloxInputDispatchResult RobloxInputRouter::HandleMouseButtonLocked( } } Status status = sink_.mouse_button(sink_.context, clamped_x, clamped_y, - event.pressed, android_button); + event.pressed, roblox_button); if (status.ok()) { const auto active = std::find(active_mouse_buttons_.begin(), - active_mouse_buttons_.end(), android_button); + active_mouse_buttons_.end(), roblox_button); if (event.pressed && active == active_mouse_buttons_.end()) { - active_mouse_buttons_.push_back(android_button); + active_mouse_buttons_.push_back(roblox_button); } else if (!event.pressed && active != active_mouse_buttons_.end()) { active_mouse_buttons_.erase(active); } diff --git a/tests/roblox_input_native_adapter_test.cc b/tests/roblox_input_native_adapter_test.cc index e7eb0a7..d9846c3 100644 --- a/tests/roblox_input_native_adapter_test.cc +++ b/tests/roblox_input_native_adapter_test.cc @@ -325,7 +325,7 @@ TEST_F(RobloxInputNativeAdapterTest, CallsExactExportedJniSignatures) { (std::vector{90.0, 190.0, 4.0, -2.0})); EXPECT_EQ(probe_.calls[1].name, "mouse-button"); EXPECT_EQ(probe_.calls[1].arguments, - (std::vector{100.0, 200.0, JNI_TRUE, 3.0})); + (std::vector{100.0, 200.0, JNI_TRUE, 2.0})); EXPECT_EQ(probe_.calls[2].name, "touch"); EXPECT_EQ(probe_.calls[2].arguments, (std::vector{0.0, 320.0, 360.0, 0.0, 1280.0, 720.0})); diff --git a/tests/roblox_input_router_test.cc b/tests/roblox_input_router_test.cc index fa38409..8e1692a 100644 --- a/tests/roblox_input_router_test.cc +++ b/tests/roblox_input_router_test.cc @@ -170,13 +170,13 @@ class RobloxInputRouterTest : public ::testing::Test { RobloxInputRouter router_{Sink(&probe_)}; }; -TEST(RobloxInputMappingTest, MapsAndroidMouseButtonsExactlyLikeApk) { - EXPECT_EQ(MapSdlMouseButtonToAndroid(SDL_BUTTON_LEFT), 0); - EXPECT_EQ(MapSdlMouseButtonToAndroid(SDL_BUTTON_RIGHT), 1); - EXPECT_EQ(MapSdlMouseButtonToAndroid(SDL_BUTTON_MIDDLE), 3); - EXPECT_EQ(MapSdlMouseButtonToAndroid(SDL_BUTTON_X1), 7); - EXPECT_EQ(MapSdlMouseButtonToAndroid(SDL_BUTTON_X2), 15); - EXPECT_EQ(MapSdlMouseButtonToAndroid(42), -1); +TEST(RobloxInputMappingTest, MapsMouseButtonsForNativeInput) { + EXPECT_EQ(MapSdlMouseButtonToRoblox(SDL_BUTTON_LEFT), 0); + EXPECT_EQ(MapSdlMouseButtonToRoblox(SDL_BUTTON_RIGHT), 1); + EXPECT_EQ(MapSdlMouseButtonToRoblox(SDL_BUTTON_MIDDLE), 2); + EXPECT_EQ(MapSdlMouseButtonToRoblox(SDL_BUTTON_X1), 7); + EXPECT_EQ(MapSdlMouseButtonToRoblox(SDL_BUTTON_X2), 15); + EXPECT_EQ(MapSdlMouseButtonToRoblox(42), -1); } TEST(RobloxInputMappingTest, MapsSdlUsbKeysToLinuxAndAndroidCodes) { @@ -238,6 +238,25 @@ TEST_F(RobloxInputRouterTest, RoutesMouseMotionButtonAndVerticalWheel) { EXPECT_FLOAT_EQ(probe_.mouse_wheels[0].delta_y, -2.0f); } +TEST_F(RobloxInputRouterTest, MiddleClickUsesMouseButton3OnPressAndRelease) { + ASSERT_TRUE(router_ + .HandleEvent(Event(platform::MouseButtonEvent{ + true, SDL_BUTTON_MIDDLE, 1, 100.0f, 80.0f})) + .dispatched()); + EXPECT_EQ(router_.Snapshot().active_mouse_buttons, 1U); + ASSERT_TRUE(router_ + .HandleEvent(Event(platform::MouseButtonEvent{ + false, SDL_BUTTON_MIDDLE, 1, 100.0f, 80.0f})) + .dispatched()); + + ASSERT_EQ(probe_.mouse_buttons.size(), 2U); + EXPECT_TRUE(probe_.mouse_buttons[0].pressed); + EXPECT_EQ(probe_.mouse_buttons[0].button, 2); + EXPECT_FALSE(probe_.mouse_buttons[1].pressed); + EXPECT_EQ(probe_.mouse_buttons[1].button, 2); + EXPECT_EQ(router_.Snapshot().active_mouse_buttons, 0U); +} + TEST_F(RobloxInputRouterTest, ScalesMouseCoordinatesAfterDisplayScaleChange) { const RobloxInputDispatchResult resize = router_.HandleEvent( From 67a9f4db9792fccebdb4cad3fb5aebc6562310c5 Mon Sep 17 00:00:00 2001 From: vii Date: Fri, 25 Sep 2026 10:40:23 -0500 Subject: [PATCH 3/3] fix(input): advance the TextBox caret past trailing whitespace (#148) The final cluster rect SDL_ttf reports stops at the last inked glyph, so a caret at the end of a single-line field did not move when a trailing space was typed. Use the laid-out width there; wrapped layouts keep the clamp because their width is the widest line rather than the last one. --- src/runtime/roblox_text_surface_overlay.cc | 17 +++++++++------ tests/roblox_text_display_state_test.cc | 25 ++++++++++++++++++++++ 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/runtime/roblox_text_surface_overlay.cc b/src/runtime/roblox_text_surface_overlay.cc index 958611b..07cd8d5 100644 --- a/src/runtime/roblox_text_surface_overlay.cc +++ b/src/runtime/roblox_text_surface_overlay.cc @@ -244,7 +244,7 @@ struct TextBytePosition { }; TextBytePosition TextPosition(TTF_Text* layout, std::size_t byte_offset, - int text_width) { + int text_width, bool single_line) { if (layout == nullptr) { return {}; } @@ -259,7 +259,11 @@ TextBytePosition TextPosition(TTF_Text* layout, std::size_t byte_offset, position.y = substring.rect.y; position.height = substring.rect.h; if ((substring.flags & TTF_SUBSTRING_TEXT_END) != 0) { - position.x = std::min(text_width, substring.rect.x + substring.rect.w); + // Trailing whitespace has no ink, so the final cluster rect stops short of + // the laid-out width that a caret at the end of a single line sits behind. + position.x = single_line ? text_width + : std::min(text_width, + substring.rect.x + substring.rect.w); } return position; } @@ -501,11 +505,12 @@ Status RobloxTextSurfaceOverlay::RasterizeLocked() { TextBytePosition selection_end_position; if (layout != nullptr) { (void)TTF_GetTextSize(layout, &text_width, &text_height); - caret_position = TextPosition(layout, caret_byte, text_width); - selection_begin_position = - TextPosition(layout, selection_begin_byte, text_width); + caret_position = + TextPosition(layout, caret_byte, text_width, !wrapped_layout); + selection_begin_position = TextPosition(layout, selection_begin_byte, + text_width, !wrapped_layout); selection_end_position = - TextPosition(layout, selection_end_byte, text_width); + TextPosition(layout, selection_end_byte, text_width, !wrapped_layout); } if (!text.value.empty()) { // SDL_ttf implementations have historically differed on whether fg.a is diff --git a/tests/roblox_text_display_state_test.cc b/tests/roblox_text_display_state_test.cc index 5931957..f19b460 100644 --- a/tests/roblox_text_display_state_test.cc +++ b/tests/roblox_text_display_state_test.cc @@ -569,6 +569,31 @@ TEST(RobloxTextDisplayStateTest, WrapsMultilineTextInsideNativeTextBox) { EXPECT_TRUE(overlay.Shutdown().ok()); } +TEST(RobloxTextDisplayStateTest, AdvancesCaretForTrailingSpace) { + RobloxTextSurfaceOverlay overlay; + ASSERT_TRUE(overlay.Initialize({800, 600}).ok()); + RobloxTextDisplaySink sink = overlay.sink(); + const auto rightmost_pixel = [&](const std::string& text, + RobloxTextDisplayEvent event) { + RobloxTextDisplayUpdate update = + Show(1, text, static_cast(text.size())); + update.event = event; + update.font_size = 18.0F; + sink.update(sink.context, update); + MocktailTextOverlayFrameInfo frame; + EXPECT_TRUE(overlay.QueryFrame(&frame)); + std::vector rgba(frame.rgba_bytes); + EXPECT_TRUE(overlay.CopyFrame(frame.revision, rgba.data(), rgba.size())); + return FindAlphaBounds(frame, rgba).maximum_x; + }; + + const int typed = rightmost_pixel("hi", RobloxTextDisplayEvent::kShow); + const int spaced = rightmost_pixel("hi ", RobloxTextDisplayEvent::kUpdate); + + EXPECT_GT(spaced, typed); + EXPECT_TRUE(overlay.Shutdown().ok()); +} + } // namespace } // namespace runtime } // namespace mocktail