diff --git a/Minecraft.Client/ClientConnection.cpp b/Minecraft.Client/ClientConnection.cpp index 916b0604..b17796ca 100644 --- a/Minecraft.Client/ClientConnection.cpp +++ b/Minecraft.Client/ClientConnection.cpp @@ -1393,6 +1393,12 @@ void ClientConnection::send(shared_ptr packet) connection->send(packet); } +void ClientConnection::queueSend(shared_ptr packet) +{ + if (done) return; + connection->queueSend(packet); +} + void ClientConnection::handleTakeItemEntity(shared_ptr packet) { if (!isPrimaryConnection()) return; @@ -4101,7 +4107,7 @@ void ClientConnection::handleVoiceChat(VoiceChatPacket *packet) // Route audio to VoiceChatManager for 3D playback VoiceChatManager::getInstance().receiveVoiceData( - packet->senderPlayerId, sx, sy, sz, + packet->senderPlayerId, packet->sequence, sx, sy, sz, (const unsigned char *)packet->audioData.data, packet->dataLength); } diff --git a/Minecraft.Client/ClientConnection.h b/Minecraft.Client/ClientConnection.h index 1fa0de48..ed6e00c3 100644 --- a/Minecraft.Client/ClientConnection.h +++ b/Minecraft.Client/ClientConnection.h @@ -80,6 +80,7 @@ public: virtual void onDisconnect(DisconnectPacket::eDisconnectReason reason, void *reasonObjects); void sendAndDisconnect(shared_ptr packet); void send(shared_ptr packet); + void queueSend(shared_ptr packet); virtual void handleTakeItemEntity(shared_ptr packet); virtual void handleChat(shared_ptr packet); virtual void handleAnimate(shared_ptr packet); @@ -165,4 +166,4 @@ private: static const int MAX_ENTITY_LINK_DEFERRAL_INTERVAL = 1000; void checkDeferredEntityLinkPackets(int newEntityId); -}; \ No newline at end of file +}; diff --git a/Minecraft.Client/Common/BuildVer.h b/Minecraft.Client/Common/BuildVer.h index eaa77d26..5c8cb707 100644 --- a/Minecraft.Client/Common/BuildVer.h +++ b/Minecraft.Client/Common/BuildVer.h @@ -1,7 +1,7 @@ #pragma once #define VER_PRODUCTBUILD 560 -#define VER_PRODUCTVERSION_STR_W L"DEV (unknown version)" +#define VER_PRODUCTVERSION_STR_W L"2eef37d-dev" #define VER_FILEVERSION_STR_W VER_PRODUCTVERSION_STR_W -#define VER_BRANCHVERSION_STR_W L"UNKNOWN BRANCH" +#define VER_BRANCHVERSION_STR_W L"haxi0/MinecraftConsoles/main" #define VER_NETWORK VER_PRODUCTBUILD diff --git a/Minecraft.Client/Common/UI/UIScene_SettingsAudioMenu.cpp b/Minecraft.Client/Common/UI/UIScene_SettingsAudioMenu.cpp index 083fbd35..cde546ae 100644 --- a/Minecraft.Client/Common/UI/UIScene_SettingsAudioMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_SettingsAudioMenu.cpp @@ -14,6 +14,35 @@ UIScene_SettingsAudioMenu::UIScene_SettingsAudioMenu(int iPad, void *initData, U swprintf( (WCHAR *)TempString, 256, L"%ls: %d%%", app.GetString( IDS_SLIDER_SOUND ),app.GetGameSettings(m_iPad,eGameSetting_SoundFXVolume)); m_sliderSound.init(TempString,eControl_Sound,0,100,app.GetGameSettings(m_iPad,eGameSetting_SoundFXVolume)); + m_hasVoiceChatButton = false; + IggyValuePath *root = IggyPlayerRootPath(getMovie()); + static const char *kVoiceButtonCandidates[] = + { + "VoiceChat", + "Button3", + "Button2", + "Button1", + "Languages" + }; + + for (int i = 0; i < static_cast(sizeof(kVoiceButtonCandidates) / sizeof(kVoiceButtonCandidates[0])); ++i) + { + if (m_buttonVoiceChat.setupControl(this, root, kVoiceButtonCandidates[i])) + { + m_buttonVoiceChat.m_pParentPanel = nullptr; + m_controls.push_back(&m_buttonVoiceChat); + m_buttonVoiceChat.init(UIString(L"Voice Chat"), eControl_VoiceChat); + m_hasVoiceChatButton = true; + app.DebugPrintf("SettingsAudio: Voice button bound to '%s'\n", kVoiceButtonCandidates[i]); + break; + } + } + + if (!m_hasVoiceChatButton) + { + app.DebugPrintf("SettingsAudio: No button control available in audio scene; fallback to X tooltip/action\n"); + } + doHorizontalResizeCheck(); if(app.GetLocalPlayerCount()>1) @@ -42,7 +71,7 @@ wstring UIScene_SettingsAudioMenu::getMoviePath() void UIScene_SettingsAudioMenu::updateTooltips() { - ui.SetTooltips( m_iPad, IDS_TOOLTIPS_SELECT,IDS_TOOLTIPS_BACK); + ui.SetTooltips( m_iPad, IDS_TOOLTIPS_SELECT, IDS_TOOLTIPS_BACK, m_hasVoiceChatButton ? -1 : IDS_TOOLTIPS_OPTIONS); } void UIScene_SettingsAudioMenu::updateComponents() @@ -87,6 +116,28 @@ void UIScene_SettingsAudioMenu::handleInput(int iPad, int key, bool repeat, bool case ACTION_MENU_RIGHT: sendInputToMovie(key, repeat, pressed, released); break; + case ACTION_MENU_X: + if (pressed && !m_hasVoiceChatButton) + { + static int s_voiceChatMenuInitData = 2; + ui.NavigateToScene(m_iPad, eUIScene_SettingsGraphicsMenu, &s_voiceChatMenuInitData); + handled = true; + } + break; + } +} + +void UIScene_SettingsAudioMenu::handlePress(F64 controlId, F64 childId) +{ + ui.PlayUISFX(eSFX_Press); + switch (static_cast(controlId)) + { + case eControl_VoiceChat: + { + static int s_voiceChatMenuInitData = 2; + ui.NavigateToScene(m_iPad, eUIScene_SettingsGraphicsMenu, &s_voiceChatMenuInitData); + } + break; } } diff --git a/Minecraft.Client/Common/UI/UIScene_SettingsAudioMenu.h b/Minecraft.Client/Common/UI/UIScene_SettingsAudioMenu.h index 6c48b22b..13ce7b9f 100644 --- a/Minecraft.Client/Common/UI/UIScene_SettingsAudioMenu.h +++ b/Minecraft.Client/Common/UI/UIScene_SettingsAudioMenu.h @@ -1,6 +1,7 @@ #pragma once #include "UIScene.h" +#include "UIControl_Button.h" class UIScene_SettingsAudioMenu : public UIScene { @@ -8,10 +9,13 @@ private: enum EControls { eControl_Music, - eControl_Sound + eControl_Sound, + eControl_VoiceChat }; UIControl_Slider m_sliderMusic, m_sliderSound; // Sliders + UIControl_Button m_buttonVoiceChat; + bool m_hasVoiceChatButton; UI_BEGIN_MAP_ELEMENTS_AND_NAMES(UIScene) UI_MAP_ELEMENT( m_sliderMusic, "Music") UI_MAP_ELEMENT( m_sliderSound, "Sound") @@ -33,6 +37,7 @@ protected: public: // INPUT virtual void handleInput(int iPad, int key, bool repeat, bool pressed, bool released, bool &handled); + virtual void handlePress(F64 controlId, F64 childId); virtual void handleSliderMove(F64 sliderId, F64 currentValue); -}; \ No newline at end of file +}; diff --git a/Minecraft.Client/Common/UI/UIScene_SettingsGraphicsMenu.cpp b/Minecraft.Client/Common/UI/UIScene_SettingsGraphicsMenu.cpp index b258d8c3..42db73db 100644 --- a/Minecraft.Client/Common/UI/UIScene_SettingsGraphicsMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_SettingsGraphicsMenu.cpp @@ -4,12 +4,16 @@ #include "..\..\Minecraft.h" #include "..\..\Options.h" #include "..\..\GameRenderer.h" +#include "..\..\VoiceChatManager.h" namespace { + static int s_voiceChatGraphicsInitData = 2; constexpr int FOV_MIN = 70; constexpr int FOV_MAX = 110; constexpr int FOV_SLIDER_MAX = 100; + static const int MAX_VOICE_VOLUME_PERCENT = 200; + static const int MAX_DEVICE_LABELS = 64; int ClampFov(int value) { @@ -31,6 +35,33 @@ namespace if (sliderValue > FOV_SLIDER_MAX) sliderValue = FOV_SLIDER_MAX; return FOV_MIN + ((sliderValue * (FOV_MAX - FOV_MIN)) / FOV_SLIDER_MAX); } + + static void BuildDeviceLabels(const vector &deviceNames, const wchar_t *prefix, wchar_t labels[][256], int &count) + { + count = static_cast(deviceNames.size()); + if (count <= 0) + { + count = 1; + swprintf(labels[0], 256, L"%ls: Default", prefix); + return; + } + if (count > MAX_DEVICE_LABELS) + { + count = MAX_DEVICE_LABELS; + } + + for (int i = 0; i < count; ++i) + { + swprintf(labels[i], 256, L"%ls: %ls", prefix, deviceNames[i].c_str()); + } + } + + static void SetControlY(UIScene &scene, UIControl &control, int y) + { + IggyName yName = scene.registerFastName(L"y"); + IggyValueSetF64RS(control.getIggyValuePath(), yName, nullptr, static_cast(y)); + control.UpdateControl(); + } } int UIScene_SettingsGraphicsMenu::LevelToDistance(int level) @@ -55,6 +86,13 @@ UIScene_SettingsGraphicsMenu::UIScene_SettingsGraphicsMenu(int iPad, void *initD { // Setup all the Iggy references we need for this scene initialiseMovie(); + m_bVoiceChatMode = (initData != nullptr && *(static_cast(initData)) == s_voiceChatGraphicsInitData); + if (m_bVoiceChatMode) + { + initVoiceChatSliders(); + return; + } + Minecraft* pMinecraft = Minecraft::GetInstance(); m_bNotInGame=(Minecraft::GetInstance()->level==nullptr); @@ -114,6 +152,88 @@ UIScene_SettingsGraphicsMenu::UIScene_SettingsGraphicsMenu(int iPad, void *initD } } +void UIScene_SettingsGraphicsMenu::initVoiceChatSliders() +{ + VoiceChatManager &vcm = VoiceChatManager::getInstance(); + if (!vcm.isInitialized()) + { + vcm.init(); + } + + // Reuse graphics checkboxes as voice mode switches. + const bool proximityEnabled = vcm.isProximityEnabled(); + m_checkboxClouds.init(UIString(L"Proximity"), eControl_Clouds, proximityEnabled); + m_checkboxBedrockFog.init(UIString(L"Voice Activate"), eControl_BedrockFog, !proximityEnabled); + removeControl(&m_checkboxCustomSkinAnim, false); + + enforceVoiceModeSwitch(-1); + + WCHAR tempString[256]; + + int micVolume = vcm.getMicVolumePercent(); + if (micVolume < 0) micVolume = 0; + if (micVolume > MAX_VOICE_VOLUME_PERCENT) micVolume = MAX_VOICE_VOLUME_PERCENT; + swprintf(tempString, 256, L"Mic Volume: %d%%", micVolume); + m_sliderRenderDistance.init(tempString, eControl_RenderDistance, 0, MAX_VOICE_VOLUME_PERCENT, micVolume); + + int voiceVolume = vcm.getVoiceChatVolumePercent(); + if (voiceVolume < 0) voiceVolume = 0; + if (voiceVolume > MAX_VOICE_VOLUME_PERCENT) voiceVolume = MAX_VOICE_VOLUME_PERCENT; + swprintf(tempString, 256, L"Voice Chat Volume: %d%%", voiceVolume); + m_sliderGamma.init(tempString, eControl_Gamma, 0, MAX_VOICE_VOLUME_PERCENT, voiceVolume); + + vector captureNames; + vector playbackNames; + vcm.getCaptureDeviceNames(captureNames, true); + vcm.getPlaybackDeviceNames(playbackNames, true); + + wchar_t captureLabels[MAX_DEVICE_LABELS][256]; + wchar_t playbackLabels[MAX_DEVICE_LABELS][256]; + int captureCount = 0; + int playbackCount = 0; + BuildDeviceLabels(captureNames, L"Input", captureLabels, captureCount); + BuildDeviceLabels(playbackNames, L"Output", playbackLabels, playbackCount); + + int captureIndex = vcm.getSelectedCaptureMenuIndex(); + int playbackIndex = vcm.getSelectedPlaybackMenuIndex(); + if (captureIndex < 0) captureIndex = 0; + if (captureIndex >= captureCount) captureIndex = 0; + if (playbackIndex < 0) playbackIndex = 0; + if (playbackIndex >= playbackCount) playbackIndex = 0; + + m_sliderFOV.setAllPossibleLabels(captureCount, captureLabels); + m_sliderFOV.init(captureLabels[captureIndex], eControl_FOV, 0, captureCount - 1, captureIndex); + + m_sliderInterfaceOpacity.setAllPossibleLabels(playbackCount, playbackLabels); + m_sliderInterfaceOpacity.init(playbackLabels[playbackIndex], eControl_InterfaceOpacity, 0, playbackCount - 1, playbackIndex); + + // Force a stable stacked layout in voice mode to avoid slider overlap across skins. + m_checkboxClouds.UpdateControl(); + m_checkboxBedrockFog.UpdateControl(); + m_sliderRenderDistance.UpdateControl(); + m_sliderGamma.UpdateControl(); + m_sliderFOV.UpdateControl(); + m_sliderInterfaceOpacity.UpdateControl(); + + const int rowSpacing = 8; + const int checkBottom = m_checkboxBedrockFog.getYPos() + m_checkboxBedrockFog.getHeight(); + int sliderY = m_sliderRenderDistance.getYPos(); + const int minSliderY = checkBottom + rowSpacing; + if (sliderY < minSliderY) + { + sliderY = minSliderY; + } + SetControlY(*this, m_sliderRenderDistance, sliderY); + sliderY += m_sliderRenderDistance.getHeight() + rowSpacing; + SetControlY(*this, m_sliderGamma, sliderY); + sliderY += m_sliderGamma.getHeight() + rowSpacing; + SetControlY(*this, m_sliderFOV, sliderY); + sliderY += m_sliderFOV.getHeight() + rowSpacing; + SetControlY(*this, m_sliderInterfaceOpacity, sliderY); + + doHorizontalResizeCheck(); +} + UIScene_SettingsGraphicsMenu::~UIScene_SettingsGraphicsMenu() { } @@ -161,6 +281,14 @@ void UIScene_SettingsGraphicsMenu::handleInput(int iPad, int key, bool repeat, b case ACTION_MENU_CANCEL: if(pressed) { + if (m_bVoiceChatMode) + { + applyVoiceModeFromCheckboxes(); + navigateBack(); + handled = true; + break; + } + // check the checkboxes app.SetGameSettings(m_iPad,eGameSetting_Clouds,m_checkboxClouds.IsChecked()?1:0); app.SetGameSettings(m_iPad,eGameSetting_BedrockFog,m_checkboxBedrockFog.IsChecked()?1:0); @@ -189,6 +317,12 @@ void UIScene_SettingsGraphicsMenu::handleSliderMove(F64 sliderId, F64 currentVal { WCHAR TempString[256]; const int value = static_cast(currentValue); + if (m_bVoiceChatMode) + { + handleVoiceSliderMove(static_cast(sliderId), value); + return; + } + switch(static_cast(sliderId)) { case eControl_RenderDistance: @@ -238,3 +372,117 @@ void UIScene_SettingsGraphicsMenu::handleSliderMove(F64 sliderId, F64 currentVal break; } } + +void UIScene_SettingsGraphicsMenu::handleCheckboxToggled(F64 controlId, bool selected) +{ + if (!m_bVoiceChatMode) + { + return; + } + + switch (static_cast(controlId)) + { + case eControl_Clouds: + m_checkboxClouds.setChecked(selected); + m_checkboxBedrockFog.setChecked(!selected); + enforceVoiceModeSwitch(eControl_Clouds); + applyVoiceModeFromCheckboxes(); + break; + + case eControl_BedrockFog: + m_checkboxBedrockFog.setChecked(selected); + m_checkboxClouds.setChecked(!selected); + enforceVoiceModeSwitch(eControl_BedrockFog); + applyVoiceModeFromCheckboxes(); + break; + } +} + +void UIScene_SettingsGraphicsMenu::handleVoiceSliderMove(int controlId, int value) +{ + VoiceChatManager &vcm = VoiceChatManager::getInstance(); + WCHAR tempString[256]; + switch (controlId) + { + case eControl_RenderDistance: + vcm.setMicVolumePercent(value); + m_sliderRenderDistance.handleSliderMove(value); + swprintf(tempString, 256, L"Mic Volume: %d%%", value); + m_sliderRenderDistance.setLabel(tempString); + break; + + case eControl_Gamma: + vcm.setVoiceChatVolumePercent(value); + m_sliderGamma.handleSliderMove(value); + swprintf(tempString, 256, L"Voice Chat Volume: %d%%", value); + m_sliderGamma.setLabel(tempString); + break; + + case eControl_FOV: + if (vcm.selectCaptureMenuIndex(value)) + { + vector names; + vcm.getCaptureDeviceNames(names, true); + if (value >= 0 && value < static_cast(names.size())) + { + swprintf(tempString, 256, L"Input: %ls", names[value].c_str()); + m_sliderFOV.setLabel(tempString); + } + } + m_sliderFOV.handleSliderMove(value); + break; + + case eControl_InterfaceOpacity: + if (vcm.selectPlaybackMenuIndex(value)) + { + vector names; + vcm.getPlaybackDeviceNames(names, true); + if (value >= 0 && value < static_cast(names.size())) + { + swprintf(tempString, 256, L"Output: %ls", names[value].c_str()); + m_sliderInterfaceOpacity.setLabel(tempString); + } + } + m_sliderInterfaceOpacity.handleSliderMove(value); + break; + } +} + +void UIScene_SettingsGraphicsMenu::enforceVoiceModeSwitch(int preferredControl) +{ + if (!m_bVoiceChatMode) + { + return; + } + + bool proximity = m_checkboxClouds.IsChecked(); + bool voiceActivate = m_checkboxBedrockFog.IsChecked(); + + if (proximity == voiceActivate) + { + if (preferredControl == eControl_BedrockFog) + { + proximity = false; + voiceActivate = true; + } + else + { + proximity = true; + voiceActivate = false; + } + } + + m_checkboxClouds.setChecked(proximity); + m_checkboxBedrockFog.setChecked(voiceActivate); +} + +void UIScene_SettingsGraphicsMenu::applyVoiceModeFromCheckboxes() +{ + VoiceChatManager &vcm = VoiceChatManager::getInstance(); + enforceVoiceModeSwitch(-1); + const bool proximityEnabled = m_checkboxClouds.IsChecked(); + vcm.setProximityEnabled(proximityEnabled); + vcm.setVoiceInputMode(!proximityEnabled + ? VoiceChatManager::VOICE_INPUT_VOICE_ACTIVATION + : VoiceChatManager::VOICE_INPUT_PUSH_TO_TALK); +} diff --git a/Minecraft.Client/Common/UI/UIScene_SettingsGraphicsMenu.h b/Minecraft.Client/Common/UI/UIScene_SettingsGraphicsMenu.h index 99022c83..ecd95c6e 100644 --- a/Minecraft.Client/Common/UI/UIScene_SettingsGraphicsMenu.h +++ b/Minecraft.Client/Common/UI/UIScene_SettingsGraphicsMenu.h @@ -7,6 +7,8 @@ class UIScene_SettingsGraphicsMenu : public UIScene { private: + bool m_bVoiceChatMode; + enum EControls { eControl_Clouds, @@ -31,6 +33,10 @@ private: UI_END_MAP_ELEMENTS_AND_NAMES() bool m_bNotInGame; + void initVoiceChatSliders(); + void handleVoiceSliderMove(int controlId, int value); + void enforceVoiceModeSwitch(int preferredControl); + void applyVoiceModeFromCheckboxes(); public: UIScene_SettingsGraphicsMenu(int iPad, void *initData, UILayer *parentLayer); virtual ~UIScene_SettingsGraphicsMenu(); @@ -47,10 +53,11 @@ protected: public: // INPUT virtual void handleInput(int iPad, int key, bool repeat, bool pressed, bool released, bool &handled); + virtual void handleCheckboxToggled(F64 controlId, bool selected); virtual void handleSliderMove(F64 sliderId, F64 currentValue); static int LevelToDistance(int dist); static int DistanceToLevel(int dist); -}; \ No newline at end of file +}; diff --git a/Minecraft.Client/Common/UI/UIScene_SettingsMenu.cpp b/Minecraft.Client/Common/UI/UIScene_SettingsMenu.cpp index 2ae9c897..94e9f74b 100644 --- a/Minecraft.Client/Common/UI/UIScene_SettingsMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_SettingsMenu.cpp @@ -12,10 +12,10 @@ UIScene_SettingsMenu::UIScene_SettingsMenu(int iPad, void *initData, UILayer *pa m_buttons[BUTTON_ALL_OPTIONS].init(IDS_OPTIONS,BUTTON_ALL_OPTIONS); m_buttons[BUTTON_ALL_AUDIO].init(IDS_AUDIO,BUTTON_ALL_AUDIO); + m_buttons[BUTTON_ALL_VOICE_CHAT].init(UIString(L"Voice Chat"),BUTTON_ALL_VOICE_CHAT); m_buttons[BUTTON_ALL_CONTROL].init(IDS_CONTROL,BUTTON_ALL_CONTROL); m_buttons[BUTTON_ALL_GRAPHICS].init(IDS_GRAPHICS,BUTTON_ALL_GRAPHICS); m_buttons[BUTTON_ALL_UI].init(IDS_USER_INTERFACE,BUTTON_ALL_UI); - m_buttons[BUTTON_ALL_RESETTODEFAULTS].init(IDS_RESET_TO_DEFAULTS,BUTTON_ALL_RESETTODEFAULTS); if(ProfileManager.GetPrimaryPad()!=m_iPad) { @@ -63,7 +63,7 @@ void UIScene_SettingsMenu::handleReload() void UIScene_SettingsMenu::updateTooltips() { - ui.SetTooltips( m_iPad, IDS_TOOLTIPS_SELECT,IDS_TOOLTIPS_BACK); + ui.SetTooltips( m_iPad, IDS_TOOLTIPS_SELECT, IDS_TOOLTIPS_BACK, -1, IDS_RESET_TO_DEFAULTS); } void UIScene_SettingsMenu::updateComponents() @@ -107,6 +107,15 @@ void UIScene_SettingsMenu::handleInput(int iPad, int key, bool repeat, bool pres #endif sendInputToMovie(key, repeat, pressed, released); break; + case ACTION_MENU_Y: + if (pressed) + { + UINT uiIDA[2]; + uiIDA[0] = IDS_CONFIRM_CANCEL; + uiIDA[1] = IDS_CONFIRM_OK; + ui.RequestAlertMessage(IDS_DEFAULTS_TITLE, IDS_DEFAULTS_TEXT, uiIDA, 2, m_iPad, &UIScene_SettingsMenu::ResetDefaultsDialogReturned, this); + } + break; case ACTION_MENU_UP: case ACTION_MENU_DOWN: sendInputToMovie(key, repeat, pressed, released); @@ -127,6 +136,12 @@ void UIScene_SettingsMenu::handlePress(F64 controlId, F64 childId) case BUTTON_ALL_AUDIO: ui.NavigateToScene(m_iPad, eUIScene_SettingsAudioMenu); break; + case BUTTON_ALL_VOICE_CHAT: + { + static int s_voiceChatMenuInitData = 2; + ui.NavigateToScene(m_iPad, eUIScene_SettingsGraphicsMenu, &s_voiceChatMenuInitData); + } + break; case BUTTON_ALL_CONTROL: ui.NavigateToScene(m_iPad, eUIScene_SettingsControlMenu); break; @@ -136,16 +151,6 @@ void UIScene_SettingsMenu::handlePress(F64 controlId, F64 childId) case BUTTON_ALL_UI: ui.NavigateToScene(m_iPad, eUIScene_SettingsUIMenu); break; - case BUTTON_ALL_RESETTODEFAULTS: - { - // check they really want to do this - UINT uiIDA[2]; - uiIDA[0]=IDS_CONFIRM_CANCEL; - uiIDA[1]=IDS_CONFIRM_OK; - - ui.RequestAlertMessage(IDS_DEFAULTS_TITLE, IDS_DEFAULTS_TEXT, uiIDA, 2, m_iPad,&UIScene_SettingsMenu::ResetDefaultsDialogReturned,this); - } - break; } } diff --git a/Minecraft.Client/Common/UI/UIScene_SettingsMenu.h b/Minecraft.Client/Common/UI/UIScene_SettingsMenu.h index 7f5fe169..66432d84 100644 --- a/Minecraft.Client/Common/UI/UIScene_SettingsMenu.h +++ b/Minecraft.Client/Common/UI/UIScene_SettingsMenu.h @@ -4,11 +4,11 @@ #define BUTTON_ALL_OPTIONS 0 #define BUTTON_ALL_AUDIO 1 -#define BUTTON_ALL_CONTROL 2 -#define BUTTON_ALL_GRAPHICS 4 -#define BUTTON_ALL_UI 5 -#define BUTTON_ALL_RESETTODEFAULTS 6 -#define BUTTONS_ALL_MAX BUTTON_ALL_RESETTODEFAULTS + 1 +#define BUTTON_ALL_VOICE_CHAT 2 +#define BUTTON_ALL_CONTROL 4 +#define BUTTON_ALL_GRAPHICS 5 +#define BUTTON_ALL_UI 6 +#define BUTTONS_ALL_MAX BUTTON_ALL_UI + 1 class UIScene_SettingsMenu : public UIScene { @@ -17,10 +17,10 @@ private: UI_BEGIN_MAP_ELEMENTS_AND_NAMES(UIScene) UI_MAP_ELEMENT( m_buttons[BUTTON_ALL_OPTIONS], "Button1") UI_MAP_ELEMENT( m_buttons[BUTTON_ALL_AUDIO], "Button2") - UI_MAP_ELEMENT( m_buttons[BUTTON_ALL_CONTROL], "Button3") - UI_MAP_ELEMENT( m_buttons[BUTTON_ALL_GRAPHICS], "Button4") - UI_MAP_ELEMENT( m_buttons[BUTTON_ALL_UI], "Button5") - UI_MAP_ELEMENT( m_buttons[BUTTON_ALL_RESETTODEFAULTS], "Button6") + UI_MAP_ELEMENT( m_buttons[BUTTON_ALL_VOICE_CHAT], "Button3") + UI_MAP_ELEMENT( m_buttons[BUTTON_ALL_CONTROL], "Button4") + UI_MAP_ELEMENT( m_buttons[BUTTON_ALL_GRAPHICS], "Button5") + UI_MAP_ELEMENT( m_buttons[BUTTON_ALL_UI], "Button6") UI_END_MAP_ELEMENTS_AND_NAMES() public: UIScene_SettingsMenu(int iPad, void *initData, UILayer *parentLayer); @@ -44,4 +44,4 @@ protected: void handlePress(F64 controlId, F64 childId); static int ResetDefaultsDialogReturned(void *pParam,int iPad,C4JStorage::EMessageResult result); -}; \ No newline at end of file +}; diff --git a/Minecraft.Client/Common/UI/UIScene_SettingsOptionsMenu.cpp b/Minecraft.Client/Common/UI/UIScene_SettingsOptionsMenu.cpp index 7b4d3d9d..1904685f 100644 --- a/Minecraft.Client/Common/UI/UIScene_SettingsOptionsMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_SettingsOptionsMenu.cpp @@ -1,6 +1,7 @@ #include "stdafx.h" #include "UI.h" #include "UIScene_SettingsOptionsMenu.h" +#include "..\..\VoiceChatManager.h" #if defined(_XBOX_ONE) #define _ENABLE_LANGUAGE_SELECT @@ -22,13 +23,99 @@ int UIScene_SettingsOptionsMenu::m_iDifficultyTitleSettingA[4]= IDS_DIFFICULTY_TITLE_HARD }; +namespace +{ + static int s_voiceChatMenuInitData = 1; + static const int MAX_VOICE_VOLUME_PERCENT = 200; + + static void SetControlY(UIScene &scene, UIControl &control, int y) + { + IggyName yName = scene.registerFastName(L"y"); + IggyValueSetF64RS(control.getIggyValuePath(), yName, nullptr, static_cast(y)); + control.UpdateControl(); + } +} + UIScene_SettingsOptionsMenu::UIScene_SettingsOptionsMenu(int iPad, void *initData, UILayer *parentLayer) : UIScene(iPad, parentLayer) { - m_bNavigateToLanguageSelector = false; + m_bVoiceChatMode = (initData != nullptr && *(static_cast(initData)) == s_voiceChatMenuInitData); // Setup all the Iggy references we need for this scene initialiseMovie(); - + + if (m_bVoiceChatMode) + { + setupVoiceChatMenu(); + } + else + { + setupStandardOptionsMenu(); + } +} + +UIScene_SettingsOptionsMenu::~UIScene_SettingsOptionsMenu() +{ +} + +void UIScene_SettingsOptionsMenu::setupVoiceChatMenu() +{ + VoiceChatManager &vcm = VoiceChatManager::getInstance(); + if (!vcm.isInitialized()) + { + vcm.init(); + } + + m_bNotInGame = (Minecraft::GetInstance()->level == nullptr); + m_bMashUpWorldsUnhideOption = false; + + const bool proximityEnabled = vcm.isProximityEnabled(); + m_checkboxViewBob.init(UIString(L"Proximity"), eControl_ViewBob, proximityEnabled); + m_checkboxShowHints.init(UIString(L"Voice Activate"), eControl_ShowHints, !proximityEnabled); + + removeControl(&m_checkboxShowTooltips, true); + removeControl(&m_checkboxInGameGamertags, true); + removeControl(&m_checkboxMashupWorlds, true); + removeControl(&m_labelDifficultyText, true); + + static wchar_t micVolumeLabels[MAX_VOICE_VOLUME_PERCENT + 1][256]; + static wchar_t voiceVolumeLabels[MAX_VOICE_VOLUME_PERCENT + 1][256]; + for (int i = 0; i <= MAX_VOICE_VOLUME_PERCENT; ++i) + { + swprintf(micVolumeLabels[i], 256, L"Mic Volume: %d%%", i); + swprintf(voiceVolumeLabels[i], 256, L"Voice Chat Volume: %d%%", i); + } + + int micVolume = vcm.getMicVolumePercent(); + int voiceVolume = vcm.getVoiceChatVolumePercent(); + if (micVolume < 0) micVolume = 0; + if (micVolume > MAX_VOICE_VOLUME_PERCENT) micVolume = MAX_VOICE_VOLUME_PERCENT; + if (voiceVolume < 0) voiceVolume = 0; + if (voiceVolume > MAX_VOICE_VOLUME_PERCENT) voiceVolume = MAX_VOICE_VOLUME_PERCENT; + + m_sliderAutosave.setAllPossibleLabels(MAX_VOICE_VOLUME_PERCENT + 1, micVolumeLabels); + m_sliderAutosave.init(micVolumeLabels[micVolume], eControl_Autosave, 0, MAX_VOICE_VOLUME_PERCENT, micVolume); + m_sliderDifficulty.setAllPossibleLabels(MAX_VOICE_VOLUME_PERCENT + 1, voiceVolumeLabels); + m_sliderDifficulty.init(voiceVolumeLabels[voiceVolume], eControl_Difficulty, 0, MAX_VOICE_VOLUME_PERCENT, voiceVolume); + + m_buttonLanguageSelect.init(UIString(L"Back"), eControl_Languages); + + m_labelDifficultyText.setVisible(false); + placeVoiceBackButtonAtBottom(); + + doHorizontalResizeCheck(); + + if (app.GetLocalPlayerCount() > 1) + { +#if TO_BE_IMPLEMENTED + app.AdjustSplitscreenScene(m_hObj,&m_OriginalPosition,m_iPad); +#endif + } + + m_labelDifficultyText.disableReinitialisation(); +} + +void UIScene_SettingsOptionsMenu::setupStandardOptionsMenu() +{ m_bNotInGame=(Minecraft::GetInstance()->level==nullptr); m_checkboxViewBob.init(IDS_VIEW_BOBBING,eControl_ViewBob,(app.GetGameSettings(m_iPad,eGameSetting_ViewBob)!=0)); @@ -45,7 +132,6 @@ UIScene_SettingsOptionsMenu::UIScene_SettingsOptionsMenu(int iPad, void *initDat } else { - //m_checkboxMashupWorlds.init(L"",eControl_ShowMashUpWorlds,false); removeControl(&m_checkboxMashupWorlds, true); m_bMashUpWorldsUnhideOption=false; } @@ -57,13 +143,12 @@ UIScene_SettingsOptionsMenu::UIScene_SettingsOptionsMenu(int iPad, void *initDat { if(i==0) { - swprintf( autosaveLabels[i], 256, L"%ls", app.GetString( IDS_SLIDER_AUTOSAVE_OFF )); + swprintf( autosaveLabels[i], 256, L"%ls", app.GetString( IDS_SLIDER_AUTOSAVE_OFF )); } else { - swprintf( autosaveLabels[i], 256, L"%ls: %d %ls", app.GetString( IDS_SLIDER_AUTOSAVE ),i*15, app.GetString( IDS_MINUTES )); + swprintf( autosaveLabels[i], 256, L"%ls: %d %ls", app.GetString( IDS_SLIDER_AUTOSAVE ),i*15, app.GetString( IDS_MINUTES )); } - } m_sliderAutosave.setAllPossibleLabels(9,autosaveLabels); m_sliderAutosave.init(autosaveLabels[ucValue],eControl_Autosave,0,8,ucValue); @@ -76,21 +161,15 @@ UIScene_SettingsOptionsMenu::UIScene_SettingsOptionsMenu(int iPad, void *initDat wchar_t difficultyLabels[4][256]; for(unsigned int i = 0; i < 4; ++i) { - swprintf( difficultyLabels[i], 256, L"%ls: %ls", app.GetString( IDS_SLIDER_DIFFICULTY ),app.GetString(m_iDifficultyTitleSettingA[i])); + swprintf( difficultyLabels[i], 256, L"%ls: %ls", app.GetString( IDS_SLIDER_DIFFICULTY ),app.GetString(m_iDifficultyTitleSettingA[i])); } m_sliderDifficulty.setAllPossibleLabels(4,difficultyLabels); m_sliderDifficulty.init(difficultyLabels[ucValue],eControl_Difficulty,0,3,ucValue); - wstring wsText=app.GetString(m_iDifficultySettingA[app.GetGameSettings(m_iPad,eGameSetting_Difficulty)]); - EHTMLFontSize size = eHTMLSize_Normal; - if(!RenderManager.IsHiDef() && !RenderManager.IsWidescreen()) - { - size = eHTMLSize_Splitscreen; - } + wstring wsText=app.GetString(m_iDifficultySettingA[app.GetGameSettings(m_iPad,eGameSetting_Difficulty)]); wchar_t startTags[64]; swprintf(startTags,64,L"",app.GetHTMLColour(eHTMLColor_White)); - wsText= startTags + wsText; - + wsText= startTags + wsText; m_labelDifficultyText.init(wsText); // If you are in-game, only the game host can change in-game gamertags, and you can't change difficulty @@ -98,7 +177,7 @@ UIScene_SettingsOptionsMenu::UIScene_SettingsOptionsMenu(int iPad, void *initDat bool bRemoveDifficulty=false; bool bRemoveAutosave=false; bool bRemoveInGameGamertags=false; - + bool bNotInGame=(Minecraft::GetInstance()->level==nullptr); bool bPrimaryPlayer = ProfileManager.GetPrimaryPad()==m_iPad; if(!bPrimaryPlayer) @@ -108,13 +187,13 @@ UIScene_SettingsOptionsMenu::UIScene_SettingsOptionsMenu(int iPad, void *initDat bRemoveInGameGamertags=true; } - if(!bNotInGame) // in the game - { + if(!bNotInGame) + { bRemoveDifficulty=true; if(!g_NetworkManager.IsHost()) { bRemoveAutosave=true; - bRemoveInGameGamertags=true; + bRemoveInGameGamertags=true; } } if(bRemoveDifficulty) @@ -137,11 +216,11 @@ UIScene_SettingsOptionsMenu::UIScene_SettingsOptionsMenu(int iPad, void *initDat // MGH - disabled the language select for the patch build, we'll re-enable afterwards // 4J Stu - Removed it with a preprocessor def as we turn this off in various places #ifdef _ENABLE_LANGUAGE_SELECT - if (app.GetGameStarted()) + if (app.GetGameStarted()) { removeControl( &m_buttonLanguageSelect, false ); } - else + else { m_buttonLanguageSelect.init(IDS_LANGUAGE_SELECTOR, eControl_Languages); } @@ -161,20 +240,96 @@ UIScene_SettingsOptionsMenu::UIScene_SettingsOptionsMenu(int iPad, void *initDat m_labelDifficultyText.disableReinitialisation(); } -UIScene_SettingsOptionsMenu::~UIScene_SettingsOptionsMenu() +void UIScene_SettingsOptionsMenu::setVoiceDifficultyLabel() { + m_labelDifficultyText.setVisible(false); +} + +void UIScene_SettingsOptionsMenu::enforceVoiceModeSwitch(int preferredControl) +{ + if (!m_bVoiceChatMode) + { + return; + } + + bool proximity = m_checkboxViewBob.IsChecked(); + bool voiceActivate = m_checkboxShowHints.IsChecked(); + + if (proximity == voiceActivate) + { + if (preferredControl == eControl_ShowHints) + { + proximity = false; + voiceActivate = true; + } + else + { + proximity = true; + voiceActivate = false; + } + } + + m_checkboxViewBob.setChecked(proximity); + m_checkboxShowHints.setChecked(voiceActivate); +} + +void UIScene_SettingsOptionsMenu::placeVoiceBackButtonAtBottom() +{ + if (!m_bVoiceChatMode) + { + return; + } + + m_sliderAutosave.UpdateControl(); + m_sliderDifficulty.UpdateControl(); + m_buttonLanguageSelect.UpdateControl(); + m_checkboxViewBob.UpdateControl(); + m_checkboxShowHints.UpdateControl(); + + const int rowSpacing = 8; + const int bottomPadding = 6; + const int controlsTop = m_checkboxViewBob.getYPos() + m_checkboxViewBob.getHeight(); + const int hintsBottom = m_checkboxShowHints.getYPos() + m_checkboxShowHints.getHeight(); + const int minAutosaveY = ((controlsTop > hintsBottom) ? controlsTop : hintsBottom) + rowSpacing; + int autosaveY = m_sliderAutosave.getYPos(); + if (autosaveY < minAutosaveY) + { + autosaveY = minAutosaveY; + } + + int difficultyY = autosaveY + m_sliderAutosave.getHeight() + rowSpacing; + int backY = difficultyY + m_sliderDifficulty.getHeight() + rowSpacing; + + UIControl *pPanel = m_buttonLanguageSelect.getParentPanel(); + if (pPanel != nullptr) + { + pPanel->UpdateControl(); + const int maxBackY = pPanel->getHeight() - m_buttonLanguageSelect.getHeight() - bottomPadding; + if (backY > maxBackY) + { + const int overflow = backY - maxBackY; + autosaveY -= overflow; + if (autosaveY < minAutosaveY) + { + autosaveY = minAutosaveY; + } + difficultyY = autosaveY + m_sliderAutosave.getHeight() + rowSpacing; + backY = difficultyY + m_sliderDifficulty.getHeight() + rowSpacing; + if (backY > maxBackY) + { + backY = maxBackY; + } + } + } + + SetControlY(*this, m_sliderAutosave, autosaveY); + SetControlY(*this, m_sliderDifficulty, difficultyY); + SetControlY(*this, m_buttonLanguageSelect, backY); } void UIScene_SettingsOptionsMenu::tick() { UIScene::tick(); - - if (m_bNavigateToLanguageSelector) - { - m_bNavigateToLanguageSelector = false; - setGameSettings(); - ui.NavigateToScene(m_iPad, eUIScene_LanguageSelector); - } } wstring UIScene_SettingsOptionsMenu::getMoviePath() @@ -245,140 +400,103 @@ void UIScene_SettingsOptionsMenu::handlePress(F64 controlId, F64 childId) switch(static_cast(controlId)) { + case eControl_ViewBob: + if (m_bVoiceChatMode) + { + m_checkboxViewBob.setChecked(true); + m_checkboxShowHints.setChecked(false); + enforceVoiceModeSwitch(eControl_ViewBob); + setGameSettings(); + break; + } + break; + case eControl_ShowHints: + if (m_bVoiceChatMode) + { + m_checkboxViewBob.setChecked(false); + m_checkboxShowHints.setChecked(true); + enforceVoiceModeSwitch(eControl_ShowHints); + setGameSettings(); + break; + } + break; case eControl_Languages: - m_bNavigateToLanguageSelector = true; + if (m_bVoiceChatMode) + { + setGameSettings(); + navigateBack(); + } + else + { + setGameSettings(); + ui.NavigateToScene(m_iPad, eUIScene_LanguageSelector); + } + break; + } +} + +void UIScene_SettingsOptionsMenu::handleCheckboxToggled(F64 controlId, bool selected) +{ + if (!m_bVoiceChatMode) + { + return; + } + + switch (static_cast(controlId)) + { + case eControl_ViewBob: + m_checkboxViewBob.setChecked(selected); + m_checkboxShowHints.setChecked(!selected); + enforceVoiceModeSwitch(eControl_ViewBob); + setGameSettings(); + break; + case eControl_ShowHints: + m_checkboxShowHints.setChecked(selected); + m_checkboxViewBob.setChecked(!selected); + enforceVoiceModeSwitch(eControl_ShowHints); + setGameSettings(); break; } } void UIScene_SettingsOptionsMenu::handleReload() { - m_bNavigateToLanguageSelector = false; - - m_checkboxViewBob.init(IDS_VIEW_BOBBING,eControl_ViewBob,(app.GetGameSettings(m_iPad,eGameSetting_ViewBob)!=0)); - m_checkboxShowHints.init(IDS_HINTS,eControl_ShowHints,(app.GetGameSettings(m_iPad,eGameSetting_Hints)!=0)); - m_checkboxShowTooltips.init(IDS_IN_GAME_TOOLTIPS,eControl_ShowTooltips,(app.GetGameSettings(m_iPad,eGameSetting_Tooltips)!=0)); - m_checkboxInGameGamertags.init(IDS_IN_GAME_GAMERTAGS,eControl_InGameGamertags,(app.GetGameSettings(m_iPad,eGameSetting_GamertagsVisible)!=0)); - - // check if we should display the mash-up option - if(m_bNotInGame && app.GetMashupPackWorlds(m_iPad)!=0xFFFFFFFF) + if (m_bVoiceChatMode) { - // the mash-up option is needed - m_bMashUpWorldsUnhideOption=true; + setupVoiceChatMenu(); } else { - //m_checkboxMashupWorlds.init(L"",eControl_ShowMashUpWorlds,false); - removeControl(&m_checkboxMashupWorlds, true); - m_bMashUpWorldsUnhideOption=false; + setupStandardOptionsMenu(); } - - unsigned char ucValue=app.GetGameSettings(m_iPad,eGameSetting_Autosave); - - wchar_t autosaveLabels[9][256]; - for(unsigned int i = 0; i < 9; ++i) - { - if(i==0) - { - swprintf( autosaveLabels[i], 256, L"%ls", app.GetString( IDS_SLIDER_AUTOSAVE_OFF )); - } - else - { - swprintf( autosaveLabels[i], 256, L"%ls: %d %ls", app.GetString( IDS_SLIDER_AUTOSAVE ),i*15, app.GetString( IDS_MINUTES )); - } - - } - m_sliderAutosave.setAllPossibleLabels(9,autosaveLabels); - m_sliderAutosave.init(autosaveLabels[ucValue],eControl_Autosave,0,8,ucValue); - -#if defined(_XBOX_ONE) || defined(__ORBIS__) - removeControl(&m_sliderAutosave,true); -#endif - - ucValue = app.GetGameSettings(m_iPad,eGameSetting_Difficulty); - - wchar_t difficultyLabels[4][256]; - for(unsigned int i = 0; i < 4; ++i) - { - swprintf( difficultyLabels[i], 256, L"%ls: %ls", app.GetString( IDS_SLIDER_DIFFICULTY ),app.GetString(m_iDifficultyTitleSettingA[i])); - } - m_sliderDifficulty.setAllPossibleLabels(4,difficultyLabels); - m_sliderDifficulty.init(difficultyLabels[ucValue],eControl_Difficulty,0,3,ucValue); - - wstring wsText=app.GetString(m_iDifficultySettingA[app.GetGameSettings(m_iPad,eGameSetting_Difficulty)]); - EHTMLFontSize size = eHTMLSize_Normal; - if(!RenderManager.IsHiDef() && !RenderManager.IsWidescreen()) - { - size = eHTMLSize_Splitscreen; - } - wchar_t startTags[64]; - swprintf(startTags,64,L"",app.GetHTMLColour(eHTMLColor_White)); - wsText= startTags + wsText; - - m_labelDifficultyText.init(wsText); - - - // If you are in-game, only the game host can change in-game gamertags, and you can't change difficulty - // only the primary player gets to change the autosave and difficulty settings - bool bRemoveDifficulty=false; - bool bRemoveAutosave=false; - bool bRemoveInGameGamertags=false; - - bool bNotInGame=(Minecraft::GetInstance()->level==nullptr); - bool bPrimaryPlayer = ProfileManager.GetPrimaryPad()==m_iPad; - if(!bPrimaryPlayer) - { - bRemoveDifficulty=true; - bRemoveAutosave=true; - bRemoveInGameGamertags=true; - } - - if(!bNotInGame) // in the game - { - bRemoveDifficulty=true; - if(!g_NetworkManager.IsHost()) - { - bRemoveAutosave=true; - bRemoveInGameGamertags=true; - } - } - if(bRemoveDifficulty) - { - m_labelDifficultyText.setVisible( false ); - removeControl(&m_sliderDifficulty, true); - } - - if(bRemoveAutosave) - { - removeControl(&m_sliderAutosave, true); - } - - if(bRemoveInGameGamertags) - { - removeControl(&m_checkboxInGameGamertags, true); - } - - // MGH - disabled the language select for the patch build, we'll re-enable afterwards - // 4J Stu - Removed it with a preprocessor def as we turn this off in various places -#ifdef _ENABLE_LANGUAGE_SELECT - // 4J-JEV: Changing languages in-game will produce many a bug. - if (app.GetGameStarted()) - { - removeControl( &m_buttonLanguageSelect, false ); - } - else - { - } -#else - removeControl( &m_buttonLanguageSelect, false ); -#endif - - doHorizontalResizeCheck(); } void UIScene_SettingsOptionsMenu::handleSliderMove(F64 sliderId, F64 currentValue) { int value = static_cast(currentValue); + + if (m_bVoiceChatMode) + { + VoiceChatManager &vcm = VoiceChatManager::getInstance(); + WCHAR TempString[256]; + switch(static_cast(sliderId)) + { + case eControl_Autosave: + vcm.setMicVolumePercent(value); + swprintf(TempString, 256, L"Mic Volume: %d%%", value); + m_sliderAutosave.setLabel(TempString); + m_sliderAutosave.handleSliderMove(value); + break; + case eControl_Difficulty: + vcm.setVoiceChatVolumePercent(value); + swprintf(TempString, 256, L"Voice Chat Volume: %d%%", value); + m_sliderDifficulty.setLabel(TempString); + m_sliderDifficulty.handleSliderMove(value); + break; + } + return; + } + switch(static_cast(sliderId)) { case eControl_Autosave: @@ -410,6 +528,18 @@ void UIScene_SettingsOptionsMenu::handleSliderMove(F64 sliderId, F64 currentValu void UIScene_SettingsOptionsMenu::setGameSettings() { + if (m_bVoiceChatMode) + { + enforceVoiceModeSwitch(-1); + VoiceChatManager &vcm = VoiceChatManager::getInstance(); + const bool proximityEnabled = m_checkboxViewBob.IsChecked(); + vcm.setProximityEnabled(proximityEnabled); + vcm.setVoiceInputMode(!proximityEnabled + ? VoiceChatManager::VOICE_INPUT_VOICE_ACTIVATION + : VoiceChatManager::VOICE_INPUT_PUSH_TO_TALK); + return; + } + // check the checkboxes app.SetGameSettings(m_iPad,eGameSetting_ViewBob,m_checkboxViewBob.IsChecked()?1:0); app.SetGameSettings(m_iPad,eGameSetting_GamertagsVisible,m_checkboxInGameGamertags.IsChecked()?1:0); @@ -425,4 +555,4 @@ void UIScene_SettingsOptionsMenu::setGameSettings() // 4J-PB - don't action changes here or we might write to the profile on backing out here and then get a change in the settings all, and write again on backing out there //app.CheckGameSettingsChanged(true,pInputData->UserIndex); -} \ No newline at end of file +} diff --git a/Minecraft.Client/Common/UI/UIScene_SettingsOptionsMenu.h b/Minecraft.Client/Common/UI/UIScene_SettingsOptionsMenu.h index e9abb0a9..1b141d2c 100644 --- a/Minecraft.Client/Common/UI/UIScene_SettingsOptionsMenu.h +++ b/Minecraft.Client/Common/UI/UIScene_SettingsOptionsMenu.h @@ -40,7 +40,7 @@ private: bool m_bNotInGame; bool m_bMashUpWorldsUnhideOption; - bool m_bNavigateToLanguageSelector; + bool m_bVoiceChatMode; public: UIScene_SettingsOptionsMenu(int iPad, void *initData, UILayer *parentLayer); @@ -61,6 +61,7 @@ public: // INPUT virtual void handleInput(int iPad, int key, bool repeat, bool pressed, bool released, bool &handled); virtual void handlePress(F64 controlId, F64 childId); + virtual void handleCheckboxToggled(F64 controlId, bool selected); virtual void handleReload(); @@ -68,5 +69,10 @@ public: protected: void setGameSettings(); + void setupVoiceChatMenu(); + void setupStandardOptionsMenu(); + void setVoiceDifficultyLabel(); + void enforceVoiceModeSwitch(int preferredControl); + void placeVoiceBackButtonAtBottom(); -}; \ No newline at end of file +}; diff --git a/Minecraft.Client/Extrax64Stubs.cpp b/Minecraft.Client/Extrax64Stubs.cpp index 0147896c..05633e8a 100644 --- a/Minecraft.Client/Extrax64Stubs.cpp +++ b/Minecraft.Client/Extrax64Stubs.cpp @@ -21,6 +21,7 @@ #include "Windows64\Social\SocialManager.h" #include "Windows64\Sentient\DynamicConfigurations.h" #include "Windows64\Network\WinsockNetLayer.h" +#include "Common\Network\GameNetworkManager.h" #include "Windows64\Windows64_Xuid.h" #elif defined __PSVITA__ #include "PSVita\Sentient\SentientManager.h" @@ -194,6 +195,13 @@ void IQNetPlayer::SendData(IQNetPlayer * player, const void* pvData, DWORD dwDat { if (WinsockNetLayer::IsActive()) { + const bool requireAck = ((dwFlags & NON_QNET_SENDDATA_ACK_REQUIRED) == NON_QNET_SENDDATA_ACK_REQUIRED); + if (!requireAck) + { + if (WinsockNetLayer::SendToSmallIdUdp(m_smallId, player->m_smallId, pvData, static_cast(dwDataSize))) + return; + } + if (!WinsockNetLayer::IsHosting() && !m_isRemote) { SOCKET sock = WinsockNetLayer::GetLocalSocket(m_smallId); diff --git a/Minecraft.Client/Minecraft.cpp b/Minecraft.Client/Minecraft.cpp index 1c5e5b08..56d9eac5 100644 --- a/Minecraft.Client/Minecraft.cpp +++ b/Minecraft.Client/Minecraft.cpp @@ -2312,14 +2312,15 @@ void Minecraft::tick(bool bFirst, bool bUpdateTextures) vcm.init(); } - // Push-To-Talk: Only capture if 'V' key or DPAD_UP is held + // Push-To-Talk input (used when voice mode is Push-To-Talk) bool pttPressed = false; #ifdef _WINDOWS64 if (g_KBMInput.IsKeyDown('V')) pttPressed = true; #endif if (InputManager.ButtonDown(iPad, MINECRAFT_ACTION_DPAD_UP)) pttPressed = true; - + vcm.setPushToTalk(pttPressed); + vcm.tick(this); // Tick the opacity timer (to display the interface at default opacity for a certain time if the user has been navigating it) diff --git a/Minecraft.Client/PlayerConnection.cpp b/Minecraft.Client/PlayerConnection.cpp index e62acabb..cef9e3ee 100644 --- a/Minecraft.Client/PlayerConnection.cpp +++ b/Minecraft.Client/PlayerConnection.cpp @@ -1813,6 +1813,6 @@ void PlayerConnection::handleVoiceChat(VoiceChatPacket *packet) // Broadcast to all connected players (including the sender's client, which will filter itself) auto sharedPacket = std::make_shared( - packet->senderPlayerId, packet->audioData, packet->dataLength); + packet->senderPlayerId, packet->sequence, packet->audioData, packet->dataLength); server->getPlayers()->broadcastAll(sharedPacket); } diff --git a/Minecraft.Client/VoiceChatManager.cpp b/Minecraft.Client/VoiceChatManager.cpp index ac0d2c58..9a03b6c0 100644 --- a/Minecraft.Client/VoiceChatManager.cpp +++ b/Minecraft.Client/VoiceChatManager.cpp @@ -7,10 +7,45 @@ #include "..\Minecraft.World\net.minecraft.world.entity.player.h" #include "..\Minecraft.World\Connection.h" #include "..\Minecraft.World\System.h" +#include // Include miniaudio #include "Common\Audio\miniaudio.h" +namespace +{ + static void RefreshDeviceLists( + std::vector &playbackDevices, + std::vector &captureDevices, + ma_device_info *pPlaybackInfos, + ma_uint32 playbackCount, + ma_device_info *pCaptureInfos, + ma_uint32 captureCount) + { + playbackDevices.clear(); + captureDevices.clear(); + + for (ma_uint32 i = 0; i < playbackCount; ++i) + { + const char *name = pPlaybackInfos[i].name; + playbackDevices.emplace_back(name, name + strlen(name)); + } + + for (ma_uint32 i = 0; i < captureCount; ++i) + { + const char *name = pCaptureInfos[i].name; + captureDevices.emplace_back(name, name + strlen(name)); + } + } + + static const ma_device_id *GetSelectedDeviceId(int selectedIndex, ma_device_info *pInfos, ma_uint32 count) + { + if (selectedIndex < 0) return nullptr; // default device + if (selectedIndex >= static_cast(count)) return nullptr; + return &pInfos[selectedIndex].id; + } +} + VoiceChatManager &VoiceChatManager::getInstance() { static VoiceChatManager instance; @@ -19,12 +54,21 @@ VoiceChatManager &VoiceChatManager::getInstance() VoiceChatManager::VoiceChatManager() : m_initialized(false) + , m_audioContext(nullptr) , m_captureDevice(nullptr) , m_playbackDevice(nullptr) , m_captureWritePos(0) , m_captureReadPos(0) , m_listenerX(0), m_listenerY(0), m_listenerZ(0) , m_isPushToTalkActive(false) + , m_voiceInputMode(VOICE_INPUT_PUSH_TO_TALK) + , m_proximityEnabled(true) + , m_voiceThreshold(300) + , m_selectedCaptureDevice(-1) + , m_selectedPlaybackDevice(-1) + , m_localSequence(0) + , m_micVolumePercent(100) + , m_voiceChatVolumePercent(100) { memset(m_captureBuffer, 0, sizeof(m_captureBuffer)); InitializeCriticalSection(&m_captureLock); @@ -70,25 +114,31 @@ void VoiceChatManager::onPlaybackAudio(ma_device *pDevice, void *pOutput, const { RemoteVoiceStream &stream = pair.second; - // Calculate distance attenuation - double dx = stream.x - mgr->m_listenerX; - double dy = stream.y - mgr->m_listenerY; - double dz = stream.z - mgr->m_listenerZ; - double dist = sqrt(dx * dx + dy * dy + dz * dz); - float volume = 1.0f; - if (dist >= MAX_VOICE_DISTANCE) + + if (mgr->m_proximityEnabled) { - volume = 0.0f; - } - else if (dist > 1.0) - { - // Linear falloff with exponential tail - float linearFactor = 1.0f - (float)(dist / MAX_VOICE_DISTANCE); - float expFactor = (float)exp(-dist * 0.08); - volume = linearFactor * expFactor; + // Calculate distance attenuation + double dx = stream.x - mgr->m_listenerX; + double dy = stream.y - mgr->m_listenerY; + double dz = stream.z - mgr->m_listenerZ; + double dist = sqrt(dx * dx + dy * dy + dz * dz); + + if (dist >= MAX_VOICE_DISTANCE) + { + volume = 0.0f; + } + else if (dist > 1.0) + { + // Linear falloff with exponential tail + float linearFactor = 1.0f - (float)(dist / MAX_VOICE_DISTANCE); + float expFactor = (float)exp(-dist * 0.08); + volume = linearFactor * expFactor; + } } + if (volume <= 0.001f) continue; + volume *= (float)mgr->m_voiceChatVolumePercent / 100.0f; if (volume <= 0.001f) continue; // Mix this stream's audio into the output @@ -115,30 +165,54 @@ void VoiceChatManager::init() { if (m_initialized) return; + m_audioContext = new ma_context(); + if (ma_context_init(nullptr, 0, nullptr, m_audioContext) != MA_SUCCESS) + { + app.DebugPrintf("VoiceChat: Failed to initialize audio context\n"); + delete m_audioContext; + m_audioContext = nullptr; + return; + } + + ma_device_info *pPlaybackInfos = nullptr; + ma_uint32 playbackCount = 0; + ma_device_info *pCaptureInfos = nullptr; + ma_uint32 captureCount = 0; + if (ma_context_get_devices(m_audioContext, &pPlaybackInfos, &playbackCount, &pCaptureInfos, &captureCount) != MA_SUCCESS) + { + app.DebugPrintf("VoiceChat: Failed to enumerate audio devices\n"); + ma_context_uninit(m_audioContext); + delete m_audioContext; + m_audioContext = nullptr; + return; + } + + RefreshDeviceLists(m_playbackDevices, m_captureDevices, pPlaybackInfos, playbackCount, pCaptureInfos, captureCount); + if (m_selectedCaptureDevice >= static_cast(m_captureDevices.size())) m_selectedCaptureDevice = -1; + if (m_selectedPlaybackDevice >= static_cast(m_playbackDevices.size())) m_selectedPlaybackDevice = -1; + // Initialize capture device (microphone) m_captureDevice = new ma_device(); ma_device_config captureConfig = ma_device_config_init(ma_device_type_capture); captureConfig.capture.format = ma_format_s16; captureConfig.capture.channels = CHANNELS; + captureConfig.capture.pDeviceID = GetSelectedDeviceId(m_selectedCaptureDevice, pCaptureInfos, captureCount); captureConfig.sampleRate = SAMPLE_RATE; captureConfig.dataCallback = onCaptureAudio; captureConfig.pUserData = this; - if (ma_device_init(nullptr, &captureConfig, m_captureDevice) != MA_SUCCESS) + if (ma_device_init(m_audioContext, &captureConfig, m_captureDevice) != MA_SUCCESS) { app.DebugPrintf("VoiceChat: Failed to initialize capture device\n"); delete m_captureDevice; m_captureDevice = nullptr; - return; } - - if (ma_device_start(m_captureDevice) != MA_SUCCESS) + else if (ma_device_start(m_captureDevice) != MA_SUCCESS) { app.DebugPrintf("VoiceChat: Failed to start capture device\n"); ma_device_uninit(m_captureDevice); delete m_captureDevice; m_captureDevice = nullptr; - return; } // Initialize playback device (speakers) @@ -146,11 +220,12 @@ void VoiceChatManager::init() ma_device_config playbackConfig = ma_device_config_init(ma_device_type_playback); playbackConfig.playback.format = ma_format_s16; playbackConfig.playback.channels = CHANNELS; + playbackConfig.playback.pDeviceID = GetSelectedDeviceId(m_selectedPlaybackDevice, pPlaybackInfos, playbackCount); playbackConfig.sampleRate = SAMPLE_RATE; playbackConfig.dataCallback = onPlaybackAudio; playbackConfig.pUserData = this; - if (ma_device_init(nullptr, &playbackConfig, m_playbackDevice) != MA_SUCCESS) + if (ma_device_init(m_audioContext, &playbackConfig, m_playbackDevice) != MA_SUCCESS) { app.DebugPrintf("VoiceChat: Failed to initialize playback device\n"); delete m_playbackDevice; @@ -168,9 +243,19 @@ void VoiceChatManager::init() } } - m_initialized = true; - app.DebugPrintf("VoiceChat: Initialized (capture=%s, playback=%s)\n", - m_captureDevice ? "OK" : "FAIL", m_playbackDevice ? "OK" : "FAIL"); + m_initialized = (m_captureDevice != nullptr || m_playbackDevice != nullptr); + if (!m_initialized) + { + ma_context_uninit(m_audioContext); + delete m_audioContext; + m_audioContext = nullptr; + } + + app.DebugPrintf("VoiceChat: Initialized (capture=%s, playback=%s, mode=%s, proximity=%s)\n", + m_captureDevice ? "OK" : "FAIL", + m_playbackDevice ? "OK" : "FAIL", + m_voiceInputMode == VOICE_INPUT_PUSH_TO_TALK ? "PTT" : "VAD", + m_proximityEnabled ? "ON" : "OFF"); } void VoiceChatManager::shutdown() @@ -195,10 +280,157 @@ void VoiceChatManager::shutdown() m_remoteStreams.clear(); LeaveCriticalSection(&m_playbackLock); + if (m_audioContext) + { + ma_context_uninit(m_audioContext); + delete m_audioContext; + m_audioContext = nullptr; + } + m_captureDevices.clear(); + m_playbackDevices.clear(); + m_initialized = false; app.DebugPrintf("VoiceChat: Shutdown\n"); } +void VoiceChatManager::setVoiceInputMode(VoiceInputMode mode) +{ + m_voiceInputMode = mode; + app.DebugPrintf("VoiceChat: Input mode set to %s\n", m_voiceInputMode == VOICE_INPUT_PUSH_TO_TALK ? "PTT" : "VoiceActivation"); +} + +void VoiceChatManager::toggleVoiceInputMode() +{ + if (m_voiceInputMode == VOICE_INPUT_PUSH_TO_TALK) + { + setVoiceInputMode(VOICE_INPUT_VOICE_ACTIVATION); + } + else + { + setVoiceInputMode(VOICE_INPUT_PUSH_TO_TALK); + } +} + +void VoiceChatManager::toggleProximityEnabled() +{ + m_proximityEnabled = !m_proximityEnabled; + app.DebugPrintf("VoiceChat: Proximity %s\n", m_proximityEnabled ? "enabled" : "disabled"); +} + +bool VoiceChatManager::cycleCaptureDevice(int dir) +{ + if (!m_initialized) init(); + if (m_captureDevices.empty()) return false; + + const int totalSlots = static_cast(m_captureDevices.size()) + 1; // default + explicit devices + int slot = m_selectedCaptureDevice + 1; // -1 -> 0 + slot = (slot + dir) % totalSlots; + if (slot < 0) slot += totalSlots; + m_selectedCaptureDevice = slot - 1; + + shutdown(); + init(); + + app.DebugPrintf("VoiceChat: Capture device -> %ls\n", getSelectedCaptureDeviceName().c_str()); + return m_initialized; +} + +bool VoiceChatManager::cyclePlaybackDevice(int dir) +{ + if (!m_initialized) init(); + if (m_playbackDevices.empty()) return false; + + const int totalSlots = static_cast(m_playbackDevices.size()) + 1; // default + explicit devices + int slot = m_selectedPlaybackDevice + 1; // -1 -> 0 + slot = (slot + dir) % totalSlots; + if (slot < 0) slot += totalSlots; + m_selectedPlaybackDevice = slot - 1; + + shutdown(); + init(); + + app.DebugPrintf("VoiceChat: Playback device -> %ls\n", getSelectedPlaybackDeviceName().c_str()); + return m_initialized; +} + +wstring VoiceChatManager::getSelectedCaptureDeviceName() const +{ + if (m_selectedCaptureDevice < 0 || m_selectedCaptureDevice >= static_cast(m_captureDevices.size())) + { + return L"Default"; + } + return m_captureDevices[m_selectedCaptureDevice]; +} + +wstring VoiceChatManager::getSelectedPlaybackDeviceName() const +{ + if (m_selectedPlaybackDevice < 0 || m_selectedPlaybackDevice >= static_cast(m_playbackDevices.size())) + { + return L"Default"; + } + return m_playbackDevices[m_selectedPlaybackDevice]; +} + +void VoiceChatManager::getCaptureDeviceNames(vector &outNames, bool includeDefault) const +{ + outNames.clear(); + if (includeDefault) outNames.push_back(L"Default"); + for (const auto &name : m_captureDevices) + { + outNames.push_back(name); + } +} + +void VoiceChatManager::getPlaybackDeviceNames(vector &outNames, bool includeDefault) const +{ + outNames.clear(); + if (includeDefault) outNames.push_back(L"Default"); + for (const auto &name : m_playbackDevices) + { + outNames.push_back(name); + } +} + +int VoiceChatManager::getSelectedCaptureMenuIndex() const +{ + return m_selectedCaptureDevice + 1; +} + +int VoiceChatManager::getSelectedPlaybackMenuIndex() const +{ + return m_selectedPlaybackDevice + 1; +} + +bool VoiceChatManager::selectCaptureMenuIndex(int menuIndex) +{ + if (!m_initialized) init(); + const int maxIndex = static_cast(m_captureDevices.size()); + if (menuIndex < 0 || menuIndex > maxIndex) return false; + + const int nextSelected = menuIndex - 1; + if (nextSelected == m_selectedCaptureDevice) return true; + + m_selectedCaptureDevice = nextSelected; + shutdown(); + init(); + return m_initialized; +} + +bool VoiceChatManager::selectPlaybackMenuIndex(int menuIndex) +{ + if (!m_initialized) init(); + const int maxIndex = static_cast(m_playbackDevices.size()); + if (menuIndex < 0 || menuIndex > maxIndex) return false; + + const int nextSelected = menuIndex - 1; + if (nextSelected == m_selectedPlaybackDevice) return true; + + m_selectedPlaybackDevice = nextSelected; + shutdown(); + init(); + return m_initialized; +} + void VoiceChatManager::tick(Minecraft *minecraft) { if (!m_initialized || m_captureDevice == nullptr) return; @@ -216,18 +448,37 @@ void VoiceChatManager::tick(Minecraft *minecraft) int available = (m_captureWritePos - m_captureReadPos + CAPTURE_BUFFER_SIZE) % CAPTURE_BUFFER_SIZE; - // Send in chunks of FRAMES_PER_PACKET (320 samples = 20ms at 16kHz) + // Send in short frames to keep UDP payloads under common MTU limits. while (available >= FRAMES_PER_PACKET) { short frameBuffer[FRAMES_PER_PACKET]; for (int i = 0; i < FRAMES_PER_PACKET; i++) { - frameBuffer[i] = m_captureBuffer[m_captureReadPos]; + int sample = m_captureBuffer[m_captureReadPos]; + sample = (sample * m_micVolumePercent) / 100; + if (sample > 32767) sample = 32767; + if (sample < -32768) sample = -32768; + frameBuffer[i] = (short)sample; m_captureReadPos = (m_captureReadPos + 1) % CAPTURE_BUFFER_SIZE; } - // Send audio only if Push-To-Talk is active - if (m_isPushToTalkActive) + bool shouldSend = false; + if (m_voiceInputMode == VOICE_INPUT_PUSH_TO_TALK) + { + shouldSend = m_isPushToTalkActive; + } + else + { + int peak = 0; + for (int i = 0; i < FRAMES_PER_PACKET; ++i) + { + int sample = abs((int)frameBuffer[i]); + if (sample > peak) peak = sample; + } + shouldSend = peak >= m_voiceThreshold; + } + + if (shouldSend) { // Mark our local player as speaking for the indicator markSpeaking(minecraft->player->entityId); @@ -238,9 +489,10 @@ void VoiceChatManager::tick(Minecraft *minecraft) memcpy(audioData.data, frameBuffer, dataLen); shared_ptr packet = std::make_shared( - minecraft->player->entityId, audioData, dataLen); + minecraft->player->entityId, m_localSequence++, audioData, dataLen); - conn->send(packet); + // Queue to slow path so it is emitted as standalone UDP transport packets. + conn->queueSend(packet); } available = (m_captureWritePos - m_captureReadPos + CAPTURE_BUFFER_SIZE) % CAPTURE_BUFFER_SIZE; @@ -268,7 +520,7 @@ void VoiceChatManager::tick(Minecraft *minecraft) tickSpeakingState(); } -void VoiceChatManager::receiveVoiceData(int playerId, double x, double y, double z, const unsigned char *data, int dataLength) +void VoiceChatManager::receiveVoiceData(int playerId, unsigned short sequence, double x, double y, double z, const unsigned char *data, int dataLength) { if (!m_initialized || m_playbackDevice == nullptr) return; if (data == nullptr || dataLength <= 0) return; @@ -284,6 +536,38 @@ void VoiceChatManager::receiveVoiceData(int playerId, double x, double y, double stream.z = z; stream.lastReceiveTime = System::currentTimeMillis(); + // Reject very old/out-of-order packets aggressively, but tolerate wrap-around. + if (stream.hasSequence) + { + unsigned short delta = static_cast(sequence - stream.lastSequence); + if (delta == 0) + { + LeaveCriticalSection(&m_playbackLock); + return; // duplicate + } + if (delta > 32768) + { + LeaveCriticalSection(&m_playbackLock); + return; // stale packet + } + + // Packet loss concealment: for small gaps, insert silence frames. + const int missingPackets = static_cast(delta) - 1; + if (missingPackets > 0) + { + const int concealPackets = (missingPackets > 3) ? 3 : missingPackets; + const int concealSamples = concealPackets * FRAMES_PER_PACKET; + for (int i = 0; i < concealSamples; ++i) + { + stream.buffer[stream.writePos] = 0; + stream.writePos = (stream.writePos + 1) % RemoteVoiceStream::BUFFER_SIZE; + } + } + } + + stream.lastSequence = sequence; + stream.hasSequence = true; + for (int i = 0; i < sampleCount; i++) { stream.buffer[stream.writePos] = samples[i]; @@ -332,3 +616,17 @@ void VoiceChatManager::tickSpeakingState() } LeaveCriticalSection(&m_speakingLock); } + +void VoiceChatManager::setMicVolumePercent(int percent) +{ + if (percent < 0) percent = 0; + if (percent > 200) percent = 200; + m_micVolumePercent = percent; +} + +void VoiceChatManager::setVoiceChatVolumePercent(int percent) +{ + if (percent < 0) percent = 0; + if (percent > 200) percent = 200; + m_voiceChatVolumePercent = percent; +} diff --git a/Minecraft.Client/VoiceChatManager.h b/Minecraft.Client/VoiceChatManager.h index bec6b8a1..56d2ba27 100644 --- a/Minecraft.Client/VoiceChatManager.h +++ b/Minecraft.Client/VoiceChatManager.h @@ -1,4 +1,7 @@ #pragma once +#include +#include +#include using namespace std; class Connection; @@ -10,6 +13,12 @@ struct ma_device; class VoiceChatManager { public: + enum VoiceInputMode + { + VOICE_INPUT_PUSH_TO_TALK = 0, + VOICE_INPUT_VOICE_ACTIVATION = 1 + }; + static VoiceChatManager &getInstance(); void init(); @@ -19,7 +28,7 @@ public: void tick(Minecraft *minecraft); // Called when a voice packet arrives from the network - void receiveVoiceData(int playerId, double x, double y, double z, const unsigned char *data, int dataLength); + void receiveVoiceData(int playerId, unsigned short sequence, double x, double y, double z, const unsigned char *data, int dataLength); // Update the local listener's position for 3D attenuation void setListenerPosition(double x, double y, double z); @@ -28,6 +37,34 @@ public: void setPushToTalk(bool active) { m_isPushToTalkActive = active; } bool isPushToTalkActive() const { return m_isPushToTalkActive; } + // Voice input mode controls + void setVoiceInputMode(VoiceInputMode mode); + VoiceInputMode getVoiceInputMode() const { return m_voiceInputMode; } + void toggleVoiceInputMode(); + + // Proximity chat controls + void setProximityEnabled(bool enabled) { m_proximityEnabled = enabled; } + bool isProximityEnabled() const { return m_proximityEnabled; } + void toggleProximityEnabled(); + + // Audio device selection controls + bool cycleCaptureDevice(int dir = 1); + bool cyclePlaybackDevice(int dir = 1); + wstring getSelectedCaptureDeviceName() const; + wstring getSelectedPlaybackDeviceName() const; + void getCaptureDeviceNames(vector &outNames, bool includeDefault = true) const; + void getPlaybackDeviceNames(vector &outNames, bool includeDefault = true) const; + int getSelectedCaptureMenuIndex() const; + int getSelectedPlaybackMenuIndex() const; + bool selectCaptureMenuIndex(int menuIndex); + bool selectPlaybackMenuIndex(int menuIndex); + + // Voice volume controls (percent, 0-200) + void setMicVolumePercent(int percent); + int getMicVolumePercent() const { return m_micVolumePercent; } + void setVoiceChatVolumePercent(int percent); + int getVoiceChatVolumePercent() const { return m_voiceChatVolumePercent; } + bool isInitialized() const { return m_initialized; } // Speaking state tracking for rendering indicators @@ -49,9 +86,20 @@ private: bool m_initialized; + struct ma_context *m_audioContext; ma_device *m_captureDevice; ma_device *m_playbackDevice; bool m_isPushToTalkActive; + VoiceInputMode m_voiceInputMode; + bool m_proximityEnabled; + int m_voiceThreshold; + std::vector m_captureDevices; + std::vector m_playbackDevices; + int m_selectedCaptureDevice; + int m_selectedPlaybackDevice; + unsigned short m_localSequence; + int m_micVolumePercent; + int m_voiceChatVolumePercent; // Capture buffer (ring buffer for PCM data captured from mic) static const int CAPTURE_BUFFER_SIZE = 96000; // 2 seconds at 48kHz mono @@ -60,9 +108,6 @@ private: int m_captureReadPos; CRITICAL_SECTION m_captureLock; - // Voice activity detection threshold - static const int VOICE_THRESHOLD = 500; - // Per-player playback buffers struct RemoteVoiceStream { @@ -72,8 +117,10 @@ private: int readPos; double x, y, z; // Last known position of this speaker int64_t lastReceiveTime; // For cleanup of stale streams + unsigned short lastSequence; + bool hasSequence; - RemoteVoiceStream() : writePos(0), readPos(0), x(0), y(0), z(0), lastReceiveTime(0) + RemoteVoiceStream() : writePos(0), readPos(0), x(0), y(0), z(0), lastReceiveTime(0), lastSequence(0), hasSequence(false) { memset(buffer, 0, sizeof(buffer)); } @@ -88,8 +135,8 @@ private: // Constants static const int SAMPLE_RATE = 48000; static const int CHANNELS = 1; - static const int FRAME_SIZE_MS = 20; - static const int FRAMES_PER_PACKET = SAMPLE_RATE * FRAME_SIZE_MS / 1000; // 320 samples + static const int FRAME_SIZE_MS = 10; + static const int FRAMES_PER_PACKET = SAMPLE_RATE * FRAME_SIZE_MS / 1000; static const int MAX_VOICE_DISTANCE = 32; // blocks // Speaking indicator state: entityId -> remaining ticks diff --git a/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp b/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp index 28d29504..c8ab84c4 100644 --- a/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp +++ b/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp @@ -18,6 +18,13 @@ SOCKET WinsockNetLayer::s_listenSocket = INVALID_SOCKET; SOCKET WinsockNetLayer::s_hostConnectionSocket = INVALID_SOCKET; HANDLE WinsockNetLayer::s_acceptThread = nullptr; HANDLE WinsockNetLayer::s_clientRecvThread = nullptr; +SOCKET WinsockNetLayer::s_sideUdpSocket = INVALID_SOCKET; +HANDLE WinsockNetLayer::s_sideUdpRecvThread = nullptr; +volatile bool WinsockNetLayer::s_sideUdpActive = false; +sockaddr_in WinsockNetLayer::s_hostUdpAddr = {}; +bool WinsockNetLayer::s_hasHostUdpAddr = false; +sockaddr_in WinsockNetLayer::s_smallIdToUdpAddr[256] = {}; +bool WinsockNetLayer::s_smallIdHasUdpAddr[256] = {}; bool WinsockNetLayer::s_isHost = false; bool WinsockNetLayer::s_connected = false; @@ -53,6 +60,8 @@ CRITICAL_SECTION WinsockNetLayer::s_freeSmallIdLock; std::vector WinsockNetLayer::s_freeSmallIds; SOCKET WinsockNetLayer::s_smallIdToSocket[256]; CRITICAL_SECTION WinsockNetLayer::s_smallIdToSocketLock; +CRITICAL_SECTION WinsockNetLayer::s_sideUdpAddrLock; +CRITICAL_SECTION WinsockNetLayer::s_sideUdpSendLock; SOCKET WinsockNetLayer::s_splitScreenSocket[XUSER_MAX_COUNT] = { INVALID_SOCKET, INVALID_SOCKET, INVALID_SOCKET, INVALID_SOCKET }; BYTE WinsockNetLayer::s_splitScreenSmallId[XUSER_MAX_COUNT] = { 0xFF, 0xFF, 0xFF, 0xFF }; @@ -85,8 +94,14 @@ bool WinsockNetLayer::Initialize() InitializeCriticalSection(&s_disconnectLock); InitializeCriticalSection(&s_freeSmallIdLock); InitializeCriticalSection(&s_smallIdToSocketLock); + InitializeCriticalSection(&s_sideUdpAddrLock); + InitializeCriticalSection(&s_sideUdpSendLock); for (int i = 0; i < 256; i++) + { s_smallIdToSocket[i] = INVALID_SOCKET; + s_smallIdHasUdpAddr[i] = false; + } + s_hasHostUdpAddr = false; s_initialized = true; @@ -99,6 +114,7 @@ void WinsockNetLayer::Shutdown() { StopAdvertising(); StopDiscovery(); + StopSideUdp(); s_active = false; s_connected = false; @@ -192,6 +208,8 @@ void WinsockNetLayer::Shutdown() DeleteCriticalSection(&s_disconnectLock); DeleteCriticalSection(&s_freeSmallIdLock); DeleteCriticalSection(&s_smallIdToSocketLock); + DeleteCriticalSection(&s_sideUdpAddrLock); + DeleteCriticalSection(&s_sideUdpSendLock); WSACleanup(); s_initialized = false; } @@ -214,6 +232,11 @@ bool WinsockNetLayer::HostGame(int port, const char* bindIp) for (int i = 0; i < 256; i++) s_smallIdToSocket[i] = INVALID_SOCKET; LeaveCriticalSection(&s_smallIdToSocketLock); + EnterCriticalSection(&s_sideUdpAddrLock); + for (int i = 0; i < 256; i++) + s_smallIdHasUdpAddr[i] = false; + s_hasHostUdpAddr = false; + LeaveCriticalSection(&s_sideUdpAddrLock); struct addrinfo hints = {}; struct addrinfo* result = nullptr; @@ -270,6 +293,11 @@ bool WinsockNetLayer::HostGame(int port, const char* bindIp) s_active = true; s_connected = true; + if (!StartSideUdpHost(port)) + { + app.DebugPrintf("Win64 LAN: Side UDP failed to start on port %d, falling back to TCP-only for voice\n", port); + } + s_acceptThread = CreateThread(nullptr, 0, AcceptThreadProc, nullptr, 0, nullptr); app.DebugPrintf("Win64 LAN: Hosting on %s:%d\n", @@ -397,6 +425,11 @@ bool WinsockNetLayer::JoinGame(const char* ip, int port) s_active = true; s_connected = true; + if (!StartSideUdpClient(ip, port)) + { + app.DebugPrintf("Win64 LAN: Side UDP failed to start for %s:%d, falling back to TCP-only for voice\n", ip, port); + } + s_clientRecvThread = CreateThread(nullptr, 0, ClientRecvThreadProc, nullptr, 0, nullptr); return true; @@ -466,6 +499,69 @@ bool WinsockNetLayer::SendToSmallId(BYTE targetSmallId, const void* data, int da } } +bool WinsockNetLayer::SendSideUdpPacket(const sockaddr_in &addr, BYTE fromSmallId, BYTE toSmallId, const void* data, int dataSize) +{ + if (s_sideUdpSocket == INVALID_SOCKET || data == nullptr || dataSize <= 0 || dataSize > 1400) + return false; + + const int packetSize = static_cast(sizeof(Win64SideUdpHeader)) + dataSize; + std::vector packet(packetSize); + Win64SideUdpHeader* header = reinterpret_cast(&packet[0]); + header->fromSmallId = fromSmallId; + header->toSmallId = toSmallId; + header->payloadSize = htons(static_cast(dataSize)); + memcpy(&packet[sizeof(Win64SideUdpHeader)], data, dataSize); + + EnterCriticalSection(&s_sideUdpSendLock); + int sent = sendto( + s_sideUdpSocket, + reinterpret_cast(&packet[0]), + packetSize, + 0, + reinterpret_cast(&addr), + sizeof(addr)); + LeaveCriticalSection(&s_sideUdpSendLock); + + return sent == packetSize; +} + +bool WinsockNetLayer::SendToSmallIdUdp(BYTE fromSmallId, BYTE targetSmallId, const void* data, int dataSize) +{ + if (!s_active || !s_sideUdpActive) + return false; + + if (s_isHost) + { + sockaddr_in addr = {}; + bool hasAddr = false; + EnterCriticalSection(&s_sideUdpAddrLock); + if (targetSmallId < 256 && s_smallIdHasUdpAddr[targetSmallId]) + { + addr = s_smallIdToUdpAddr[targetSmallId]; + hasAddr = true; + } + LeaveCriticalSection(&s_sideUdpAddrLock); + if (!hasAddr) + return false; + return SendSideUdpPacket(addr, fromSmallId, targetSmallId, data, dataSize); + } + else + { + sockaddr_in addr = {}; + bool hasAddr = false; + EnterCriticalSection(&s_sideUdpAddrLock); + if (s_hasHostUdpAddr) + { + addr = s_hostUdpAddr; + hasAddr = true; + } + LeaveCriticalSection(&s_sideUdpAddrLock); + if (!hasAddr) + return false; + return SendSideUdpPacket(addr, fromSmallId, targetSmallId, data, dataSize); + } +} + SOCKET WinsockNetLayer::GetSocketForSmallId(BYTE smallId) { EnterCriticalSection(&s_smallIdToSocketLock); @@ -713,6 +809,9 @@ DWORD WINAPI WinsockNetLayer::RecvThreadProc(LPVOID param) EnterCriticalSection(&s_disconnectLock); s_disconnectedSmallIds.push_back(clientSmallId); LeaveCriticalSection(&s_disconnectLock); + EnterCriticalSection(&s_sideUdpAddrLock); + s_smallIdHasUdpAddr[clientSmallId] = false; + LeaveCriticalSection(&s_sideUdpAddrLock); return 0; } @@ -757,6 +856,9 @@ void WinsockNetLayer::CloseConnectionBySmallId(BYTE smallId) closesocket(s_connections[i].tcpSocket); s_connections[i].tcpSocket = INVALID_SOCKET; app.DebugPrintf("Win64 LAN: Force-closed TCP connection for smallId=%d\n", smallId); + EnterCriticalSection(&s_sideUdpAddrLock); + s_smallIdHasUdpAddr[smallId] = false; + LeaveCriticalSection(&s_sideUdpAddrLock); break; } } @@ -971,6 +1073,208 @@ DWORD WINAPI WinsockNetLayer::ClientRecvThreadProc(LPVOID param) return 0; } +bool WinsockNetLayer::StartSideUdpHost(int port) +{ + StopSideUdp(); + + s_sideUdpSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (s_sideUdpSocket == INVALID_SOCKET) + { + app.DebugPrintf("Win64 LAN: Side UDP socket() failed: %d\n", WSAGetLastError()); + return false; + } + + BOOL reuseAddr = TRUE; + setsockopt(s_sideUdpSocket, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuseAddr, sizeof(reuseAddr)); + + struct sockaddr_in bindAddr; + memset(&bindAddr, 0, sizeof(bindAddr)); + bindAddr.sin_family = AF_INET; + bindAddr.sin_port = htons(static_cast(port)); + bindAddr.sin_addr.s_addr = INADDR_ANY; + + if (::bind(s_sideUdpSocket, (struct sockaddr*)&bindAddr, sizeof(bindAddr)) == SOCKET_ERROR) + { + app.DebugPrintf("Win64 LAN: Side UDP bind failed on port %d: %d\n", port, WSAGetLastError()); + closesocket(s_sideUdpSocket); + s_sideUdpSocket = INVALID_SOCKET; + return false; + } + + DWORD timeout = 200; + setsockopt(s_sideUdpSocket, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout, sizeof(timeout)); + + EnterCriticalSection(&s_sideUdpAddrLock); + for (int i = 0; i < 256; i++) + s_smallIdHasUdpAddr[i] = false; + s_hasHostUdpAddr = false; + LeaveCriticalSection(&s_sideUdpAddrLock); + + s_sideUdpActive = true; + s_sideUdpRecvThread = CreateThread(nullptr, 0, SideUdpRecvThreadProc, nullptr, 0, nullptr); + if (s_sideUdpRecvThread == nullptr) + { + s_sideUdpActive = false; + closesocket(s_sideUdpSocket); + s_sideUdpSocket = INVALID_SOCKET; + return false; + } + + app.DebugPrintf("Win64 LAN: Side UDP started (host) on port %d\n", port); + return true; +} + +bool WinsockNetLayer::StartSideUdpClient(const char* ip, int port) +{ + StopSideUdp(); + + s_sideUdpSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (s_sideUdpSocket == INVALID_SOCKET) + { + app.DebugPrintf("Win64 LAN: Side UDP socket() failed: %d\n", WSAGetLastError()); + return false; + } + + DWORD timeout = 200; + setsockopt(s_sideUdpSocket, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout, sizeof(timeout)); + + struct sockaddr_in bindAddr; + memset(&bindAddr, 0, sizeof(bindAddr)); + bindAddr.sin_family = AF_INET; + bindAddr.sin_port = htons(0); + bindAddr.sin_addr.s_addr = INADDR_ANY; + + if (::bind(s_sideUdpSocket, (struct sockaddr*)&bindAddr, sizeof(bindAddr)) == SOCKET_ERROR) + { + app.DebugPrintf("Win64 LAN: Side UDP client bind failed: %d\n", WSAGetLastError()); + closesocket(s_sideUdpSocket); + s_sideUdpSocket = INVALID_SOCKET; + return false; + } + + struct addrinfo hints = {}; + struct addrinfo* result = nullptr; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_protocol = IPPROTO_UDP; + + char portStr[16]; + sprintf_s(portStr, "%d", port); + if (getaddrinfo(ip, portStr, &hints, &result) != 0 || result == nullptr) + { + app.DebugPrintf("Win64 LAN: Side UDP invalid host IP: %s\n", ip); + closesocket(s_sideUdpSocket); + s_sideUdpSocket = INVALID_SOCKET; + return false; + } + sockaddr_in hostAddr = *reinterpret_cast(result->ai_addr); + freeaddrinfo(result); + + EnterCriticalSection(&s_sideUdpAddrLock); + s_hostUdpAddr = hostAddr; + s_hasHostUdpAddr = true; + LeaveCriticalSection(&s_sideUdpAddrLock); + + s_sideUdpActive = true; + s_sideUdpRecvThread = CreateThread(nullptr, 0, SideUdpRecvThreadProc, nullptr, 0, nullptr); + if (s_sideUdpRecvThread == nullptr) + { + s_sideUdpActive = false; + closesocket(s_sideUdpSocket); + s_sideUdpSocket = INVALID_SOCKET; + EnterCriticalSection(&s_sideUdpAddrLock); + s_hasHostUdpAddr = false; + LeaveCriticalSection(&s_sideUdpAddrLock); + return false; + } + + app.DebugPrintf("Win64 LAN: Side UDP started (client) to %s:%d\n", ip, port); + return true; +} + +void WinsockNetLayer::StopSideUdp() +{ + s_sideUdpActive = false; + + if (s_sideUdpSocket != INVALID_SOCKET) + { + closesocket(s_sideUdpSocket); + s_sideUdpSocket = INVALID_SOCKET; + } + + if (s_sideUdpRecvThread != nullptr) + { + WaitForSingleObject(s_sideUdpRecvThread, 1000); + CloseHandle(s_sideUdpRecvThread); + s_sideUdpRecvThread = nullptr; + } + + EnterCriticalSection(&s_sideUdpAddrLock); + s_hasHostUdpAddr = false; + for (int i = 0; i < 256; i++) + s_smallIdHasUdpAddr[i] = false; + LeaveCriticalSection(&s_sideUdpAddrLock); +} + +DWORD WINAPI WinsockNetLayer::SideUdpRecvThreadProc(LPVOID param) +{ + std::vector recvBuf; + recvBuf.resize(1600); + + while (s_sideUdpActive && s_sideUdpSocket != INVALID_SOCKET) + { + sockaddr_in senderAddr; + int senderLen = sizeof(senderAddr); + + int recvLen = recvfrom( + s_sideUdpSocket, + reinterpret_cast(&recvBuf[0]), + static_cast(recvBuf.size()), + 0, + reinterpret_cast(&senderAddr), + &senderLen); + + if (recvLen == SOCKET_ERROR) + { + if (!s_sideUdpActive) + break; + continue; + } + + if (recvLen < static_cast(sizeof(Win64SideUdpHeader))) + continue; + + Win64SideUdpHeader* header = reinterpret_cast(&recvBuf[0]); + const int payloadSize = ntohs(header->payloadSize); + const int expectedSize = static_cast(sizeof(Win64SideUdpHeader)) + payloadSize; + if (payloadSize <= 0 || expectedSize != recvLen) + continue; + + if (s_isHost) + { + EnterCriticalSection(&s_sideUdpAddrLock); + s_smallIdToUdpAddr[header->fromSmallId] = senderAddr; + s_smallIdHasUdpAddr[header->fromSmallId] = true; + LeaveCriticalSection(&s_sideUdpAddrLock); + } + else + { + EnterCriticalSection(&s_sideUdpAddrLock); + s_hostUdpAddr = senderAddr; + s_hasHostUdpAddr = true; + LeaveCriticalSection(&s_sideUdpAddrLock); + } + + HandleDataReceived( + header->fromSmallId, + header->toSmallId, + &recvBuf[sizeof(Win64SideUdpHeader)], + static_cast(payloadSize)); + } + + return 0; +} + bool WinsockNetLayer::StartAdvertising(int gamePort, const wchar_t* hostName, unsigned int gameSettings, unsigned int texPackId, unsigned char subTexId, unsigned short netVer) { if (s_advertising) return true; diff --git a/Minecraft.Client/Windows64/Network/WinsockNetLayer.h b/Minecraft.Client/Windows64/Network/WinsockNetLayer.h index c5d68975..246ffec3 100644 --- a/Minecraft.Client/Windows64/Network/WinsockNetLayer.h +++ b/Minecraft.Client/Windows64/Network/WinsockNetLayer.h @@ -60,6 +60,15 @@ struct Win64RemoteConnection volatile bool active; }; +#pragma pack(push, 1) +struct Win64SideUdpHeader +{ + BYTE fromSmallId; + BYTE toSmallId; + WORD payloadSize; +}; +#pragma pack(pop) + class WinsockNetLayer { public: @@ -71,6 +80,7 @@ public: static bool SendToSmallId(BYTE targetSmallId, const void* data, int dataSize); static bool SendOnSocket(SOCKET sock, const void* data, int dataSize); + static bool SendToSmallIdUdp(BYTE fromSmallId, BYTE targetSmallId, const void* data, int dataSize); // Non-host split-screen: additional TCP connections to host, one per pad static bool JoinSplitScreen(int padIndex, BYTE* outSmallId); @@ -112,11 +122,25 @@ private: static DWORD WINAPI SplitScreenRecvThreadProc(LPVOID param); static DWORD WINAPI AdvertiseThreadProc(LPVOID param); static DWORD WINAPI DiscoveryThreadProc(LPVOID param); + static DWORD WINAPI SideUdpRecvThreadProc(LPVOID param); + static bool StartSideUdpHost(int port); + static bool StartSideUdpClient(const char* ip, int port); + static void StopSideUdp(); + static bool SendSideUdpPacket(const sockaddr_in &addr, BYTE fromSmallId, BYTE toSmallId, const void* data, int dataSize); static SOCKET s_listenSocket; static SOCKET s_hostConnectionSocket; static HANDLE s_acceptThread; static HANDLE s_clientRecvThread; + static SOCKET s_sideUdpSocket; + static HANDLE s_sideUdpRecvThread; + static volatile bool s_sideUdpActive; + static sockaddr_in s_hostUdpAddr; + static bool s_hasHostUdpAddr; + static sockaddr_in s_smallIdToUdpAddr[256]; + static bool s_smallIdHasUdpAddr[256]; + static CRITICAL_SECTION s_sideUdpAddrLock; + static CRITICAL_SECTION s_sideUdpSendLock; static bool s_isHost; static bool s_connected; diff --git a/Minecraft.Client/Xbox/Network/NetworkPlayerXbox.cpp b/Minecraft.Client/Xbox/Network/NetworkPlayerXbox.cpp index edadfac4..38a733b1 100644 --- a/Minecraft.Client/Xbox/Network/NetworkPlayerXbox.cpp +++ b/Minecraft.Client/Xbox/Network/NetworkPlayerXbox.cpp @@ -1,5 +1,8 @@ #include "stdafx.h" #include "NetworkPlayerXbox.h" +#ifdef _WINDOWS64 +#include "..\..\Common\Network\GameNetworkManager.h" +#endif NetworkPlayerXbox::NetworkPlayerXbox(IQNetPlayer *qnetPlayer) { @@ -17,6 +20,10 @@ void NetworkPlayerXbox::SendData(INetworkPlayer *player, const void *pvData, int DWORD flags; flags = QNET_SENDDATA_RELIABLE | QNET_SENDDATA_SEQUENTIAL; if( lowPriority ) flags |= QNET_SENDDATA_LOW_PRIORITY | QNET_SENDDATA_SECONDARY; +#ifdef _WINDOWS64 + // Win64 stub transport uses this custom flag to select reliable TCP vs side UDP. + if (ack) flags |= NON_QNET_SENDDATA_ACK_REQUIRED; +#endif m_qnetPlayer->SendData(static_cast(player)->m_qnetPlayer, pvData, dataSize, flags); } @@ -139,4 +146,4 @@ int NetworkPlayerXbox::GetTimeSinceLastChunkPacket_ms() const int64_t currentTime = System::currentTimeMillis(); return static_cast(currentTime - m_lastChunkPacketTime); -} \ No newline at end of file +} diff --git a/Minecraft.World/Connection.cpp b/Minecraft.World/Connection.cpp index d4450266..2f2ca65d 100644 --- a/Minecraft.World/Connection.cpp +++ b/Minecraft.World/Connection.cpp @@ -246,9 +246,9 @@ bool Connection::writeTick() LeaveCriticalSection(&writeLock); - // If the shouldDelay flag is still set at this point then we want to write it to QNet as a single packet with priority flags - // Otherwise just buffer the packet with other outgoing packets as the java game did - if(packet->shouldDelay) + // For delay-sensitive packets (or UDP packets), send as standalone datagrams. + // Everything else can still be buffered with the normal stream. + if(packet->shouldDelay || packet->usesUdpTransport()) { // Flush any buffered data BEFORE writing directly to the socket. // bufferedDos and sos->writeWithFlags both write to the same underlying @@ -263,10 +263,12 @@ bool Connection::writeTick() // 4J Stu - Changed this so that rather than writing to the network stream through a buffered stream we want to: // a) Only push whole "game" packets to QNet, rather than amalgamated chunks of data that may include many packets, and partial packets // b) To be able to change the priority and queue of a packet if required + int flags = 0; #ifdef _XBOX - int flags = QNET_SENDDATA_LOW_PRIORITY | QNET_SENDDATA_SECONDARY; + flags = QNET_SENDDATA_LOW_PRIORITY | QNET_SENDDATA_SECONDARY; #else - int flags = NON_QNET_SENDDATA_ACK_REQUIRED; + // If not UDP, request ACK for important delayed packets. + flags = packet->usesUdpTransport() ? 0 : NON_QNET_SENDDATA_ACK_REQUIRED; #endif sos->writeWithFlags( baos->buf, 0, baos->size(), flags ); baos->reset(); diff --git a/Minecraft.World/Packet.cpp b/Minecraft.World/Packet.cpp index 9851feb5..cc3a3bf3 100644 --- a/Minecraft.World/Packet.cpp +++ b/Minecraft.World/Packet.cpp @@ -510,6 +510,11 @@ bool Packet::isAync() return false; } +bool Packet::usesUdpTransport() +{ + return false; +} + // 4J Stu - Brought these functions forward for enchanting/game rules shared_ptr Packet::readItem(DataInputStream *dis) { diff --git a/Minecraft.World/Packet.h b/Minecraft.World/Packet.h index c639c8f1..ef12c0c2 100644 --- a/Minecraft.World/Packet.h +++ b/Minecraft.World/Packet.h @@ -98,6 +98,7 @@ public: virtual bool canBeInvalidated(); virtual bool isInvalidatedBy(shared_ptr packet); virtual bool isAync(); + virtual bool usesUdpTransport(); // 4J Stu - Brought these functions forward for enchanting/game rules static shared_ptr readItem(DataInputStream *dis); @@ -106,4 +107,4 @@ public: protected: static void writeNbt(CompoundTag *tag, DataOutputStream *dos); -}; \ No newline at end of file +}; diff --git a/Minecraft.World/Socket.cpp b/Minecraft.World/Socket.cpp index 41ac3702..b0e75b10 100644 --- a/Minecraft.World/Socket.cpp +++ b/Minecraft.World/Socket.cpp @@ -466,7 +466,7 @@ void Socket::SocketOutputStreamNetwork::write(byteArray b) void Socket::SocketOutputStreamNetwork::write(byteArray b, unsigned int offset, unsigned int length) { - writeWithFlags(b, offset, length, 0); + writeWithFlags(b, offset, length, NON_QNET_SENDDATA_ACK_REQUIRED); } void Socket::SocketOutputStreamNetwork::writeWithFlags(byteArray b, unsigned int offset, unsigned int length, int flags) @@ -549,4 +549,4 @@ void Socket::SocketOutputStreamNetwork::writeWithFlags(byteArray b, unsigned int void Socket::SocketOutputStreamNetwork::close() { m_streamOpen = false; -} \ No newline at end of file +} diff --git a/Minecraft.World/VoiceChatPacket.cpp b/Minecraft.World/VoiceChatPacket.cpp index 2599e329..27ac9da8 100644 --- a/Minecraft.World/VoiceChatPacket.cpp +++ b/Minecraft.World/VoiceChatPacket.cpp @@ -4,12 +4,12 @@ #include "VoiceChatPacket.h" VoiceChatPacket::VoiceChatPacket() - : senderPlayerId(-1), dataLength(0), audioData() + : senderPlayerId(-1), sequence(0), dataLength(0), audioData() { } -VoiceChatPacket::VoiceChatPacket(int senderPlayerId, byteArray audioData, short dataLength) - : senderPlayerId(senderPlayerId), audioData(audioData), dataLength(dataLength) +VoiceChatPacket::VoiceChatPacket(int senderPlayerId, unsigned short sequence, byteArray audioData, short dataLength) + : senderPlayerId(senderPlayerId), sequence(sequence), audioData(audioData), dataLength(dataLength) { } @@ -20,6 +20,7 @@ VoiceChatPacket::~VoiceChatPacket() void VoiceChatPacket::read(DataInputStream *dis) { senderPlayerId = dis->readInt(); + sequence = static_cast(dis->readShort()); dataLength = dis->readShort(); if (dataLength > 0 && dataLength <= 8192) { @@ -36,6 +37,7 @@ void VoiceChatPacket::read(DataInputStream *dis) void VoiceChatPacket::write(DataOutputStream *dos) { dos->writeInt(senderPlayerId); + dos->writeShort(static_cast(sequence)); dos->writeShort(dataLength); if (dataLength > 0) { @@ -50,5 +52,5 @@ void VoiceChatPacket::handle(PacketListener *listener) int VoiceChatPacket::getEstimatedSize() { - return sizeof(int) + sizeof(short) + dataLength; + return sizeof(int) + sizeof(short) + sizeof(short) + dataLength; } diff --git a/Minecraft.World/VoiceChatPacket.h b/Minecraft.World/VoiceChatPacket.h index 7a137ba2..0da70fe6 100644 --- a/Minecraft.World/VoiceChatPacket.h +++ b/Minecraft.World/VoiceChatPacket.h @@ -7,17 +7,20 @@ class VoiceChatPacket : public Packet, public enable_shared_from_this create() { return std::make_shared(); } virtual int getId() { return 251; }