From 66d8c647ceed2b367cfc95cbf4b190cbf8288fdc Mon Sep 17 00:00:00 2001 From: MrTheShy <49885496+MrTheShy@users.noreply.github.com> Date: Fri, 6 Mar 2026 11:46:51 +0100 Subject: [PATCH] Fix splitscreen mouse, keyboard cursor, and local player join Mouse hover and click in split-screen was broken: the coordinate conversion from window pixels to Flash/SWF space did not account for the viewport tile-origin offset or the smaller display dimensions of each splitscreen quadrant. Now the mouse position is mapped through three steps: window pixels to UIController screen space, subtract the viewport origin (which varies per quadrant/split type), then scale from display size to SWF authoring size. This fixes hover highlighting and click targeting in all splitscreen layouts. Mouse input was also bleeding into other splitscreen players' UI groups because the scene lookup iterated all groups. Now it only checks the fullscreen group and the primary (KBM) player's group, so controller players' menus are never affected by mouse movement. Mouse grab/release (cursor lock for gameplay) was triggering for every local player's tick, causing fights between splitscreen players over the cursor state. Now only the primary pad player controls grab state. The in-game keyboard scene in PC mode had no cursor movement: typing always appended at the end and backspace always deleted from the end. Added a cursor position tracker (m_iCursorPos) so that characters are inserted at the cursor, backspace deletes behind it, and arrow keys, Home, End, and Delete all work as expected. The Flash caret is synced to the cursor position each tick. Also stopped syncing the text buffer back from Flash in PC mode, which was resetting the cursor every tick. Arrow keys in PC mode no longer get forwarded to Flash (which would move the on-screen keyboard selector instead of the text cursor). AddLocalPlayerByUserIndex was calling NotifyPlayerJoined before the IQNet slot was actually registered, passing a pointer obtained via GetLocalPlayerByUserIndex which checks customData (not set yet at that point). Now AddLocalPlayerByUserIndex is called first, and if it succeeds, the notification uses the static m_player array directly. The stub AddLocalPlayerByUserIndex now properly initialises the slot with gamertag and remote/host flags instead of being a no-op. IsSignedIn was hardcoded to return true only for pad 0, preventing splitscreen players from joining. Now it checks IsPadConnected so any connected controller can sign in. GetXUID returned INVALID_XUID for all pads except 0, which broke splitscreen player identity. Now each pad gets a unique XUID derived from the base value plus the pad index. Pinned internal resolution to 1920x1080 and removed GetSystemMetrics auto-detection which was picking up the native monitor resolution and breaking the 16:9 assumption in the viewport math and Flash layout. DPI awareness is kept for consistent pixel coordinates. --- .../Network/PlatformNetworkManagerStub.cpp | 9 ++- Minecraft.Client/Common/UI/UIController.cpp | 59 +++++++++++++--- .../Common/UI/UIScene_Keyboard.cpp | 67 +++++++++++++++++-- Minecraft.Client/Common/UI/UIScene_Keyboard.h | 1 + Minecraft.Client/Extrax64Stubs.cpp | 24 ++++--- Minecraft.Client/Minecraft.cpp | 11 ++- Minecraft.Client/PS3/Media/splashes.txt | 2 +- .../Windows64/Windows64_Minecraft.cpp | 11 ++- 8 files changed, 153 insertions(+), 31 deletions(-) diff --git a/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.cpp b/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.cpp index 85531e472..c0d1c9d3d 100644 --- a/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.cpp +++ b/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.cpp @@ -267,8 +267,13 @@ int CPlatformNetworkManagerStub::GetLocalPlayerMask(int playerIndex) bool CPlatformNetworkManagerStub::AddLocalPlayerByUserIndex( int userIndex ) { - NotifyPlayerJoined(m_pIQNet->GetLocalPlayerByUserIndex(userIndex)); - return ( m_pIQNet->AddLocalPlayerByUserIndex(userIndex) == S_OK ); + if ( m_pIQNet->AddLocalPlayerByUserIndex(userIndex) != S_OK ) + return false; + // Player is now registered in IQNet — get a pointer and notify the network layer. + // Use the static array directly: GetLocalPlayerByUserIndex checks customData which + // isn't set until addNetworkPlayer runs inside NotifyPlayerJoined. + NotifyPlayerJoined(&IQNet::m_player[userIndex]); + return true; } bool CPlatformNetworkManagerStub::RemoveLocalPlayerByUserIndex( int userIndex ) diff --git a/Minecraft.Client/Common/UI/UIController.cpp b/Minecraft.Client/Common/UI/UIController.cpp index f059d8211..9418d9509 100644 --- a/Minecraft.Client/Common/UI/UIController.cpp +++ b/Minecraft.Client/Common/UI/UIController.cpp @@ -804,13 +804,16 @@ void UIController::tickInput() eUILayer_Fullscreen, eUILayer_Scene, }; - for (int l = 0; l < _countof(mouseLayers) && !pScene; ++l) + // Only check the fullscreen group and the primary (KBM) player's group. + // Other splitscreen players use controllers — mouse must not affect them. + const int mouseGroups[] = { (int)eUIGroup_Fullscreen, ProfileManager.GetPrimaryPad() + 1 }; + for (int l = 0; l < _countof(mouseLayers) && !pScene; ++l) + { + for (int g = 0; g < _countof(mouseGroups) && !pScene; ++g) { - for (int grp = 0; grp < eUIGroup_COUNT && !pScene; ++grp) - { - pScene = m_groups[grp]->GetTopScene(mouseLayers[l]); - } + pScene = m_groups[mouseGroups[g]]->GetTopScene(mouseLayers[l]); } + } if (pScene && pScene->getMovie()) { int rawMouseX = g_KBMInput.GetMouseX(); @@ -823,7 +826,12 @@ void UIController::tickInput() m_lastHoverMouseX = rawMouseX; m_lastHoverMouseY = rawMouseY; - // Convert mouse to scene/movie coordinates + // Convert mouse window-pixel coords to Flash/SWF authoring coords. + // In split-screen the scene is rendered at a tile-origin offset + // and at a smaller display size, so we must: + // 1. Map window pixels → UIController screen space + // 2. Subtract the viewport tile origin + // 3. Scale from display dimensions to SWF authoring dimensions F32 sceneMouseX = (F32)rawMouseX; F32 sceneMouseY = (F32)rawMouseY; { @@ -835,8 +843,43 @@ void UIController::tickInput() int winH = rc.bottom - rc.top; if (winW > 0 && winH > 0) { - sceneMouseX = sceneMouseX * ((F32)pScene->getRenderWidth() / (F32)winW); - sceneMouseY = sceneMouseY * ((F32)pScene->getRenderHeight() / (F32)winH); + // Step 1: window pixels → screen space + F32 screenX = sceneMouseX * (getScreenWidth() / (F32)winW); + F32 screenY = sceneMouseY * (getScreenHeight() / (F32)winH); + + // Step 2 & 3: account for split-screen viewport + C4JRender::eViewportType vp = pScene->GetParentLayer()->getViewport(); + S32 displayW = 0, displayH = 0; + getRenderDimensions(vp, displayW, displayH); + + S32 originX = 0, originY = 0; + switch (vp) + { + case C4JRender::VIEWPORT_TYPE_SPLIT_TOP: + originX = (S32)(getScreenWidth() / 4); break; + case C4JRender::VIEWPORT_TYPE_SPLIT_BOTTOM: + originX = (S32)(getScreenWidth() / 4); + originY = (S32)(getScreenHeight() / 2); break; + case C4JRender::VIEWPORT_TYPE_SPLIT_LEFT: + originY = (S32)(getScreenHeight() / 4); break; + case C4JRender::VIEWPORT_TYPE_SPLIT_RIGHT: + originX = (S32)(getScreenWidth() / 2); + originY = (S32)(getScreenHeight() / 4); break; + case C4JRender::VIEWPORT_TYPE_QUADRANT_TOP_RIGHT: + originX = (S32)(getScreenWidth() / 2); break; + case C4JRender::VIEWPORT_TYPE_QUADRANT_BOTTOM_LEFT: + originY = (S32)(getScreenHeight() / 2); break; + case C4JRender::VIEWPORT_TYPE_QUADRANT_BOTTOM_RIGHT: + originX = (S32)(getScreenWidth() / 2); + originY = (S32)(getScreenHeight() / 2); break; + default: break; // FULLSCREEN and QUADRANT_TOP_LEFT: origin (0,0) + } + + if (displayW > 0 && displayH > 0) + { + sceneMouseX = (screenX - originX) * ((F32)pScene->getRenderWidth() / (F32)displayW); + sceneMouseY = (screenY - originY) * ((F32)pScene->getRenderHeight() / (F32)displayH); + } } } } diff --git a/Minecraft.Client/Common/UI/UIScene_Keyboard.cpp b/Minecraft.Client/Common/UI/UIScene_Keyboard.cpp index b28564c3a..be3ee118d 100644 --- a/Minecraft.Client/Common/UI/UIScene_Keyboard.cpp +++ b/Minecraft.Client/Common/UI/UIScene_Keyboard.cpp @@ -38,6 +38,7 @@ UIScene_Keyboard::UIScene_Keyboard(int iPad, void *initData, UILayer *parentLaye } m_win64TextBuffer = defaultText; + m_iCursorPos = (int)m_win64TextBuffer.length(); m_EnterTextLabel.init(titleText); m_KeyboardTextInput.init(defaultText, -1); @@ -111,6 +112,9 @@ UIScene_Keyboard::UIScene_Keyboard(int iPad, void *initData, UILayer *parentLaye if (IggyValuePathMakeNameRef(&keyPath, root, s_keyNames[i])) IggyValueSetBooleanRS(&keyPath, nameVisible, NULL, false); } + + m_KeyboardTextInput.setCaretVisible(true); + m_KeyboardTextInput.setCaretIndex(m_iCursorPos); } #endif @@ -165,9 +169,13 @@ void UIScene_Keyboard::tick() // Sync our buffer from Flash so we pick up changes made via controller/on-screen buttons. // Without this, switching between controller and keyboard would use stale text. - const wchar_t* flashText = m_KeyboardTextInput.getLabel(); - if (flashText) - m_win64TextBuffer = flashText; + // In PC mode we own the buffer — skip sync to preserve cursor position. + if (!m_bPCMode) + { + const wchar_t* flashText = m_KeyboardTextInput.getLabel(); + if (flashText) + m_win64TextBuffer = flashText; + } // Accumulate physical keyboard chars into our own buffer, then push to Flash via setLabel. // This bypasses Iggy's focus system (char events only route to the focused element). @@ -178,7 +186,16 @@ void UIScene_Keyboard::tick() { if (ch == 0x08) // backspace { - if (!m_win64TextBuffer.empty()) + if (m_bPCMode) + { + if (m_iCursorPos > 0) + { + m_win64TextBuffer.erase(m_iCursorPos - 1, 1); + m_iCursorPos--; + changed = true; + } + } + else if (!m_win64TextBuffer.empty()) { m_win64TextBuffer.pop_back(); changed = true; @@ -194,13 +211,45 @@ void UIScene_Keyboard::tick() } else if ((int)m_win64TextBuffer.length() < m_win64MaxChars) { - m_win64TextBuffer += ch; + if (m_bPCMode) + { + m_win64TextBuffer.insert(m_iCursorPos, 1, ch); + m_iCursorPos++; + } + else + { + m_win64TextBuffer += ch; + } + changed = true; + } + } + + if (m_bPCMode) + { + // Arrow keys, Home, End, Delete for cursor movement + if (g_KBMInput.IsKeyPressed(VK_LEFT) && m_iCursorPos > 0) + m_iCursorPos--; + if (g_KBMInput.IsKeyPressed(VK_RIGHT) && m_iCursorPos < (int)m_win64TextBuffer.length()) + m_iCursorPos++; + if (g_KBMInput.IsKeyPressed(VK_HOME)) + m_iCursorPos = 0; + if (g_KBMInput.IsKeyPressed(VK_END)) + m_iCursorPos = (int)m_win64TextBuffer.length(); + if (g_KBMInput.IsKeyPressed(VK_DELETE) && m_iCursorPos < (int)m_win64TextBuffer.length()) + { + m_win64TextBuffer.erase(m_iCursorPos, 1); changed = true; } } if (changed) m_KeyboardTextInput.setLabel(m_win64TextBuffer.c_str(), true /*instant*/); + + if (m_bPCMode) + { + m_KeyboardTextInput.setCaretVisible(true); + m_KeyboardTextInput.setCaretIndex(m_iCursorPos); + } } #endif @@ -269,11 +318,17 @@ void UIScene_Keyboard::handleInput(int iPad, int key, bool repeat, bool pressed, switch(key) { case ACTION_MENU_OK: + sendInputToMovie(key, repeat, pressed, released); + handled = true; + break; case ACTION_MENU_LEFT: case ACTION_MENU_RIGHT: case ACTION_MENU_UP: case ACTION_MENU_DOWN: - sendInputToMovie(key, repeat, pressed, released); +#ifdef _WINDOWS64 + if (!m_bPCMode) +#endif + sendInputToMovie(key, repeat, pressed, released); handled = true; break; } diff --git a/Minecraft.Client/Common/UI/UIScene_Keyboard.h b/Minecraft.Client/Common/UI/UIScene_Keyboard.h index 054322f20..146934c1f 100644 --- a/Minecraft.Client/Common/UI/UIScene_Keyboard.h +++ b/Minecraft.Client/Common/UI/UIScene_Keyboard.h @@ -13,6 +13,7 @@ private: wstring m_win64TextBuffer; int m_win64MaxChars; bool m_bPCMode; // Hides on-screen keyboard buttons; physical keyboard only + int m_iCursorPos; #endif protected: diff --git a/Minecraft.Client/Extrax64Stubs.cpp b/Minecraft.Client/Extrax64Stubs.cpp index 29865e400..d37238a6b 100644 --- a/Minecraft.Client/Extrax64Stubs.cpp +++ b/Minecraft.Client/Extrax64Stubs.cpp @@ -233,7 +233,20 @@ void Win64_SetupRemoteQNetPlayer(IQNetPlayer * player, BYTE smallId, bool isHost static bool Win64_IsActivePlayer(IQNetPlayer* p, DWORD index); -HRESULT IQNet::AddLocalPlayerByUserIndex(DWORD dwUserIndex) { return S_OK; } +HRESULT IQNet::AddLocalPlayerByUserIndex(DWORD dwUserIndex) { + if (dwUserIndex >= MINECRAFT_NET_MAX_PLAYERS) return E_FAIL; + m_player[dwUserIndex].m_isRemote = false; + m_player[dwUserIndex].m_isHostPlayer = false; + // Give the joining player a distinct gamertag + extern wchar_t g_Win64UsernameW[17]; + if (dwUserIndex == 0) + wcscpy_s(m_player[0].m_gamertag, 32, g_Win64UsernameW); + else + swprintf_s(m_player[dwUserIndex].m_gamertag, 32, L"%s(%d)", g_Win64UsernameW, dwUserIndex + 1); + if (dwUserIndex >= s_playerCount) + s_playerCount = dwUserIndex + 1; + return S_OK; +} IQNetPlayer* IQNet::GetHostPlayer() { return &m_player[0]; } IQNetPlayer* IQNet::GetLocalPlayerByUserIndex(DWORD dwUserIndex) { @@ -559,7 +572,7 @@ void C_4JProfile::SetTrialTextStringTable(CXuiStringTable * pStringTable, int void C_4JProfile::SetTrialAwardText(eAwardType AwardType, int iTitle, int iText) {} int C_4JProfile::GetLockedProfile() { return 0; } void C_4JProfile::SetLockedProfile(int iProf) {} -bool C_4JProfile::IsSignedIn(int iQuadrant) { return (iQuadrant == 0); } +bool C_4JProfile::IsSignedIn(int iQuadrant) { return InputManager.IsPadConnected(iQuadrant); } bool C_4JProfile::IsSignedInLive(int iProf) { return true; } bool C_4JProfile::IsGuest(int iQuadrant) { return false; } UINT C_4JProfile::RequestSignInUI(bool bFromInvite, bool bLocalGame, bool bNoGuestsAllowed, bool bMultiplayerSignIn, bool bAddUser, int(*Func)(LPVOID, const bool, const int iPad), LPVOID lpParam, int iQuadrant) { return 0; } @@ -570,13 +583,8 @@ bool C_4JProfile::QuerySigninStatus(void) { return true; } void C_4JProfile::GetXUID(int iPad, PlayerUID * pXuid, bool bOnlineXuid) { #ifdef _WINDOWS64 - if (iPad != 0) - { - *pXuid = INVALID_XUID; - return; - } if (IQNet::s_isHosting) - *pXuid = 0xe000d45248242f2e; + *pXuid = 0xe000d45248242f2e + iPad; else *pXuid = 0xe000d45248242f2e + WinsockNetLayer::GetLocalSmallId(); #else diff --git a/Minecraft.Client/Minecraft.cpp b/Minecraft.Client/Minecraft.cpp index b87745f03..38bfcdbf4 100644 --- a/Minecraft.Client/Minecraft.cpp +++ b/Minecraft.Client/Minecraft.cpp @@ -2325,16 +2325,21 @@ void Minecraft::tick(bool bFirst, bool bUpdateTextures) } #ifdef _WINDOWS64 - if ((screen != NULL || ui.GetMenuDisplayed(iPad)) && g_KBMInput.IsMouseGrabbed()) + // Mouse grab/release only for the primary (KBM) player — splitscreen + // players use controllers and must never fight over the cursor state. + if (iPad == ProfileManager.GetPrimaryPad()) { - g_KBMInput.SetMouseGrabbed(false); + if ((screen != NULL || ui.GetMenuDisplayed(iPad)) && g_KBMInput.IsMouseGrabbed()) + { + g_KBMInput.SetMouseGrabbed(false); + } } #endif if (screen == NULL && !ui.GetMenuDisplayed(iPad) ) { #ifdef _WINDOWS64 - if (!g_KBMInput.IsMouseGrabbed() && g_KBMInput.IsWindowFocused()) + if (iPad == ProfileManager.GetPrimaryPad() && !g_KBMInput.IsMouseGrabbed() && g_KBMInput.IsWindowFocused()) { g_KBMInput.SetMouseGrabbed(true); } diff --git a/Minecraft.Client/PS3/Media/splashes.txt b/Minecraft.Client/PS3/Media/splashes.txt index 7f7835c6c..df4a4be19 100644 --- a/Minecraft.Client/PS3/Media/splashes.txt +++ b/Minecraft.Client/PS3/Media/splashes.txt @@ -43,7 +43,7 @@ Dungeon! Exclusive! The bee's knees! Down with O.P.P.! -Closed source! +Closed source xD! Classy! Wow! Not on steam! diff --git a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp index 2eef5f737..ae26b6d6b 100644 --- a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp +++ b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp @@ -86,6 +86,13 @@ DWORD dwProfileSettingsA[NUM_PROFILE_VALUES]= BOOL g_bWidescreen = TRUE; +// DO NOT auto-detect from the monitor (GetSystemMetrics etc.) — the game resolution +// must stay 1920x1080. The precompiled 4J_Render lib, all Flash SWF assets, and the +// split-screen viewport math (RenderManager.StateSetViewport, UIController::getRenderDimensions, +// setupRenderPosition) all assume a 16:9 framebuffer. Using the native monitor resolution +// breaks split-screen on ultrawide/non-16:9 displays: the UI renders offset and the +// bottom portion of each viewport is left empty. +// ApplyScreenMode() can still override these for debug/test resolutions via launch args. int g_iScreenWidth = 1920; int g_iScreenHeight = 1080; @@ -1177,10 +1184,8 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, if (pSlash) { *(pSlash + 1) = '\0'; SetCurrentDirectoryA(szExeDir); } } - // Declare DPI awareness so GetSystemMetrics returns physical pixels + // Declare DPI awareness so pixel coordinates are consistent SetProcessDPIAware(); - g_iScreenWidth = GetSystemMetrics(SM_CXSCREEN); - g_iScreenHeight = GetSystemMetrics(SM_CYSCREEN); // Load username from username.txt char exePath[MAX_PATH] = {};