From 9f5fd833552e41da4e24ad0f1284728d85a8d9b7 Mon Sep 17 00:00:00 2001 From: haxi0 Date: Fri, 13 Mar 2026 01:33:01 +0300 Subject: [PATCH] rebind voice controls: ptt on z, mute on x, and fix controller mute toggle routing --- Minecraft.Client/Common/App_enums.h | 3 +- Minecraft.Client/Common/Media/media.txt | 1 + Minecraft.Client/Gui.cpp | 146 ++++++++++++++++++ Minecraft.Client/Minecraft.cpp | 19 ++- Minecraft.Client/PlayerRenderer.cpp | 57 +++++-- Minecraft.Client/VoiceChatManager.cpp | 41 +++++ Minecraft.Client/VoiceChatManager.h | 7 + .../Windows64/KeyboardMouseInput.h | 2 + .../Windows64/Windows64_Minecraft.cpp | 9 +- 9 files changed, 264 insertions(+), 21 deletions(-) diff --git a/Minecraft.Client/Common/App_enums.h b/Minecraft.Client/Common/App_enums.h index 15a17978..78cd64da 100644 --- a/Minecraft.Client/Common/App_enums.h +++ b/Minecraft.Client/Common/App_enums.h @@ -870,6 +870,7 @@ enum EControllerActions MINECRAFT_ACTION_DPAD_RIGHT, MINECRAFT_ACTION_DPAD_UP, MINECRAFT_ACTION_DPAD_DOWN, + MINECRAFT_ACTION_VOICE_MUTE, MINECRAFT_ACTION_MAX, @@ -948,4 +949,4 @@ enum eMCLang eMCLang_hans, eMCLang_hant, -}; \ No newline at end of file +}; diff --git a/Minecraft.Client/Common/Media/media.txt b/Minecraft.Client/Common/Media/media.txt index 59b0fbe1..4f91764e 100644 --- a/Minecraft.Client/Common/Media/media.txt +++ b/Minecraft.Client/Common/Media/media.txt @@ -5,3 +5,4 @@ Graphics\MinecraftIcon.png Graphics\TexturePackIcon.png Graphics\InGameInfo\voiceSpeaking.png Graphics\InGameInfo\voiceNotSpeaking.png +Graphics\InGameInfo\voiceMuted.png diff --git a/Minecraft.Client/Gui.cpp b/Minecraft.Client/Gui.cpp index 43b41998..9b45afd1 100644 --- a/Minecraft.Client/Gui.cpp +++ b/Minecraft.Client/Gui.cpp @@ -9,8 +9,13 @@ #include "GameMode.h" #include "Lighting.h" #include "ChatScreen.h" +#include "VoiceChatManager.h" +#include "BufferedImage.h" +#include "Tesselator.h" #include "MultiPlayerLevel.h" #include "..\Minecraft.World\JavaMath.h" +#include "..\Minecraft.World\File.h" +#include "..\Minecraft.World\InputOutputStream.h" #include "..\Minecraft.World\net.minecraft.world.entity.player.h" #include "..\Minecraft.World\net.minecraft.world.effect.h" #include "..\Minecraft.World\net.minecraft.world.food.h" @@ -42,6 +47,110 @@ float Gui::currentGuiBlendFactor = 1.0f; // 4J added float Gui::currentGuiScaleFactor = 1.0f; // 4J added ItemRenderer *Gui::itemRenderer = new ItemRenderer(); +namespace +{ + enum VoiceHudIconState + { + VOICE_HUD_ICON_NOT_SPEAKING = 0, + VOICE_HUD_ICON_SPEAKING = 1, + VOICE_HUD_ICON_MUTED = 2 + }; + + static const int VOICE_HUD_ICON_WIDTH = 24; + static const int VOICE_HUD_ICON_HEIGHT = 16; + + BufferedImage *LoadVoiceHudIconImage(VoiceHudIconState state) + { + const wchar_t *filePath = L"Common\\Media\\Graphics\\InGameInfo\\voiceNotSpeaking.png"; + if (state == VOICE_HUD_ICON_SPEAKING) + { + filePath = L"Common\\Media\\Graphics\\InGameInfo\\voiceSpeaking.png"; + } + else if (state == VOICE_HUD_ICON_MUTED) + { + filePath = L"Common\\Media\\Graphics\\InGameInfo\\voiceMuted.png"; + } + + File file(filePath); + if (file.exists()) + { + byteArray imageBytes(static_cast(file.length())); + FileInputStream stream(file); + const int bytesRead = stream.read(imageBytes); + stream.close(); + if (bytesRead > 0) + { + imageBytes.length = static_cast(bytesRead); + BufferedImage *image = new BufferedImage(imageBytes.data, imageBytes.length); + delete[] imageBytes.data; + return image; + } + delete[] imageBytes.data; + } + + const wchar_t *archivePath = L"Graphics\\InGameInfo\\voiceNotSpeaking.png"; + if (state == VOICE_HUD_ICON_SPEAKING) + { + archivePath = L"Graphics\\InGameInfo\\voiceSpeaking.png"; + } + else if (state == VOICE_HUD_ICON_MUTED) + { + archivePath = L"Graphics\\InGameInfo\\voiceMuted.png"; + } + + if (!app.hasArchiveFile(archivePath)) + { + return nullptr; + } + + byteArray imageBytes = app.getArchiveFile(archivePath); + if (imageBytes.data == nullptr || imageBytes.length == 0) + { + return nullptr; + } + + BufferedImage *image = new BufferedImage(imageBytes.data, imageBytes.length); + delete[] imageBytes.data; + return image; + } + + int GetVoiceHudIconTextureId(Textures *textures, VoiceHudIconState state) + { + static int s_voiceHudTextureIds[3] = { -1, -1, -1 }; + const int stateIndex = static_cast(state); + if (stateIndex < 0 || stateIndex > 2) + { + return -1; + } + + int &textureId = s_voiceHudTextureIds[stateIndex]; + if (textureId >= 0) + { + return textureId; + } + + BufferedImage *image = LoadVoiceHudIconImage(state); + if (image == nullptr) + { + return -1; + } + + textureId = textures->getTexture(image, C4JRender::TEXTURE_FORMAT_RxGyBzAw, false); + return textureId; + } + + void DrawVoiceHudIcon(int x, int y) + { + Tesselator *t = Tesselator::getInstance(); + t->begin(); + t->vertexUV(static_cast(x), static_cast(y + VOICE_HUD_ICON_HEIGHT), 0.0f, 0.0f, 1.0f); + t->vertexUV(static_cast(x + VOICE_HUD_ICON_WIDTH), static_cast(y + VOICE_HUD_ICON_HEIGHT), 0.0f, 1.0f, 1.0f); + t->vertexUV(static_cast(x + VOICE_HUD_ICON_WIDTH), static_cast(y), 0.0f, 1.0f, 0.0f); + t->vertexUV(static_cast(x), static_cast(y), 0.0f, 0.0f, 0.0f); + t->end(); + } +} + Gui::Gui(Minecraft *minecraft) { // 4J - initialisers added @@ -759,6 +868,43 @@ void Gui::render(float a, bool mouseFree, int xMouse, int yMouse) glDisable(GL_RESCALE_NORMAL); } } + + if (bDisplayGui) + { + VoiceChatManager &vcm = VoiceChatManager::getInstance(); + VoiceHudIconState iconState = VOICE_HUD_ICON_NOT_SPEAKING; + if (vcm.isLocalMuted()) + { + iconState = VOICE_HUD_ICON_MUTED; + } + else if (minecraft->player != nullptr && vcm.isEntitySpeaking(minecraft->player->entityId)) + { + iconState = VOICE_HUD_ICON_SPEAKING; + } + + const int iconTextureId = GetVoiceHudIconTextureId(minecraft->textures, iconState); + if (iconTextureId >= 0) + { + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glColor4f(1.0f, 1.0f, 1.0f, 1.0f); + minecraft->textures->bind(iconTextureId); + + int x; + int y; + if (bTwoPlayerSplitscreen) + { + x = iWidthOffset + screenWidth / 2 - VOICE_HUD_ICON_WIDTH / 2; + y = iHeightOffset + screenHeight - iSafezoneYHalf - iTooltipsYOffset - VOICE_HUD_ICON_HEIGHT - 24; + } + else + { + x = screenWidth / 2 - VOICE_HUD_ICON_WIDTH / 2; + y = screenHeight - iSafezoneYHalf - iTooltipsYOffset - VOICE_HUD_ICON_HEIGHT - 24; + } + DrawVoiceHudIcon(x, y); + } + } } #if RENDER_HUD diff --git a/Minecraft.Client/Minecraft.cpp b/Minecraft.Client/Minecraft.cpp index 56d9eac5..e78310d5 100644 --- a/Minecraft.Client/Minecraft.cpp +++ b/Minecraft.Client/Minecraft.cpp @@ -1472,7 +1472,10 @@ void Minecraft::run_middle() if(InputManager.ButtonPressed(i, MINECRAFT_ACTION_SNEAK_TOGGLE)) localplayers[i]->ullButtonsPressed|=1LL<ullButtonsPressed|=1LL<ullButtonsPressed|=1LL<ullButtonsPressed|=1LL<ullButtonsPressed|=1LL<ullButtonsPressed|=1LL<ullButtonsPressed|=1LL<ullButtonsPressed & (1LL << MINECRAFT_ACTION_VOICE_MUTE))) + { + vcm.toggleLocalMuted(); + } + // Push-To-Talk input (used when voice mode is Push-To-Talk) bool pttPressed = false; #ifdef _WINDOWS64 - if (g_KBMInput.IsKeyDown('V')) pttPressed = true; + if (iPad == 0 && g_KBMInput.IsKeyDown(KeyboardMouseInput::KEY_VOICE_PTT)) pttPressed = true; #endif if (InputManager.ButtonDown(iPad, MINECRAFT_ACTION_DPAD_UP)) pttPressed = true; + if (vcm.isLocalMuted()) pttPressed = false; vcm.setPushToTalk(pttPressed); diff --git a/Minecraft.Client/PlayerRenderer.cpp b/Minecraft.Client/PlayerRenderer.cpp index 1278dbf3..5c42faae 100644 --- a/Minecraft.Client/PlayerRenderer.cpp +++ b/Minecraft.Client/PlayerRenderer.cpp @@ -62,11 +62,24 @@ namespace static const float VOICE_ICON_HALF_HEIGHT = 8.0f; static const float VOICE_ICON_HALF_WIDTH = VOICE_ICON_HALF_HEIGHT * (60.0f / 40.0f); - BufferedImage *LoadVoiceIndicatorImage(bool speaking) + enum VoiceIndicatorState { - const wchar_t *filePath = speaking - ? L"Common\\Media\\Graphics\\InGameInfo\\voiceSpeaking.png" - : L"Common\\Media\\Graphics\\InGameInfo\\voiceNotSpeaking.png"; + VOICE_INDICATOR_NOT_SPEAKING = 0, + VOICE_INDICATOR_SPEAKING = 1, + VOICE_INDICATOR_MUTED = 2 + }; + + BufferedImage *LoadVoiceIndicatorImage(VoiceIndicatorState state) + { + const wchar_t *filePath = L"Common\\Media\\Graphics\\InGameInfo\\voiceNotSpeaking.png"; + if (state == VOICE_INDICATOR_SPEAKING) + { + filePath = L"Common\\Media\\Graphics\\InGameInfo\\voiceSpeaking.png"; + } + else if (state == VOICE_INDICATOR_MUTED) + { + filePath = L"Common\\Media\\Graphics\\InGameInfo\\voiceMuted.png"; + } File file(filePath); if (file.exists()) { @@ -84,9 +97,15 @@ namespace delete[] imageBytes.data; } - const wchar_t *archivePath = speaking - ? L"Graphics\\InGameInfo\\voiceSpeaking.png" - : L"Graphics\\InGameInfo\\voiceNotSpeaking.png"; + const wchar_t *archivePath = L"Graphics\\InGameInfo\\voiceNotSpeaking.png"; + if (state == VOICE_INDICATOR_SPEAKING) + { + archivePath = L"Graphics\\InGameInfo\\voiceSpeaking.png"; + } + else if (state == VOICE_INDICATOR_MUTED) + { + archivePath = L"Graphics\\InGameInfo\\voiceMuted.png"; + } if (!app.hasArchiveFile(archivePath)) { return nullptr; @@ -103,18 +122,21 @@ namespace return image; } - int GetVoiceIndicatorTextureId(Textures *textures, bool speaking) + int GetVoiceIndicatorTextureId(Textures *textures, VoiceIndicatorState state) { - static int s_voiceSpeakingTextureId = -1; - static int s_voiceNotSpeakingTextureId = -1; - - int &textureId = speaking ? s_voiceSpeakingTextureId : s_voiceNotSpeakingTextureId; + static int s_voiceTextureIds[3] = { -1, -1, -1 }; + int stateIndex = static_cast(state); + if (stateIndex < 0 || stateIndex > 2) + { + return -1; + } + int &textureId = s_voiceTextureIds[stateIndex]; if (textureId >= 0) { return textureId; } - BufferedImage *image = LoadVoiceIndicatorImage(speaking); + BufferedImage *image = LoadVoiceIndicatorImage(state); if (image == nullptr) { return -1; @@ -547,7 +569,12 @@ void PlayerRenderer::renderNameTags(shared_ptr player, double x, d // Voice chat indicator if (player != nullptr) { - const bool isSpeaking = VoiceChatManager::getInstance().isEntitySpeaking(player->entityId); + VoiceChatManager &vcm = VoiceChatManager::getInstance(); + const bool isSpeaking = vcm.isEntitySpeaking(player->entityId); + const bool isLocalMuted = (dynamic_cast(player.get()) != nullptr) && vcm.isLocalMuted(); + const VoiceIndicatorState state = isLocalMuted + ? VOICE_INDICATOR_MUTED + : (isSpeaking ? VOICE_INDICATOR_SPEAKING : VOICE_INDICATOR_NOT_SPEAKING); glPushMatrix(); // Position above the head (slightly higher than the name tag) glTranslatef(static_cast(x), static_cast(y) + player->bbHeight + 0.75f, static_cast(z)); @@ -565,7 +592,7 @@ void PlayerRenderer::renderNameTags(shared_ptr player, double x, d glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glColor4f(1, 1, 1, 1); - const int voiceIndicatorTextureId = GetVoiceIndicatorTextureId(entityRenderDispatcher->textures, isSpeaking); + const int voiceIndicatorTextureId = GetVoiceIndicatorTextureId(entityRenderDispatcher->textures, state); if (voiceIndicatorTextureId < 0) { glPopMatrix(); diff --git a/Minecraft.Client/VoiceChatManager.cpp b/Minecraft.Client/VoiceChatManager.cpp index c129616f..29e1a2c9 100644 --- a/Minecraft.Client/VoiceChatManager.cpp +++ b/Minecraft.Client/VoiceChatManager.cpp @@ -128,6 +128,7 @@ VoiceChatManager::VoiceChatManager() , m_pushToTalkHoldFrames(0) , m_captureFilterLastInput(0.0f) , m_captureFilterLastOutput(0.0f) + , m_localMuted(false) { memset(m_captureBuffer, 0, sizeof(m_captureBuffer)); InitializeCriticalSection(&m_captureLock); @@ -605,6 +606,11 @@ void VoiceChatManager::tick(Minecraft *minecraft) shouldSend = m_voiceActivationHoldFrames > 0; } + if (m_localMuted) + { + shouldSend = false; + } + if (shouldSend) { // Mark our local player as speaking for the indicator @@ -627,6 +633,11 @@ void VoiceChatManager::tick(Minecraft *minecraft) LeaveCriticalSection(&m_captureLock); + if (m_localMuted) + { + clearSpeaking(minecraft->player->entityId); + } + // Cleanup stale remote streams (no data for >2 seconds) int64_t now = System::currentTimeMillis(); EnterCriticalSection(&m_playbackLock); @@ -726,6 +737,13 @@ bool VoiceChatManager::isEntitySpeaking(int entityId) return speaking; } +void VoiceChatManager::clearSpeaking(int entityId) +{ + EnterCriticalSection(&m_speakingLock); + m_speakingEntities.erase(entityId); + LeaveCriticalSection(&m_speakingLock); +} + void VoiceChatManager::tickSpeakingState() { EnterCriticalSection(&m_speakingLock); @@ -764,3 +782,26 @@ void VoiceChatManager::setVoiceActivationGainPercent(int percent) if (percent > MAX_VOICE_ACTIVATION_GAIN_PERCENT) percent = MAX_VOICE_ACTIVATION_GAIN_PERCENT; m_voiceActivationGainPercent = percent; } + +void VoiceChatManager::setLocalMuted(bool muted) +{ + if (m_localMuted == muted) + { + return; + } + + m_localMuted = muted; + if (m_localMuted) + { + m_isPushToTalkActive = false; + m_pushToTalkHoldFrames = 0; + m_voiceActivationHoldFrames = 0; + } + + app.DebugPrintf("VoiceChat: Local mic %s\n", m_localMuted ? "muted" : "unmuted"); +} + +void VoiceChatManager::toggleLocalMuted() +{ + setLocalMuted(!m_localMuted); +} diff --git a/Minecraft.Client/VoiceChatManager.h b/Minecraft.Client/VoiceChatManager.h index 4157f0ff..ad919a58 100644 --- a/Minecraft.Client/VoiceChatManager.h +++ b/Minecraft.Client/VoiceChatManager.h @@ -74,11 +74,17 @@ public: void setVoiceActivationGainPercent(int percent); int getVoiceActivationGainPercent() const { return m_voiceActivationGainPercent; } + // Local microphone mute controls + void setLocalMuted(bool muted); + void toggleLocalMuted(); + bool isLocalMuted() const { return m_localMuted; } + bool isInitialized() const { return m_initialized; } // Speaking state tracking for rendering indicators void markSpeaking(int entityId); bool isEntitySpeaking(int entityId); + void clearSpeaking(int entityId); void tickSpeakingState(); private: @@ -113,6 +119,7 @@ private: int m_pushToTalkHoldFrames; float m_captureFilterLastInput; float m_captureFilterLastOutput; + bool m_localMuted; // Capture buffer (ring buffer for PCM data captured from mic) static const int CAPTURE_BUFFER_SIZE = 96000; // 2 seconds at 48kHz mono diff --git a/Minecraft.Client/Windows64/KeyboardMouseInput.h b/Minecraft.Client/Windows64/KeyboardMouseInput.h index 0f14dfa1..b03c5c0c 100644 --- a/Minecraft.Client/Windows64/KeyboardMouseInput.h +++ b/Minecraft.Client/Windows64/KeyboardMouseInput.h @@ -31,6 +31,8 @@ public: static const int KEY_THIRD_PERSON = VK_F5; static const int KEY_DEBUG_INFO = VK_F3; static const int KEY_DEBUG_MENU = VK_F4; + static const int KEY_VOICE_PTT = 'Z'; + static const int KEY_VOICE_MUTE = 'X'; void Init(); void Tick(); diff --git a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp index 70aeb22b..415f84b3 100644 --- a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp +++ b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp @@ -355,7 +355,8 @@ void DefineActions(void) InputManager.SetGameJoypadMaps(MAP_STYLE_0,MINECRAFT_ACTION_SNEAK_TOGGLE, _360_JOY_BUTTON_RTHUMB); InputManager.SetGameJoypadMaps(MAP_STYLE_0,MINECRAFT_ACTION_CRAFTING, _360_JOY_BUTTON_X); InputManager.SetGameJoypadMaps(MAP_STYLE_0,MINECRAFT_ACTION_RENDER_THIRD_PERSON, _360_JOY_BUTTON_LTHUMB); - InputManager.SetGameJoypadMaps(MAP_STYLE_0,MINECRAFT_ACTION_GAME_INFO, _360_JOY_BUTTON_BACK); + InputManager.SetGameJoypadMaps(MAP_STYLE_0,MINECRAFT_ACTION_GAME_INFO, 0); + InputManager.SetGameJoypadMaps(MAP_STYLE_0,MINECRAFT_ACTION_VOICE_MUTE, _360_JOY_BUTTON_BACK); InputManager.SetGameJoypadMaps(MAP_STYLE_0,MINECRAFT_ACTION_DPAD_LEFT, _360_JOY_BUTTON_DPAD_LEFT); InputManager.SetGameJoypadMaps(MAP_STYLE_0,MINECRAFT_ACTION_DPAD_RIGHT, _360_JOY_BUTTON_DPAD_RIGHT); @@ -404,7 +405,8 @@ void DefineActions(void) InputManager.SetGameJoypadMaps(MAP_STYLE_1,MINECRAFT_ACTION_SNEAK_TOGGLE, _360_JOY_BUTTON_LTHUMB); InputManager.SetGameJoypadMaps(MAP_STYLE_1,MINECRAFT_ACTION_CRAFTING, _360_JOY_BUTTON_X); InputManager.SetGameJoypadMaps(MAP_STYLE_1,MINECRAFT_ACTION_RENDER_THIRD_PERSON, _360_JOY_BUTTON_RTHUMB); - InputManager.SetGameJoypadMaps(MAP_STYLE_1,MINECRAFT_ACTION_GAME_INFO, _360_JOY_BUTTON_BACK); + InputManager.SetGameJoypadMaps(MAP_STYLE_1,MINECRAFT_ACTION_GAME_INFO, 0); + InputManager.SetGameJoypadMaps(MAP_STYLE_1,MINECRAFT_ACTION_VOICE_MUTE, _360_JOY_BUTTON_BACK); InputManager.SetGameJoypadMaps(MAP_STYLE_1,MINECRAFT_ACTION_DPAD_LEFT, _360_JOY_BUTTON_DPAD_LEFT); InputManager.SetGameJoypadMaps(MAP_STYLE_1,MINECRAFT_ACTION_DPAD_RIGHT, _360_JOY_BUTTON_DPAD_RIGHT); @@ -445,7 +447,8 @@ void DefineActions(void) InputManager.SetGameJoypadMaps(MAP_STYLE_2,MINECRAFT_ACTION_SNEAK_TOGGLE, _360_JOY_BUTTON_LB); InputManager.SetGameJoypadMaps(MAP_STYLE_2,MINECRAFT_ACTION_CRAFTING, _360_JOY_BUTTON_X); InputManager.SetGameJoypadMaps(MAP_STYLE_2,MINECRAFT_ACTION_RENDER_THIRD_PERSON, _360_JOY_BUTTON_LTHUMB); - InputManager.SetGameJoypadMaps(MAP_STYLE_2,MINECRAFT_ACTION_GAME_INFO, _360_JOY_BUTTON_BACK); + InputManager.SetGameJoypadMaps(MAP_STYLE_2,MINECRAFT_ACTION_GAME_INFO, 0); + InputManager.SetGameJoypadMaps(MAP_STYLE_2,MINECRAFT_ACTION_VOICE_MUTE, _360_JOY_BUTTON_BACK); InputManager.SetGameJoypadMaps(MAP_STYLE_2,ACTION_MENU_PAUSEMENU, _360_JOY_BUTTON_START); InputManager.SetGameJoypadMaps(MAP_STYLE_2,ACTION_MENU_STICK_PRESS, _360_JOY_BUTTON_LTHUMB);