From 515c22f50db79e1e26912d4c3944d43e1b3c4225 Mon Sep 17 00:00:00 2001 From: NSDeathman Date: Mon, 9 Mar 2026 18:40:13 +0300 Subject: [PATCH 01/15] Fix gamepad hot-plug detection --- .../Windows64/Windows64_Minecraft.cpp | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp index 70aeb22bf..3ab47affc 100644 --- a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp +++ b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp @@ -1806,23 +1806,16 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, InputManager.Tick(); // Detect KBM vs controller input mode - if (InputManager.IsPadConnected(0)) - { - const bool controllerUsed = InputManager.ButtonPressed(0) || - InputManager.GetJoypadStick_LX(0, false) != 0.0f || - InputManager.GetJoypadStick_LY(0, false) != 0.0f || - InputManager.GetJoypadStick_RX(0, false) != 0.0f || - InputManager.GetJoypadStick_RY(0, false) != 0.0f; + const bool controllerUsed = InputManager.ButtonPressed(0) || + InputManager.GetJoypadStick_LX(0, false) != 0.0f || + InputManager.GetJoypadStick_LY(0, false) != 0.0f || + InputManager.GetJoypadStick_RX(0, false) != 0.0f || + InputManager.GetJoypadStick_RY(0, false) != 0.0f; - if (controllerUsed) - g_KBMInput.SetKBMActive(false); - else if (g_KBMInput.HasAnyInput()) - g_KBMInput.SetKBMActive(true); - } - else - { + if (controllerUsed) + g_KBMInput.SetKBMActive(false); + else if (g_KBMInput.HasAnyInput()) g_KBMInput.SetKBMActive(true); - } if (!g_KBMInput.IsMouseGrabbed()) { From fe65ec24f199c79352f0bab971e1ad8b5c611fd9 Mon Sep 17 00:00:00 2001 From: Tuff CWC Date: Mon, 9 Mar 2026 21:02:26 +0100 Subject: [PATCH 02/15] Remove dead link from README (#1064) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 122157ba8..8ea3141ab 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ## Introduction -This project contains the source code of Minecraft Legacy Console Edition v1.6.0560.0 (TU19) from https://archive.org/details/minecraft-legacy-console-edition-source-code, with some fixes and improvements applied. +This project contains the source code of Minecraft Legacy Console Edition v1.6.0560.0 (TU19) with some fixes and improvements applied. ## Download Windows users can download our [Nightly Build](https://github.com/smartcmd/MinecraftConsoles/releases/tag/nightly)! Simply download the `.zip` file and extract it to a folder where you'd like to keep the game. You can set your username in `username.txt` (you'll have to make this file) and add servers to connect to in `servers.txt` From e9dba1fa5658b80833ab5b39b0e1028c33794c95 Mon Sep 17 00:00:00 2001 From: la <76826837+3UR@users.noreply.github.com> Date: Tue, 10 Mar 2026 07:01:17 +1000 Subject: [PATCH 03/15] Prevent TNT Minecart exploding with TNT disabled (#1067) This set of changes was made to make the code better mimic TU20 based on its disassembly --- Minecraft.World/MinecartTNT.cpp | 39 ++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/Minecraft.World/MinecartTNT.cpp b/Minecraft.World/MinecartTNT.cpp index 6e72d66b7..18681aa46 100644 --- a/Minecraft.World/MinecartTNT.cpp +++ b/Minecraft.World/MinecartTNT.cpp @@ -64,28 +64,34 @@ void MinecartTNT::destroy(DamageSource *source) double speedSqr = xd * xd + zd * zd; - if (!source->isExplosion()) + if (!app.GetGameHostOption(eGameHostOption_TNT) || !source->isExplosion()) { - spawnAtLocation(std::make_shared(Tile::tnt, 1), 0); + spawnAtLocation( shared_ptr( new ItemInstance(Tile::tnt, 1) ), 0); } - if (source->isFire() || source->isExplosion() || speedSqr >= 0.01f) + if (app.GetGameHostOption(eGameHostOption_TNT)) { - explode(speedSqr); + if (source->isFire() || source->isExplosion() || speedSqr >= 0.01f) + { + explode(speedSqr); + } } } void MinecartTNT::explode(double speedSqr) { + if (!app.GetGameHostOption(eGameHostOption_TNT)) + { + remove(); + return; + } + if (!level->isClientSide) { double speed = sqrt(speedSqr); - if (speed > 5.0) speed = 5.0; - if (app.GetGameHostOption(eGameHostOption_TNT)) - { - level->explode(shared_from_this(), x, y, z, static_cast(4 + random->nextDouble() * 1.5f * speed), true); - remove(); - } + if (speed > 5) speed = 5; + level->explode(shared_from_this(), x, y, z, (float) (4 + random->nextDouble() * 1.5f * speed), true); + remove(); } } @@ -122,12 +128,15 @@ void MinecartTNT::handleEntityEvent(byte eventId) void MinecartTNT::primeFuse() { - fuse = 80; - - if (!level->isClientSide) + if (app.GetGameHostOption(eGameHostOption_TNT)) { - level->broadcastEntityEvent(shared_from_this(), EVENT_PRIME); - level->playEntitySound(shared_from_this(), eSoundType_RANDOM_FUSE, 1, 1.0f); + fuse = 80; + + if (!level->isClientSide) + { + level->broadcastEntityEvent(shared_from_this(), EVENT_PRIME); + level->playEntitySound(shared_from_this(), eSoundType_RANDOM_FUSE, 1, 1.0f); + } } } From 0fbc8903f309b50baa92dbfaff0d6445e0274bb0 Mon Sep 17 00:00:00 2001 From: Loki Date: Mon, 9 Mar 2026 18:26:00 -0500 Subject: [PATCH 04/15] Update README, remove servers.txt references --- README.md | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 8ea3141ab..e4272e379 100644 --- a/README.md +++ b/README.md @@ -35,30 +35,11 @@ Basic LAN multiplayer is available on the Windows build - Other players on the same LAN can discover the session from the in-game Join Game menu - Game connections use TCP port `25565` by default - LAN discovery uses UDP port `25566` -- Add servers to your server list with `servers.txt` (temp solution) +- Add servers to your server list with the in-game Add Server button (temp) - Rename yourself without losing data by keeping your `uid.dat` Parts of this feature are based on code from [LCEMP](https://github.com/LCEMP/LCEMP) (thanks!) -### servers.txt - -To add a server to your game, create the `servers.txt` file in the same directory as you have `Minecraft.Client.exe`. Inside, follow this format: -``` -serverip.example.com -25565 -The name of your server in UI! -``` - -For example, here's a valid servers.txt -``` -1.1.1.1 -25565 -Cloudflare's Very Own LCE Server -127.0.0.1 -25565 -Localhost Test Crap -``` - ### Launch Arguments | Argument | Description | @@ -100,7 +81,7 @@ Minecraft.Client.exe -name Steve -fullscreen 2. Clone the repository. 3. Open the project by double-clicking `MinecraftConsoles.sln`. 4. Make sure `Minecraft.Client` is set as the Startup Project. -5. Set the build configuration to **Debug** (Release is also OK but has some bugs) and the target platform to **Windows64**, then build and run. +5. Set the build configuration to **Debug** (Release is also ok but missing some debug features) and the target platform to **Windows64**, then build and run. ### CMake (Windows x64) From e6eafda90e7827909acb75a23bc50b4b9dbf4a6f Mon Sep 17 00:00:00 2001 From: Loki Date: Mon, 9 Mar 2026 18:27:02 -0500 Subject: [PATCH 05/15] Remove another servers.txt reference --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e4272e379..0c476666e 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This project contains the source code of Minecraft Legacy Console Edition v1.6.0560.0 (TU19) with some fixes and improvements applied. ## Download -Windows users can download our [Nightly Build](https://github.com/smartcmd/MinecraftConsoles/releases/tag/nightly)! Simply download the `.zip` file and extract it to a folder where you'd like to keep the game. You can set your username in `username.txt` (you'll have to make this file) and add servers to connect to in `servers.txt` +Windows users can download our [Nightly Build](https://github.com/smartcmd/MinecraftConsoles/releases/tag/nightly)! Simply download the `.zip` file and extract it to a folder where you'd like to keep the game. You can set your username in `username.txt` (you'll have to make this file) ## Platform Support From 58c236ead5c35248a9c993c7b296e41c2799d305 Mon Sep 17 00:00:00 2001 From: Loki Rautio Date: Mon, 9 Mar 2026 20:49:50 -0500 Subject: [PATCH 06/15] Disable git LFS It was never even enabled properly to begin with --- .gitattributes | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.gitattributes b/.gitattributes index 2a377d08a..e69de29bb 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,8 +0,0 @@ -*.png filter=lfs diff=lfs merge=lfs -text -*.jpg filter=lfs diff=lfs merge=lfs -text -*.ogg filter=lfs diff=lfs merge=lfs -text -*.binka filter=lfs diff=lfs merge=lfs -text -*.arc filter=lfs diff=lfs merge=lfs -text -*.ttf filter=lfs diff=lfs merge=lfs -text -*.bin filter=lfs diff=lfs merge=lfs -text -*.ico filter=lfs diff=lfs merge=lfs -text From 3bcf588fbe041bd9d81f51348c3e98876dc9d4e2 Mon Sep 17 00:00:00 2001 From: Marlian <84173858+MCbabel@users.noreply.github.com> Date: Tue, 10 Mar 2026 04:02:39 +0100 Subject: [PATCH 07/15] Fix crash when loading saved tutorial worlds (#1001) writeRuleFile() was missing the schematic file count integer before the schematic entries. The reader in readRuleFile() expected this count, causing a stream misalignment that led to an assertion failure (Unrecognised schematic version) when reloading a saved tutorial world. The fix writes the count on save and adds backward-compatible reading that detects old saves (without count) via a peek heuristic and falls back to count-less parsing. Co-authored-by: MCbabel --- .../Common/GameRules/GameRuleManager.cpp | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/Minecraft.Client/Common/GameRules/GameRuleManager.cpp b/Minecraft.Client/Common/GameRules/GameRuleManager.cpp index ff294a650..95434c08c 100644 --- a/Minecraft.Client/Common/GameRules/GameRuleManager.cpp +++ b/Minecraft.Client/Common/GameRules/GameRuleManager.cpp @@ -344,6 +344,7 @@ void GameRuleManager::writeRuleFile(DataOutputStream *dos) // Write schematic files. unordered_map *files; files = getLevelGenerationOptions()->getUnfinishedSchematicFiles(); + dos->writeInt((int)files->size()); for ( auto& it : *files ) { const wstring& filename = it.first; @@ -497,17 +498,36 @@ bool GameRuleManager::readRuleFile(LevelGenerationOptions *lgo, byte *dIn, UINT }*/ // subfile + // Old saves didn't write a numFiles count before the schematic entries. + // Detect this: a real count is small, but a UTF filename prefix reads as a large int. UINT numFiles = contentDis->readInt(); - for (UINT i = 0; i < numFiles; i++) + + if (lgo->isFromSave() && numFiles > 100) { - wstring sFilename = contentDis->readUTF(); - int length = contentDis->readInt(); - byteArray ba( length ); - - contentDis->read(ba); - - levelGenerator->loadSchematicFile(sFilename, ba.data, ba.length); + contentDis->skip(-4); + while (true) + { + int peek = contentDis->readInt(); + if (peek <= 100) { contentDis->skip(-4); break; } + contentDis->skip(-4); + wstring sFilename = contentDis->readUTF(); + int length = contentDis->readInt(); + byteArray ba( length ); + contentDis->read(ba); + levelGenerator->loadSchematicFile(sFilename, ba.data, ba.length); + } + } + else + { + for (UINT i = 0; i < numFiles; i++) + { + wstring sFilename = contentDis->readUTF(); + int length = contentDis->readInt(); + byteArray ba( length ); + contentDis->read(ba); + levelGenerator->loadSchematicFile(sFilename, ba.data, ba.length); + } } LEVEL_GEN_ID lgoID = LEVEL_GEN_ID_NULL; From 5f777a7f45455641b9ff4bb534cadcf0f763b0a3 Mon Sep 17 00:00:00 2001 From: Alezito2008 <92759854+Alezito2008@users.noreply.github.com> Date: Tue, 10 Mar 2026 00:03:39 -0300 Subject: [PATCH 08/15] Fix: Prevent clicking disabled checkboxes (#1075) --- Minecraft.Client/Common/UI/UIScene.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Minecraft.Client/Common/UI/UIScene.cpp b/Minecraft.Client/Common/UI/UIScene.cpp index 5630e9726..0088f43d4 100644 --- a/Minecraft.Client/Common/UI/UIScene.cpp +++ b/Minecraft.Client/Common/UI/UIScene.cpp @@ -578,9 +578,12 @@ bool UIScene::handleMouseClick(F32 x, F32 y) if (bestCtrl->getControlType() == UIControl::eCheckBox) { UIControl_CheckBox *cb = static_cast(bestCtrl); - bool newState = !cb->IsChecked(); - cb->setChecked(newState); - handleCheckboxToggled((F64)bestId, newState); + if (cb->IsEnabled()) + { + bool newState = !cb->IsChecked(); + cb->setChecked(newState); + handleCheckboxToggled((F64)bestId, newState); + } } else { From c90a6bf5ab4d1d96ac510f7357c9618c2bf4bc77 Mon Sep 17 00:00:00 2001 From: "Us3ful\"-Dev" Date: Tue, 10 Mar 2026 04:05:56 +0100 Subject: [PATCH 09/15] Fixed Enderman, monster aggro in creative (#1051) Fixed endermans by making a invulnerable check --- Minecraft.World/EnderMan.cpp | 7 ++++++- Minecraft.World/Monster.cpp | 9 ++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Minecraft.World/EnderMan.cpp b/Minecraft.World/EnderMan.cpp index e12318187..da469a3a2 100644 --- a/Minecraft.World/EnderMan.cpp +++ b/Minecraft.World/EnderMan.cpp @@ -410,9 +410,14 @@ bool EnderMan::hurt(DamageSource *source, float damage) if ( dynamic_cast(source) != nullptr && source->getEntity()->instanceof(eTYPE_PLAYER)) { - aggroedByPlayer = true; + if (!dynamic_pointer_cast(source->getEntity())->abilities.invulnerable) + { + aggroedByPlayer = true; + } + else setCreepy(false); } + if (dynamic_cast(source) != nullptr) { aggroedByPlayer = false; diff --git a/Minecraft.World/Monster.cpp b/Minecraft.World/Monster.cpp index 2de453153..ad19a36d4 100644 --- a/Minecraft.World/Monster.cpp +++ b/Minecraft.World/Monster.cpp @@ -60,7 +60,14 @@ bool Monster::hurt(DamageSource *source, float dmg) if (sourceEntity != shared_from_this()) { - attackTarget = sourceEntity; + if (sourceEntity->instanceof(eTYPE_PLAYER)) + { + if (!dynamic_pointer_cast(sourceEntity)->abilities.invulnerable) + { + attackTarget = sourceEntity; + } + } + else attackTarget = sourceEntity; } return true; } From 91ae76f132678b010dabd30dbd87abf2087dd929 Mon Sep 17 00:00:00 2001 From: Marlian <84173858+MCbabel@users.noreply.github.com> Date: Tue, 10 Mar 2026 04:06:38 +0100 Subject: [PATCH 10/15] Fix tamed horses despawning when player moves away (#1057) Co-authored-by: MCbabel --- Minecraft.World/EntityHorse.cpp | 9 +++++++++ Minecraft.World/EntityHorse.h | 1 + 2 files changed, 10 insertions(+) diff --git a/Minecraft.World/EntityHorse.cpp b/Minecraft.World/EntityHorse.cpp index 32c9eb2de..0bf5bc080 100644 --- a/Minecraft.World/EntityHorse.cpp +++ b/Minecraft.World/EntityHorse.cpp @@ -515,6 +515,15 @@ bool EntityHorse::canSpawn() return Animal::canSpawn(); } +bool EntityHorse::removeWhenFarAway() +{ + if (isTamed()) return false; + if (isSaddled()) return false; + if (isLeashed()) return false; + if (getArmorType() > 0) return false; + return Animal::removeWhenFarAway(); +} + shared_ptr EntityHorse::getClosestMommy(shared_ptr baby, double searchRadius) { diff --git a/Minecraft.World/EntityHorse.h b/Minecraft.World/EntityHorse.h index c2784491a..c91da172a 100644 --- a/Minecraft.World/EntityHorse.h +++ b/Minecraft.World/EntityHorse.h @@ -192,6 +192,7 @@ private: public: virtual void containerChanged(); virtual bool canSpawn(); + virtual bool removeWhenFarAway() override; protected: virtual shared_ptr getClosestMommy(shared_ptr baby, double searchRadius); From c998346312c753fa4359804c2c1823e23bce5b91 Mon Sep 17 00:00:00 2001 From: eh-K <165239265+eh-K@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:07:38 -0500 Subject: [PATCH 11/15] FIX: Bonus Chests spawn again when loading back in. #982 (#992) * Fixed bug where Bonus Chests would spawn again when loading back into it. Fixes #982 Added a check for if the world is new. Meaning no more additional chests if the world is loaded up again. * Replace NULL with nullptr for chest check --- Minecraft.World/BonusChestFeature.cpp | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/Minecraft.World/BonusChestFeature.cpp b/Minecraft.World/BonusChestFeature.cpp index cd33e60ae..86dc97258 100644 --- a/Minecraft.World/BonusChestFeature.cpp +++ b/Minecraft.World/BonusChestFeature.cpp @@ -24,18 +24,21 @@ bool BonusChestFeature::place(Level *level, Random *random, int x, int y, int z) bool BonusChestFeature::place(Level *level, Random *random, int x, int y, int z, bool force) { - if( !force ) + //Will only spawn a bonus chest if the world is new and has never been saved. + if (level->isNew) { - int t = 0; - while (((t = level->getTile(x, y, z)) == 0 || t == Tile::leaves_Id) && y > 1) + if( !force ) + { + int t = 0; + while (((t = level->getTile(x, y, z)) == 0 || t == Tile::leaves_Id) && y > 1) y--; - if (y < 1) - { - return false; + if (y < 1) + { + return false; + } + y++; } - y++; - } for (int i = 0; i < 4; i++) { @@ -85,4 +88,6 @@ bool BonusChestFeature::place(Level *level, Random *random, int x, int y, int z, } return false; + + } } From d7596aa28c0ff7773e5a64155e54f2821517c591 Mon Sep 17 00:00:00 2001 From: la <76826837+3UR@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:30:01 +1000 Subject: [PATCH 12/15] Fix issue where visually the HUD shows you have 0 hearts when you dont (#1089) * Fix for issue where player is able to stay alive with zero hearts in their healthbar. * use static cast over c style cast --- Minecraft.Client/Common/UI/IUIScene_HUD.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Minecraft.Client/Common/UI/IUIScene_HUD.cpp b/Minecraft.Client/Common/UI/IUIScene_HUD.cpp index fd9779665..d2754789c 100644 --- a/Minecraft.Client/Common/UI/IUIScene_HUD.cpp +++ b/Minecraft.Client/Common/UI/IUIScene_HUD.cpp @@ -195,8 +195,8 @@ void IUIScene_HUD::renderPlayerHealth() // Update health bool blink = pMinecraft->localplayers[iPad]->invulnerableTime / 3 % 2 == 1; if (pMinecraft->localplayers[iPad]->invulnerableTime < 10) blink = false; - int currentHealth = pMinecraft->localplayers[iPad]->getHealth(); - int oldHealth = pMinecraft->localplayers[iPad]->lastHealth; + int currentHealth = static_cast(ceil(pMinecraft->localplayers[iPad]->getHealth())); + int oldHealth = static_cast(ceil(pMinecraft->localplayers[iPad]->lastHealth)); bool bHasPoison = pMinecraft->localplayers[iPad]->hasEffect(MobEffect::poison); bool bHasWither = pMinecraft->localplayers[iPad]->hasEffect(MobEffect::wither); AttributeInstance *maxHealthAttribute = pMinecraft->localplayers[iPad]->getAttribute(SharedMonsterAttributes::MAX_HEALTH); From a195ac7172642196a244260291f966ed7cea1edc Mon Sep 17 00:00:00 2001 From: Loki Date: Tue, 10 Mar 2026 09:37:52 -0500 Subject: [PATCH 13/15] Only single topic PRs, please - CONTRIBUTING.md --- CONTRIBUTING.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c75279d7d..855491da1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,6 +46,13 @@ However, we would accept changes that... - Having workable multi-platform compilation for ARM, Consoles, Linux - Being a good base for further expansion and modding of LCE, such as backports and "modpacks". +# Scope of PRs +All Pull Requests should fully document the changes they include in their file changes. They should also be limited to one general topic and not touch all over the codebase unless its justifiable. + +For example, we would not accept a PR that reworks UI, multiplayer code, and furnace ticking even if its a "fixup" PR as its too difficult to review a ton of code changes that are all irrelevant from each other. However, a PR focused on adding a bunch of commands or fixes several crashes that are otherwise irrelevant to each other would be accepted. + +If your PR includes any undocumented changes it will be closed. + # Use of AI and LLMs We currently do not accept any new code into the project that was written largely, entirely, or even noticably by an LLM. All contributions should be made by humans that understand the codebase. From 1036b7368e5625d34666beb24163756c1da6b227 Mon Sep 17 00:00:00 2001 From: Marvelco Date: Wed, 11 Mar 2026 05:04:19 +0200 Subject: [PATCH 14/15] Fixed DLC map loading / saving, missing chunks (#1114) * fixed all DLC maps * fixed old saves have overlapping chunks with the new system --- .../GameRules/LevelGenerationOptions.cpp | 69 +++++++++++++++++++ .../Common/Network/GameNetworkManager.cpp | 7 +- Minecraft.Client/MinecraftServer.cpp | 6 +- Minecraft.World/RegionFileCache.cpp | 5 ++ 4 files changed, 85 insertions(+), 2 deletions(-) diff --git a/Minecraft.Client/Common/GameRules/LevelGenerationOptions.cpp b/Minecraft.Client/Common/GameRules/LevelGenerationOptions.cpp index 2f121f4f9..2af1826cf 100644 --- a/Minecraft.Client/Common/GameRules/LevelGenerationOptions.cpp +++ b/Minecraft.Client/Common/GameRules/LevelGenerationOptions.cpp @@ -455,6 +455,74 @@ unordered_map *LevelGenerationOptions::getUnfin void LevelGenerationOptions::loadBaseSaveData() { +#ifdef _WINDOWS64 + + int gameRulesCount = m_parentDLCPack ? m_parentDLCPack->getDLCItemsCount(DLCManager::e_DLCType_GameRulesHeader) : 0; + + wstring baseSave = getBaseSavePath(); + wstring packName = baseSave.substr(0, baseSave.find(L'.')); + + for (int i = 0; i < gameRulesCount; ++i) + { + DLCGameRulesHeader* dlcFile = static_cast(m_parentDLCPack->getFile(DLCManager::e_DLCType_GameRulesHeader, i)); + + if (!dlcFile->getGrfPath().empty()) + { + File grf(L"Windows64Media\\DLC\\" + packName + L"\\Data\\" + dlcFile->getGrfPath()); + + if (grf.exists()) + { + wstring path = grf.getPath(); + HANDLE fileHandle = CreateFileW(path.c_str(), GENERIC_READ, 0, nullptr, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, nullptr); + + if (fileHandle != INVALID_HANDLE_VALUE) + { + DWORD dwFileSize = grf.length(); + DWORD bytesRead; + PBYTE pbData = new BYTE[dwFileSize]; + BOOL bSuccess = ReadFile(fileHandle, pbData, dwFileSize, &bytesRead, nullptr); + CloseHandle(fileHandle); + + if (bSuccess) + { + dlcFile->setGrfData(pbData, dwFileSize, m_stringTable); + app.m_gameRules.setLevelGenerationOptions(dlcFile->lgo); + } + delete[] pbData; + } + } + } + } + + if (requiresBaseSave() && !getBaseSavePath().empty()) + { + File save(L"Windows64Media\\DLC\\" + packName + L"\\Data\\" + baseSave); + + if (save.exists()) + { + wstring path = save.getPath(); + HANDLE fileHandle = CreateFileW(path.c_str(), GENERIC_READ, 0, nullptr, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, nullptr); + + if (fileHandle != INVALID_HANDLE_VALUE) + { + DWORD dwFileSize = GetFileSize(fileHandle, nullptr); + DWORD bytesRead; + PBYTE pbData = new BYTE[dwFileSize]; + BOOL bSuccess = ReadFile(fileHandle, pbData, dwFileSize, &bytesRead, nullptr); + CloseHandle(fileHandle); + + if (bSuccess) + setBaseSaveData(pbData, dwFileSize); + else + delete[] pbData; + } + } + } + + setLoadedData(); + app.SetAction(ProfileManager.GetPrimaryPad(), eAppAction_ReloadTexturePack); + +#else int mountIndex = -1; if(m_parentDLCPack != nullptr) mountIndex = m_parentDLCPack->GetDLCMountIndex(); @@ -481,6 +549,7 @@ void LevelGenerationOptions::loadBaseSaveData() setLoadedData(); app.SetAction(ProfileManager.GetPrimaryPad(), eAppAction_ReloadTexturePack); } +#endif } int LevelGenerationOptions::packMounted(LPVOID pParam,int iPad,DWORD dwErr,DWORD dwLicenceMask) diff --git a/Minecraft.Client/Common/Network/GameNetworkManager.cpp b/Minecraft.Client/Common/Network/GameNetworkManager.cpp index 3c032bf97..a502dbfb7 100644 --- a/Minecraft.Client/Common/Network/GameNetworkManager.cpp +++ b/Minecraft.Client/Common/Network/GameNetworkManager.cpp @@ -942,13 +942,18 @@ int CGameNetworkManager::ServerThreadProc( void* lpParameter ) app.SetGameHostOption(eGameHostOption_All,param->settings); // 4J Stu - If we are loading a DLC save that's separate from the texture pack, load - if( param->levelGen != nullptr && (param->texturePackId == 0 || param->levelGen->getRequiredTexturePackId() != param->texturePackId) ) + if (param != nullptr && param->levelGen != nullptr && param->levelGen->isFromDLC()) { while((Minecraft::GetInstance()->skins->needsUIUpdate() || ui.IsReloadingSkin())) { Sleep(1); } param->levelGen->loadBaseSaveData(); + + while (!param->levelGen->hasLoadedData()) + { + Sleep(1); + } } } diff --git a/Minecraft.Client/MinecraftServer.cpp b/Minecraft.Client/MinecraftServer.cpp index bdcc9f813..2cf6930ac 100644 --- a/Minecraft.Client/MinecraftServer.cpp +++ b/Minecraft.Client/MinecraftServer.cpp @@ -937,7 +937,11 @@ bool MinecraftServer::loadLevel(LevelStorageSource *storageSource, const wstring storage = shared_ptr(new McRegionLevelStorage(newFormatSave, File(L"."), name, true)); #else - storage = std::make_shared(new ConsoleSaveFileOriginal(L""), File(L"."), name, true); + ConsoleSaveFileOriginal* pSave = new ConsoleSaveFileOriginal(L""); + + pSave->ConvertToLocalPlatform(); + storage = std::make_shared(pSave, File(L"."), name, true); + #endif } diff --git a/Minecraft.World/RegionFileCache.cpp b/Minecraft.World/RegionFileCache.cpp index ab0492b48..bd6405fa3 100644 --- a/Minecraft.World/RegionFileCache.cpp +++ b/Minecraft.World/RegionFileCache.cpp @@ -12,6 +12,11 @@ bool RegionFileCache::useSplitSaves(ESavePlatform platform) case SAVE_FILE_PLATFORM_XBONE: case SAVE_FILE_PLATFORM_PS4: return true; + case SAVE_FILE_PLATFORM_WIN64: + { + LevelGenerationOptions* lgo = app.getLevelGenerationOptions(); + return (lgo != nullptr && lgo->isFromDLC()); + } default: return false; }; From dca3bf2bc3d37a1dd2e8c2fab4c89ce462bda4df Mon Sep 17 00:00:00 2001 From: NSDeathman Date: Wed, 11 Mar 2026 13:27:47 +0300 Subject: [PATCH 15/15] Fixed and completely refactored game music playing and categories choosing --- .../Common/Audio/Consoles_SoundEngine.cpp | 8 + .../Common/Audio/Consoles_SoundEngine.h | 3 + .../Common/Audio/MusicTrackManager.cpp | 97 ++ .../Common/Audio/MusicTrackManager.h | 117 ++ Minecraft.Client/Common/Audio/SoundEngine.cpp | 1153 ++++++++--------- Minecraft.Client/Common/Audio/SoundEngine.h | 30 +- Minecraft.Client/Common/Consoles_App.cpp | 10 +- Minecraft.Client/Common/UI/UIController.cpp | 10 +- Minecraft.Client/DLCTexturePack.cpp | 2 +- 9 files changed, 772 insertions(+), 658 deletions(-) create mode 100644 Minecraft.Client/Common/Audio/MusicTrackManager.cpp create mode 100644 Minecraft.Client/Common/Audio/MusicTrackManager.h diff --git a/Minecraft.Client/Common/Audio/Consoles_SoundEngine.cpp b/Minecraft.Client/Common/Audio/Consoles_SoundEngine.cpp index 54e9a1ef4..c75effc83 100644 --- a/Minecraft.Client/Common/Audio/Consoles_SoundEngine.cpp +++ b/Minecraft.Client/Common/Audio/Consoles_SoundEngine.cpp @@ -18,6 +18,10 @@ void ConsoleSoundEngine::SetIsPlayingStreamingGameMusic(bool bVal) { m_bIsPlayingStreamingGameMusic=bVal; } +bool ConsoleSoundEngine::GetIsPlayingMenuMusic() +{ + return m_bIsPlayingMenuMusic; +} bool ConsoleSoundEngine::GetIsPlayingEndMusic() { return m_bIsPlayingEndMusic; @@ -26,6 +30,10 @@ bool ConsoleSoundEngine::GetIsPlayingNetherMusic() { return m_bIsPlayingNetherMusic; } +void ConsoleSoundEngine::SetIsPlayingMenuMusic(bool bVal) +{ + m_bIsPlayingMenuMusic = bVal; +} void ConsoleSoundEngine::SetIsPlayingEndMusic(bool bVal) { m_bIsPlayingEndMusic=bVal; diff --git a/Minecraft.Client/Common/Audio/Consoles_SoundEngine.h b/Minecraft.Client/Common/Audio/Consoles_SoundEngine.h index 9a1ae3723..5adeafd44 100644 --- a/Minecraft.Client/Common/Audio/Consoles_SoundEngine.h +++ b/Minecraft.Client/Common/Audio/Consoles_SoundEngine.h @@ -61,9 +61,11 @@ public: virtual bool GetIsPlayingStreamingGameMusic() ; virtual void SetIsPlayingStreamingCDMusic(bool bVal) ; virtual void SetIsPlayingStreamingGameMusic(bool bVal) ; + virtual bool GetIsPlayingMenuMusic(); virtual bool GetIsPlayingEndMusic() ; virtual bool GetIsPlayingNetherMusic() ; virtual void SetIsPlayingEndMusic(bool bVal) ; + virtual void SetIsPlayingMenuMusic(bool bVal); virtual void SetIsPlayingNetherMusic(bool bVal) ; static const WCHAR *wchSoundNames[eSoundType_MAX]; static const WCHAR *wchUISoundNames[eSFX_MAX]; @@ -94,6 +96,7 @@ private: bool m_bIsPlayingStreamingCDMusic; bool m_bIsPlayingStreamingGameMusic; + bool m_bIsPlayingMenuMusic; bool m_bIsPlayingEndMusic; bool m_bIsPlayingNetherMusic; }; \ No newline at end of file diff --git a/Minecraft.Client/Common/Audio/MusicTrackManager.cpp b/Minecraft.Client/Common/Audio/MusicTrackManager.cpp new file mode 100644 index 000000000..7fbe2a55e --- /dev/null +++ b/Minecraft.Client/Common/Audio/MusicTrackManager.cpp @@ -0,0 +1,97 @@ +#include "stdafx.h" +#include "MusicTrackManager.h" +#include "../Minecraft.World/Random.h" + +MusicTrackManager::MusicTrackManager(Random* rng) + : m_random(rng) +{ + // The domains will be filled later via setDomainRange(). +} + +MusicTrackManager::~MusicTrackManager() +{ + // unordered_map will automatically destroy its DomainInfo objects, + // which in turn delete[] the heard arrays. +} + +void MusicTrackManager::setDomainRange(Domain domain, int minIdx, int maxIdx) +{ + assert(minIdx <= maxIdx); + // Use emplace to construct the DomainInfo in-place. + // If the domain already exists, this will replace it (C++17). + m_domains.erase(domain); // Remove old entry if any + m_domains.emplace(domain, DomainInfo(minIdx, maxIdx)); +} + +int MusicTrackManager::selectTrack(Domain domain) +{ + // Special case: if domain is None, return -1 (no track). + if (domain == Domain::None) + return -1; + + DomainInfo& info = getInfo(domain); + + // If range contains only one track, just return that track. + if (info.trackCount == 1) + return info.minIdx; + + // Check whether all tracks have been heard. + bool allHeard = true; + for (int i = 0; i < info.trackCount; ++i) + { + if (!info.heard[i]) + { + allHeard = false; + break; + } + } + + // If all tracks have been heard, reset the heard flags. + if (allHeard) + { + std::memset(info.heard, 0, sizeof(bool) * info.trackCount); + } + + // Try up to (trackCount/2 + 1) times to pick a track that hasn't been heard. + // This biases toward unplayed tracks but doesn't guarantee it if the random + // keeps hitting played ones. It's a compromise between fairness and performance. + const int maxAttempts = info.trackCount / 2 + 1; + for (int attempt = 0; attempt < maxAttempts; ++attempt) + { + int idx = m_random->nextInt(info.trackCount); // 0 .. trackCount-1 + if (!info.heard[idx]) + { + info.heard[idx] = true; + return info.minIdx + idx; + } + } + + // Fallback: if we couldn't find an unplayed track (should be rare), + // just pick any random track and mark it as heard. + int fallbackIdx = m_random->nextInt(info.trackCount); + info.heard[fallbackIdx] = true; + return info.minIdx + fallbackIdx; +} + +void MusicTrackManager::resetDomain(Domain domain) +{ + DomainInfo& info = getInfo(domain); + std::memset(info.heard, 0, sizeof(bool) * info.trackCount); +} + +// ---------- private helpers ---------- + +MusicTrackManager::DomainInfo& MusicTrackManager::getInfo(Domain domain) +{ + auto it = m_domains.find(domain); + assert(it != m_domains.end() && "Domain not initialized. Call setDomainRange first."); + return it->second; +} + +const MusicTrackManager::DomainInfo& MusicTrackManager::getInfo(Domain domain) const +{ + auto it = m_domains.find(domain); + assert(it != m_domains.end() && "Domain not initialized."); + return it->second; +} + diff --git a/Minecraft.Client/Common/Audio/MusicTrackManager.h b/Minecraft.Client/Common/Audio/MusicTrackManager.h new file mode 100644 index 000000000..a253ded60 --- /dev/null +++ b/Minecraft.Client/Common/Audio/MusicTrackManager.h @@ -0,0 +1,117 @@ +#pragma once + +#include +#include // for memset +#include + +// Forward declaration of the random number generator used by SoundEngine. +// Replace with your actual random class if different. +class Random; + +/** + * Manages selection of music tracks across different gameplay domains + * (Menu, Overworld Survival, Overworld Creative, Nether, End). + * Each domain maintains its own range of track indices and a history + * of recently played tracks to avoid immediate repetition. + */ +class MusicTrackManager +{ +public: + // Enumeration of all music domains. The values match the original + // SoundEngine constants for easy conversion. + enum class Domain + { + Menu = -1, + OverworldSurvival = 0, + OverworldCreative = 1, + Nether = 2, + End = 3, + None = 4 // Special value for when no background music should play (e.g., during a music disc) + }; + + /** + * Constructor. + * @param rng Pointer to a random number generator (must remain valid). + */ + explicit MusicTrackManager(Random* rng); + + /** + * Destructor – frees all dynamically allocated heard-track arrays. + */ + ~MusicTrackManager(); + + // Prevent copying (to avoid double deletion of internal arrays). + MusicTrackManager(const MusicTrackManager&) = delete; + MusicTrackManager& operator=(const MusicTrackManager&) = delete; + + /** + * Sets the inclusive index range for a given domain. + * @param domain The music domain. + * @param minIdx First valid track index. + * @param maxIdx Last valid track index (must be >= minIdx). + */ + void setDomainRange(Domain domain, int minIdx, int maxIdx); + + /** + * Selects a track index for the specified domain. + * Tries to return a track that has not been played recently; + * if all tracks have been played, resets the history and picks one randomly. + * For domains with only one track, that track is always returned. + * @param domain The domain for which to choose a track. + * @return A valid track index within the domain's range. + */ + int selectTrack(Domain domain); + + /** + * Resets the heard history for a domain, marking all tracks as "not heard". + * Useful when switching domains or after a major game state change. + * @param domain The domain to reset. + */ + void resetDomain(Domain domain); + + int getDomainMin(Domain d) const { return getInfo(d).minIdx; } + int getDomainMax(Domain d) const { return getInfo(d).maxIdx; } + +private: + // Internal structure storing range and heard-array for a domain. + struct DomainInfo + { + int minIdx; // First track index (inclusive) + int maxIdx; // Last track index (inclusive) + int trackCount; // Number of tracks in this domain + bool* heard; // Dynamic array: heard[i] == true if track (minIdx + i) has been played recently + + DomainInfo(int minVal = 0, int maxVal = 0) + : minIdx(minVal), maxIdx(maxVal), trackCount(maxVal - minVal + 1) + { + heard = new bool[trackCount](); // value-initialized to false + } + + // Move constructor (optional, but needed if we want to store in unordered_map with emplace) + DomainInfo(DomainInfo&& other) noexcept + : minIdx(other.minIdx), maxIdx(other.maxIdx), trackCount(other.trackCount), heard(other.heard) + { + other.heard = nullptr; // prevent double deletion + } + + // Destructor + ~DomainInfo() + { + delete[] heard; + } + + // No copy + DomainInfo(const DomainInfo&) = delete; + DomainInfo& operator=(const DomainInfo&) = delete; + }; + + // Map from Domain to its info. + std::unordered_map m_domains; + + // Pointer to the random number generator. + Random* m_random; + + // Helper to get the info for a domain (assumes domain exists). + DomainInfo& getInfo(Domain domain); + const DomainInfo& getInfo(Domain domain) const; +}; diff --git a/Minecraft.Client/Common/Audio/SoundEngine.cpp b/Minecraft.Client/Common/Audio/SoundEngine.cpp index 24cb7bf45..989eaf238 100644 --- a/Minecraft.Client/Common/Audio/SoundEngine.cpp +++ b/Minecraft.Client/Common/Audio/SoundEngine.cpp @@ -114,6 +114,9 @@ const char *SoundEngine::m_szStreamFileA[eStream_Max]= "hal4", "nuance1", "nuance2", + "piano1", + "piano2", + "piano3", // 11 #ifndef _XBOX "creative1", @@ -121,26 +124,23 @@ const char *SoundEngine::m_szStreamFileA[eStream_Max]= "creative3", "creative4", "creative5", - "creative6", + "creative6", // 17 + "menu1", "menu2", "menu3", - "menu4", + "menu4", // 21 #endif - "piano1", - "piano2", - "piano3", - // Nether "nether1", "nether2", "nether3", - "nether4", + "nether4", // 25 // The End "the_end_dragon_alive", - "the_end_end", + "the_end_end", // 27 // CDs "11", @@ -191,23 +191,22 @@ void SoundEngine::init(Options* pOptions) return; } -void SoundEngine::SetStreamingSounds(int iOverworldMin, int iOverWorldMax, int iNetherMin, int iNetherMax, int iEndMin, int iEndMax, int iCD1) +void SoundEngine::SetStreamingSounds(int iMenuMin, int iMenuMax, + int iOverworldSurvivalMin, int iOverWorldSurvivalMax, + int iOverworldCreativeMin, int iOverWorldCreativeMax, + int iNetherMin, int iNetherMax, + int iEndMin, int iEndMax, + int iCD1) { - m_iStream_Overworld_Min=iOverworldMin; - m_iStream_Overworld_Max=iOverWorldMax; - m_iStream_Nether_Min=iNetherMin; - m_iStream_Nether_Max=iNetherMax; - m_iStream_End_Min=iEndMin; - m_iStream_End_Max=iEndMax; - m_iStream_CD_1=iCD1; + using Domain = MusicTrackManager::Domain; - // array to monitor recently played tracks - if(m_bHeardTrackA) - { - delete [] m_bHeardTrackA; - } - m_bHeardTrackA = new bool[iEndMax+1]; - memset(m_bHeardTrackA,0,sizeof(bool)*iEndMax+1); + m_musicTrackManager.setDomainRange(Domain::Menu, iMenuMin, iMenuMax); + m_musicTrackManager.setDomainRange(Domain::OverworldSurvival, iOverworldSurvivalMin, iOverWorldSurvivalMax); + m_musicTrackManager.setDomainRange(Domain::OverworldCreative, iOverworldCreativeMin, iOverWorldCreativeMax); + m_musicTrackManager.setDomainRange(Domain::Nether, iNetherMin, iNetherMax); + m_musicTrackManager.setDomainRange(Domain::End, iEndMin, iEndMax); + + m_iStream_CD_1 = iCD1; } void SoundEngine::updateMiniAudio() @@ -391,9 +390,8 @@ void SoundEngine::tick(shared_ptr *players, float a) // SoundEngine // ///////////////////////////////////////////// -SoundEngine::SoundEngine() +SoundEngine::SoundEngine(): random(new Random()), m_musicTrackManager(random), m_currentMusicDomain(MusicTrackManager::Domain::Menu) { - random = new Random(); memset(&m_engine, 0, sizeof(ma_engine)); memset(&m_engineConfig, 0, sizeof(ma_engine_config)); m_musicStreamActive = false; @@ -401,15 +399,29 @@ SoundEngine::SoundEngine() m_iMusicDelay=0; m_validListenerCount=0; - m_bHeardTrackA=nullptr; + m_musicTrackManager.setDomainRange(MusicTrackManager::Domain::Menu, + eStream_Overworld_Menu1, + eStream_Overworld_Menu4); - // Start the streaming music playing some music from the overworld - SetStreamingSounds(eStream_Overworld_Calm1,eStream_Overworld_piano3, - eStream_Nether1,eStream_Nether4, - eStream_end_dragon,eStream_end_end, - eStream_CD_1); + m_musicTrackManager.setDomainRange(MusicTrackManager::Domain::OverworldSurvival, + eStream_Overworld_Calm1, + eStream_Overworld_piano3); - m_musicID=getMusicID(LevelData::DIMENSION_OVERWORLD); + m_musicTrackManager.setDomainRange(MusicTrackManager::Domain::OverworldCreative, + eStream_Overworld_Creative1, + eStream_Overworld_Creative6); + + m_musicTrackManager.setDomainRange(MusicTrackManager::Domain::Nether, + eStream_Nether1, + eStream_Nether4); + + m_musicTrackManager.setDomainRange(MusicTrackManager::Domain::End, + eStream_end_dragon, + eStream_end_end); + + m_iStream_CD_1 = eStream_CD_1; + + m_musicID = getTrackForDomain(MusicTrackManager::Domain::Menu); m_StreamingAudioInfo.bIs3D=false; m_StreamingAudioInfo.x=0; @@ -660,176 +672,80 @@ void SoundEngine::playUI(int iSound, float volume, float pitch) // playStreaming // ///////////////////////////////////////////// -void SoundEngine::playStreaming(const wstring& name, float x, float y , float z, float volume, float pitch, bool bMusicDelay) +MusicTrackManager::Domain SoundEngine::determineCurrentMusicDomain() const { - // This function doesn't actually play a streaming sound, just sets states and an id for the music tick to play it - // Level audio will be played when a play with an empty name comes in - // CD audio will be played when a named stream comes in + Minecraft* mc = Minecraft::GetInstance(); + if (!mc || !mc->level) + return MusicTrackManager::Domain::Menu; - m_StreamingAudioInfo.x=x; - m_StreamingAudioInfo.y=y; - m_StreamingAudioInfo.z=z; - m_StreamingAudioInfo.volume=volume; - m_StreamingAudioInfo.pitch=pitch; + bool inEnd = false, inNether = false, creative = false; - if(m_StreamState==eMusicStreamState_Playing) + for (unsigned int i = 0; i < MAX_LOCAL_PLAYERS; ++i) { - m_StreamState=eMusicStreamState_Stop; - } - else if(m_StreamState==eMusicStreamState_Opening) - { - m_StreamState=eMusicStreamState_OpeningCancel; + auto player = mc->localplayers[i]; + if (!player) continue; + + if (player->dimension == LevelData::DIMENSION_END) + inEnd = true; + else if (player->dimension == LevelData::DIMENSION_NETHER) + inNether = true; + + if (player->level->getLevelData()->getGameType()->isCreative()) + creative = true; } - if(name.empty()) - { - // music, or stop CD - m_StreamingAudioInfo.bIs3D=false; + if (inEnd) return MusicTrackManager::Domain::End; + if (inNether) return MusicTrackManager::Domain::Nether; + if (creative) return MusicTrackManager::Domain::OverworldCreative; + return MusicTrackManager::Domain::OverworldSurvival; +} - // we need a music id - // random delay of up to 3 minutes for music - m_iMusicDelay = random->nextInt(20 * 60 * 3);//random->nextInt(20 * 60 * 10) + 20 * 60 * 10; +void SoundEngine::playStreaming(const wstring& name, float x, float y, float z, float volume, float pitch, bool bMusicDelay) +{ + m_StreamingAudioInfo.x = x; + m_StreamingAudioInfo.y = y; + m_StreamingAudioInfo.z = z; + m_StreamingAudioInfo.volume = volume; + m_StreamingAudioInfo.pitch = pitch; + + if (m_StreamState == eMusicStreamState_Playing) + m_StreamState = eMusicStreamState_Stop; + else if (m_StreamState == eMusicStreamState_Opening) + m_StreamState = eMusicStreamState_OpeningCancel; + + if (name.empty()) + { + m_StreamingAudioInfo.bIs3D = false; + m_iMusicDelay = bMusicDelay ? random->nextInt(20 * 60 * 3) : 0; #ifdef _DEBUG - m_iMusicDelay=0; + m_iMusicDelay = 0; #endif - Minecraft *pMinecraft=Minecraft::GetInstance(); - bool playerInEnd=false; - bool playerInNether=false; - - for(unsigned int i=0;ilocalplayers[i]!=nullptr) - { - if(pMinecraft->localplayers[i]->dimension==LevelData::DIMENSION_END) - { - playerInEnd=true; - } - else if(pMinecraft->localplayers[i]->dimension==LevelData::DIMENSION_NETHER) - { - playerInNether=true; - } - } - } - if(playerInEnd) - { - m_musicID = getMusicID(LevelData::DIMENSION_END); - } - else if(playerInNether) - { - m_musicID = getMusicID(LevelData::DIMENSION_NETHER); - } - else - { - m_musicID = getMusicID(LevelData::DIMENSION_OVERWORLD); - } + MusicTrackManager::Domain domain = determineCurrentMusicDomain(); + m_currentMusicDomain = domain; + m_musicID = getTrackForDomain(domain); } else { - // jukebox - m_StreamingAudioInfo.bIs3D=true; - m_musicID=getMusicID(name); - m_iMusicDelay=0; + // Disk (jukebox) + m_StreamingAudioInfo.bIs3D = true; + m_musicID = getMusicID(name); + m_iMusicDelay = 0; } } - -int SoundEngine::GetRandomishTrack(int iStart,int iEnd) -{ - // 4J-PB - make it more likely that we'll get a track we've not heard for a while, although repeating tracks sometimes is fine - - // if all tracks have been heard, clear the flags - bool bAllTracksHeard=true; - int iVal=iStart; - for(size_t i=iStart;i<=iEnd;i++) - { - if(m_bHeardTrackA[i]==false) - { - bAllTracksHeard=false; - app.DebugPrintf("Not heard all tracks yet\n"); - break; - } - } - - if(bAllTracksHeard) - { - app.DebugPrintf("Heard all tracks - resetting the tracking array\n"); - - for(size_t i=iStart;i<=iEnd;i++) - { - m_bHeardTrackA[i]=false; - } - } - - // trying to get a track we haven't heard, but not too hard - for(size_t i=0;i<=((iEnd-iStart)/2);i++) - { - // random->nextInt(1) will always return 0 - iVal=random->nextInt((iEnd-iStart)+1)+iStart; - if(m_bHeardTrackA[iVal]==false) - { - // not heard this - app.DebugPrintf("(%d) Not heard track %d yet, so playing it now\n",i,iVal); - m_bHeardTrackA[iVal]=true; - break; - } - else - { - app.DebugPrintf("(%d) Skipping track %d already heard it recently\n",i,iVal); - } - } - - app.DebugPrintf("Select track %d\n",iVal); - return iVal; -} ///////////////////////////////////////////// // -// getMusicID +// getTrackForDomain(MusicTrackManager::Domain domain) // ///////////////////////////////////////////// -int SoundEngine::getMusicID(int iDomain) +int SoundEngine::getTrackForDomain(MusicTrackManager::Domain domain) { - int iRandomVal=0; - Minecraft *pMinecraft=Minecraft::GetInstance(); - - // Before the game has started? - if(pMinecraft==nullptr) - { - // any track from the overworld - return GetRandomishTrack(m_iStream_Overworld_Min,m_iStream_Overworld_Max); - } - - if(pMinecraft->skins->isUsingDefaultSkin()) - { - switch(iDomain) - { - case LevelData::DIMENSION_END: - // the end isn't random - it has different music depending on whether the dragon is alive or not, but we've not added the dead dragon music yet - return m_iStream_End_Min; - case LevelData::DIMENSION_NETHER: - return GetRandomishTrack(m_iStream_Nether_Min,m_iStream_Nether_Max); - //return m_iStream_Nether_Min + random->nextInt(m_iStream_Nether_Max-m_iStream_Nether_Min); - default: //overworld - //return m_iStream_Overworld_Min + random->nextInt(m_iStream_Overworld_Max-m_iStream_Overworld_Min); - return GetRandomishTrack(m_iStream_Overworld_Min,m_iStream_Overworld_Max); - } - } - else - { - // using a texture pack - may have multiple End music tracks - switch(iDomain) - { - case LevelData::DIMENSION_END: - return GetRandomishTrack(m_iStream_End_Min,m_iStream_End_Max); - case LevelData::DIMENSION_NETHER: - //return m_iStream_Nether_Min + random->nextInt(m_iStream_Nether_Max-m_iStream_Nether_Min); - return GetRandomishTrack(m_iStream_Nether_Min,m_iStream_Nether_Max); - default: //overworld - //return m_iStream_Overworld_Min + random->nextInt(m_iStream_Overworld_Max-m_iStream_Overworld_Min); - return GetRandomishTrack(m_iStream_Overworld_Min,m_iStream_Overworld_Max); - } - } + if (domain == MusicTrackManager::Domain::End) + return m_musicTrackManager.getDomainMin(domain); + else + return m_musicTrackManager.selectTrack(domain); } ///////////////////////////////////////////// @@ -958,516 +874,485 @@ void SoundEngine::playMusicTick() #endif } -// AP - moved to a separate function so it can be called from the mixer callback on Vita -void SoundEngine::playMusicUpdate() +//============================================================================= +// playMusicUpdate +// Called every frame (or via mixer callback on Vita) to manage streaming music. +// Handles state machine for background music and music discs (jukebox). +//============================================================================= +void SoundEngine::playMusicUpdate() { - static float fMusicVol = 0.0f; - fMusicVol = getMasterMusicVolume(); + // Cache the current master music volume (may be zero if system music is playing) + float masterVolume = getMasterMusicVolume(); - switch(m_StreamState) - { - case eMusicStreamState_Idle: + //------------------------------------------------------------------------- + // State machine for streaming audio (background music or discs) + //------------------------------------------------------------------------- + switch (m_StreamState) + { + //--------------------------------------------------------------------- + // IDLE – no stream is open; waiting for a delay or ready to start + //--------------------------------------------------------------------- + case eMusicStreamState_Idle: + { + // If a delay is active (e.g., between tracks), decrement and wait + if (m_iMusicDelay > 0) + { + m_iMusicDelay--; + return; + } - // start a stream playing - if (m_iMusicDelay > 0) - { - m_iMusicDelay--; - return; - } + // Sanity check: if a stream is already active, something went wrong + if (m_musicStreamActive) + { + app.DebugPrintf("WARNING: m_musicStreamActive already true in Idle state, resetting to Playing\n"); + m_StreamState = eMusicStreamState_Playing; + return; + } - if (m_musicStreamActive) - { - app.DebugPrintf("WARNING: m_musicStreamActive already true in Idle state, resetting to Playing\n"); - m_StreamState = eMusicStreamState_Playing; - return; - } - - if(m_musicID!=-1) - { - // start playing it - - -#if ( defined __PS3__ || defined __PSVITA__ || defined __ORBIS__ ) + // If no track ID is selected, fallback to a default (should not happen) + if (m_musicID == -1) + { + m_musicID = m_musicTrackManager.selectTrack(MusicTrackManager::Domain::Menu); + } + // Build the full file path for the selected music ID. + // This block handles platform-specific paths, DLC/mash-up packs, and CD music. + // The result is stored in m_szStreamName. + { + // Start with the base music path (platform‑dependent) +#if (defined __PS3__ || defined __PSVITA__ || defined __ORBIS__) #ifdef __PS3__ - // 4J-PB - Need to check if we are a patched BD build - if(app.GetBootedFromDiscPatch()) - { - sprintf(m_szStreamName,"%s/%s",app.GetBDUsrDirPath(m_szMusicPath), m_szMusicPath ); - app.DebugPrintf("SoundEngine::playMusicUpdate - (booted from disc patch) music path - %s",m_szStreamName); - } - else - { - sprintf(m_szStreamName,"%s/%s",getUsrDirPath(), m_szMusicPath ); - } + // PS3 special case: booted from disc patch vs. installed data + if (app.GetBootedFromDiscPatch()) + { + sprintf(m_szStreamName, "%s/%s", app.GetBDUsrDirPath(m_szMusicPath), m_szMusicPath); + app.DebugPrintf("SoundEngine::playMusicUpdate - (booted from disc patch) music path - %s", m_szStreamName); + } + else + { + sprintf(m_szStreamName, "%s/%s", getUsrDirPath(), m_szMusicPath); + } #else - sprintf(m_szStreamName,"%s/%s",getUsrDirPath(), m_szMusicPath ); + // Other consoles (Vita, Orbis) + sprintf(m_szStreamName, "%s/%s", getUsrDirPath(), m_szMusicPath); +#endif +#else + // Windows / Durango – plain relative path + strcpy((char*)m_szStreamName, m_szMusicPath); #endif -#else - strcpy((char *)m_szStreamName,m_szMusicPath); -#endif - // are we using a mash-up pack? - //if(pMinecraft && !pMinecraft->skins->isUsingDefaultSkin() && pMinecraft->skins->getSelected()->hasAudio()) - if(Minecraft::GetInstance()->skins->getSelected()->hasAudio()) - { - // It's a mash-up - need to use the DLC path for the music - TexturePack *pTexPack=Minecraft::GetInstance()->skins->getSelected(); - DLCTexturePack *pDLCTexPack=(DLCTexturePack *)pTexPack; - DLCPack *pack = pDLCTexPack->getDLCInfoParentPack(); - DLCAudioFile *dlcAudioFile = (DLCAudioFile *) pack->getFile(DLCManager::e_DLCType_Audio, 0); + // Check if a mash‑up pack (DLC) is active and has custom audio + if (Minecraft::GetInstance()->skins->getSelected()->hasAudio()) + { + // Mash‑up pack: use DLC audio files + TexturePack* pTexPack = Minecraft::GetInstance()->skins->getSelected(); + DLCTexturePack* pDLCTexPack = (DLCTexturePack*)pTexPack; + DLCPack* pack = pDLCTexPack->getDLCInfoParentPack(); + DLCAudioFile* dlcAudioFile = (DLCAudioFile*)pack->getFile(DLCManager::e_DLCType_Audio, 0); - app.DebugPrintf("Mashup pack \n"); + app.DebugPrintf("Mashup pack\n"); - // build the name + // Determine whether this is game music (track index < first CD) or a CD + if (m_musicID < m_iStream_CD_1) + { + // Game music from the mash‑up pack + SetIsPlayingStreamingGameMusic(true); + SetIsPlayingStreamingCDMusic(false); + m_MusicType = eMusicType_Game; + m_StreamingAudioInfo.bIs3D = false; - // if the music ID is beyond the end of the texture pack music files, then it's a CD - if(m_musicIDGetSoundName(m_musicID); - wstring wstrFile=L"TPACK:\\Data\\" + wstrSoundName +L".wav"; - std::wstring mountedPath = StorageManager.GetMountedPath(wstrFile); - wcstombs(m_szStreamName,mountedPath.c_str(),255); + // Xbox One: use StorageManager to resolve TPACK path + wstring& wstrSoundName = dlcAudioFile->GetSoundName(m_musicID); + wstring wstrFile = L"TPACK:\\Data\\" + wstrSoundName + L".wav"; + std::wstring mountedPath = StorageManager.GetMountedPath(wstrFile); + wcstombs(m_szStreamName, mountedPath.c_str(), 255); #else - wstring &wstrSoundName=dlcAudioFile->GetSoundName(m_musicID); - char szName[255]; - wcstombs(szName,wstrSoundName.c_str(),255); + // Other platforms: convert wstring to char and build TPACK path + wstring& wstrSoundName = dlcAudioFile->GetSoundName(m_musicID); + char szName[255]; + wcstombs(szName, wstrSoundName.c_str(), 255); #if defined __PS3__ || defined __ORBIS__ || defined __PSVITA__ - string strFile="TPACK:/Data/" + string(szName) + ".wav"; + string strFile = "TPACK:/Data/" + string(szName) + ".wav"; #else - string strFile="TPACK:\\Data\\" + string(szName) + ".wav"; + string strFile = "TPACK:\\Data\\" + string(szName) + ".wav"; #endif - std::string mountedPath = StorageManager.GetMountedPath(strFile); - strcpy(m_szStreamName,mountedPath.c_str()); + std::string mountedPath = StorageManager.GetMountedPath(strFile); + strcpy(m_szStreamName, mountedPath.c_str()); #endif - } - else - { - SetIsPlayingStreamingGameMusic(false); - SetIsPlayingStreamingCDMusic(true); - m_MusicType=eMusicType_CD; - m_StreamingAudioInfo.bIs3D=true; + } + else + { + // CD track from the mash‑up pack + SetIsPlayingStreamingGameMusic(false); + SetIsPlayingStreamingCDMusic(true); + m_MusicType = eMusicType_CD; + m_StreamingAudioInfo.bIs3D = true; - // Need to adjust to index into the cds in the game's m_szStreamFileA - strcat((char *)m_szStreamName,"cds/"); - strcat((char *)m_szStreamName,m_szStreamFileA[m_musicID-m_iStream_CD_1+eStream_CD_1]); - strcat((char *)m_szStreamName,".wav"); - } - } - else - { - // 4J-PB - if this is a PS3 disc patch, we have to check if the music file is in the patch data + // Append "cds/" and the base filename (from the global stream file array) + strcat((char*)m_szStreamName, "cds/"); + strcat((char*)m_szStreamName, m_szStreamFileA[m_musicID - m_iStream_CD_1 + eStream_CD_1]); + strcat((char*)m_szStreamName, ".wav"); + } + } + else + { + // No mash‑up pack – use standard Minecraft music files #ifdef __PS3__ - if(app.GetBootedFromDiscPatch() && (m_musicID(m_szStreamName), "rb") == 0 && pFile) + { + fclose(pFile); + } + else + { + // File not found – try changing the extension + const char* extensions[] = { ".ogg", ".mp3", ".wav" }; + size_t extCount = sizeof(extensions) / sizeof(extensions[0]); + bool found = false; - FILE* pFile = nullptr; - - if (fopen_s(&pFile, reinterpret_cast(m_szStreamName), "rb") == 0 && pFile) - { - fclose(pFile); - } - else - { - const char* extensions[] = { ".ogg", ".mp3", ".wav" }; - size_t extCount = sizeof(extensions) / sizeof(extensions[0]); - bool found = false; + // Find the position of the current extension (assumed to be ".wav") + char* dotPos = strrchr(reinterpret_cast(m_szStreamName), '.'); + if (dotPos != nullptr && (dotPos - reinterpret_cast(m_szStreamName)) < 250) + { + for (size_t i = 0; i < extCount; ++i) + { + strcpy_s(dotPos, 5, extensions[i]); // Replace extension + if (fopen_s(&pFile, reinterpret_cast(m_szStreamName), "rb") == 0 && pFile) + { + fclose(pFile); + found = true; + break; + } + } + } - char* dotPos = strrchr(reinterpret_cast(m_szStreamName), '.'); - if (dotPos != nullptr && (dotPos - reinterpret_cast(m_szStreamName)) < 250) - { - for (size_t i = 0; i < extCount; i++) - { - strcpy_s(dotPos, 5, extensions[i]); - - if (fopen_s(&pFile, reinterpret_cast(m_szStreamName), "rb") == 0 && pFile) - { - fclose(pFile); - found = true; - break; - } - } - } + if (!found) + { + // Restore original extension for error message + if (dotPos != nullptr) + strcpy_s(dotPos, 5, ".wav"); + app.DebugPrintf("WARNING: No audio file found for music ID %d (tried .ogg, .mp3, .wav)\n", m_musicID); + return; // Abort – stay in Idle + } + } + } // end file path construction - if (!found) - { - if (dotPos != nullptr) - { - strcpy_s(dotPos, 5, ".wav"); - } - app.DebugPrintf("WARNING: No audio file found for music ID %d (tried .ogg, .mp3, .wav)\n", m_musicID); - return; - } - } + app.DebugPrintf("Starting streaming - %s\n", m_szStreamName); - app.DebugPrintf("Starting streaming - %s\n",m_szStreamName); - m_openStreamThread = new C4JThread(OpenStreamThreadProc, this, "OpenStreamThreadProc"); - m_openStreamThread->Run(); - m_StreamState = eMusicStreamState_Opening; - } - break; + // Launch a thread to open the audio file (prevents blocking the main thread) + m_openStreamThread = new C4JThread(OpenStreamThreadProc, this, "OpenStreamThreadProc"); + m_openStreamThread->Run(); + m_StreamState = eMusicStreamState_Opening; + break; + } - case eMusicStreamState_Opening: - if( !m_openStreamThread->isRunning() ) - { - delete m_openStreamThread; - m_openStreamThread = nullptr; + //--------------------------------------------------------------------- + // OPENING – waiting for the background thread to finish opening the file + //--------------------------------------------------------------------- + case eMusicStreamState_Opening: + { + if (!m_openStreamThread->isRunning()) + { + // Thread finished + delete m_openStreamThread; + m_openStreamThread = nullptr; - app.DebugPrintf("OpenStreamThreadProc finished. m_musicStreamActive=%d\n", m_musicStreamActive); + app.DebugPrintf("OpenStreamThreadProc finished. m_musicStreamActive=%d\n", m_musicStreamActive); - if (!m_musicStreamActive) - { - const char* currentExt = strrchr(reinterpret_cast(m_szStreamName), '.'); - if (currentExt && _stricmp(currentExt, ".wav") == 0) - { - const bool isCD = (m_musicID >= m_iStream_CD_1); - const char* folder = isCD ? "cds/" : "music/"; - - int n = sprintf_s(reinterpret_cast(m_szStreamName), 512, "%s%s%s.wav", m_szMusicPath, folder, m_szStreamFileA[m_musicID]); - - if (n > 0) - { - FILE* pFile = nullptr; - if (fopen_s(&pFile, reinterpret_cast(m_szStreamName), "rb") == 0 && pFile) - { - fclose(pFile); - - m_openStreamThread = new C4JThread(OpenStreamThreadProc, this, "OpenStreamThreadProc"); - m_openStreamThread->Run(); - break; - } - } - } - - m_StreamState = eMusicStreamState_Idle; - break; - } - - if (m_StreamingAudioInfo.bIs3D) - { - ma_sound_set_spatialization_enabled(&m_musicStream, MA_TRUE); - ma_sound_set_position(&m_musicStream, m_StreamingAudioInfo.x, m_StreamingAudioInfo.y, m_StreamingAudioInfo.z); - } - else - { - ma_sound_set_spatialization_enabled(&m_musicStream, MA_FALSE); - } + // If the stream failed to open, try a fallback (e.g., if we used a numbered variant) + if (!m_musicStreamActive) + { + const char* currentExt = strrchr(reinterpret_cast(m_szStreamName), '.'); + if (currentExt && _stricmp(currentExt, ".wav") == 0) + { + // Attempt to rebuild the path using the base filename (without number) + const bool isCD = (m_musicID >= m_iStream_CD_1); + const char* folder = isCD ? "cds/" : "music/"; + int n = sprintf_s(reinterpret_cast(m_szStreamName), 512, "%s%s%s.wav", + m_szMusicPath, folder, m_szStreamFileA[m_musicID]); + if (n > 0) + { + FILE* pFile = nullptr; + if (fopen_s(&pFile, reinterpret_cast(m_szStreamName), "rb") == 0 && pFile) + { + fclose(pFile); + // Retry opening with the new path + m_openStreamThread = new C4JThread(OpenStreamThreadProc, this, "OpenStreamThreadProc"); + m_openStreamThread->Run(); + break; // stay in Opening + } + } + } + // No fallback worked – go back to Idle + m_StreamState = eMusicStreamState_Idle; + break; + } - ma_sound_set_pitch(&m_musicStream, m_StreamingAudioInfo.pitch); + // Stream opened successfully; configure spatialization, pitch, volume + if (m_StreamingAudioInfo.bIs3D) + { + ma_sound_set_spatialization_enabled(&m_musicStream, MA_TRUE); + ma_sound_set_position(&m_musicStream, + m_StreamingAudioInfo.x, + m_StreamingAudioInfo.y, + m_StreamingAudioInfo.z); + } + else + { + ma_sound_set_spatialization_enabled(&m_musicStream, MA_FALSE); + } - float finalVolume = m_StreamingAudioInfo.volume * getMasterMusicVolume(); + ma_sound_set_pitch(&m_musicStream, m_StreamingAudioInfo.pitch); - ma_sound_set_volume(&m_musicStream, finalVolume); - ma_result startResult = ma_sound_start(&m_musicStream); - app.DebugPrintf("ma_sound_start result: %d\n", startResult); + float finalVolume = m_StreamingAudioInfo.volume * masterVolume; + ma_sound_set_volume(&m_musicStream, finalVolume); - m_StreamState=eMusicStreamState_Playing; - } - break; - case eMusicStreamState_OpeningCancel: - if( !m_openStreamThread->isRunning() ) - { - delete m_openStreamThread; - m_openStreamThread = nullptr; - m_StreamState = eMusicStreamState_Stop; - } - break; - case eMusicStreamState_Stop: - if (m_musicStreamActive) - { - ma_sound_stop(&m_musicStream); - ma_sound_uninit(&m_musicStream); - m_musicStreamActive = false; - } + // Start playback + ma_result startResult = ma_sound_start(&m_musicStream); + app.DebugPrintf("ma_sound_start result: %d\n", startResult); - SetIsPlayingStreamingCDMusic(false); - SetIsPlayingStreamingGameMusic(false); + m_StreamState = eMusicStreamState_Playing; + } + break; + } - m_StreamState = eMusicStreamState_Idle; - break; - case eMusicStreamState_Stopping: - break; - case eMusicStreamState_Play: - break; - case eMusicStreamState_Playing: - { - static int frameCount = 0; - if (frameCount++ % 60 == 0) - { - if (m_musicStreamActive) - { - bool isPlaying = ma_sound_is_playing(&m_musicStream); - float vol = ma_sound_get_volume(&m_musicStream); - bool isAtEnd = ma_sound_at_end(&m_musicStream); - } - } - } - if(GetIsPlayingStreamingGameMusic()) - { - //if(m_MusicInfo.pCue!=nullptr) - { - bool playerInEnd = false; - bool playerInNether=false; - Minecraft *pMinecraft = Minecraft::GetInstance(); - for(unsigned int i = 0; i < MAX_LOCAL_PLAYERS; ++i) - { - if(pMinecraft->localplayers[i]!=nullptr) - { - if(pMinecraft->localplayers[i]->dimension==LevelData::DIMENSION_END) - { - playerInEnd=true; - } - else if(pMinecraft->localplayers[i]->dimension==LevelData::DIMENSION_NETHER) - { - playerInNether=true; - } - } - } + //--------------------------------------------------------------------- + // OPENINGCANCEL – user requested stop while opening + //--------------------------------------------------------------------- + case eMusicStreamState_OpeningCancel: + { + if (!m_openStreamThread->isRunning()) + { + delete m_openStreamThread; + m_openStreamThread = nullptr; + m_StreamState = eMusicStreamState_Stop; + } + break; + } - if(playerInEnd && !GetIsPlayingEndMusic()) - { - m_StreamState=eMusicStreamState_Stop; + //--------------------------------------------------------------------- + // STOP – actively stop the current stream + //--------------------------------------------------------------------- + case eMusicStreamState_Stop: + { + if (m_musicStreamActive) + { + ma_sound_stop(&m_musicStream); + ma_sound_uninit(&m_musicStream); + m_musicStreamActive = false; + } - // Set the end track - m_musicID = getMusicID(LevelData::DIMENSION_END); - SetIsPlayingEndMusic(true); - SetIsPlayingNetherMusic(false); - } - else if(!playerInEnd && GetIsPlayingEndMusic()) - { - if(playerInNether) - { - m_StreamState=eMusicStreamState_Stop; + // Clear flags indicating what type of music was playing + SetIsPlayingStreamingCDMusic(false); + SetIsPlayingStreamingGameMusic(false); - // Set the end track - m_musicID = getMusicID(LevelData::DIMENSION_NETHER); - SetIsPlayingEndMusic(false); - SetIsPlayingNetherMusic(true); - } - else - { - m_StreamState=eMusicStreamState_Stop; + m_StreamState = eMusicStreamState_Idle; + break; + } - // Set the end track - m_musicID = getMusicID(LevelData::DIMENSION_OVERWORLD); - SetIsPlayingEndMusic(false); - SetIsPlayingNetherMusic(false); - } - } - else if (playerInNether && !GetIsPlayingNetherMusic()) - { - m_StreamState=eMusicStreamState_Stop; - // set the Nether track - m_musicID = getMusicID(LevelData::DIMENSION_NETHER); - SetIsPlayingNetherMusic(true); - SetIsPlayingEndMusic(false); - } - else if(!playerInNether && GetIsPlayingNetherMusic()) - { - if(playerInEnd) - { - m_StreamState=eMusicStreamState_Stop; - // set the Nether track - m_musicID = getMusicID(LevelData::DIMENSION_END); - SetIsPlayingNetherMusic(false); - SetIsPlayingEndMusic(true); - } - else - { - m_StreamState=eMusicStreamState_Stop; - // set the Nether track - m_musicID = getMusicID(LevelData::DIMENSION_OVERWORLD); - SetIsPlayingNetherMusic(false); - SetIsPlayingEndMusic(false); - } - } + //--------------------------------------------------------------------- + // PLAYING – the stream is actively playing + //--------------------------------------------------------------------- + case eMusicStreamState_Playing: + { + // Optional periodic debug logging (once per second at 60 fps) + static int frameCount = 0; + if (frameCount++ % 60 == 0) + { + if (m_musicStreamActive) + { + bool isPlaying = ma_sound_is_playing(&m_musicStream); + float vol = ma_sound_get_volume(&m_musicStream); + bool isAtEnd = ma_sound_at_end(&m_musicStream); + // (debug info could be printed here if needed) + } + } - // volume change required? - if (m_musicStreamActive) - { - float finalVolume = m_StreamingAudioInfo.volume * fMusicVol; + // Separate handling for game music (background) and CD music (jukebox) + if (GetIsPlayingStreamingGameMusic()) + { + // Background music: check if the musical context has changed + MusicTrackManager::Domain currentDomain = determineCurrentMusicDomain(); - ma_sound_set_volume(&m_musicStream, finalVolume); - } - } - } - else - { - // Music disc playing - if it's a 3D stream, then set the position - we don't have any streaming audio in the world that moves, so this isn't - // required unless we have more than one listener, and are setting the listening position to the origin and setting a fake position - // for the sound down the z axis - if (m_StreamingAudioInfo.bIs3D && m_validListenerCount > 1) - { - int iClosestListener = 0; - float fClosestDist = 1e6f; + // If the domain changed, we need to stop the current track + // so that a new one (appropriate for the new domain) will start. + if (currentDomain != m_currentMusicDomain) + { + m_StreamState = eMusicStreamState_Stop; + // Store the new domain for when we restart + m_currentMusicDomain = currentDomain; + m_musicID = getTrackForDomain(currentDomain); + break; + } - for (size_t i = 0; i < MAX_LOCAL_PLAYERS; i++) - { - if (m_ListenerA[i].bValid) - { - float dx = m_StreamingAudioInfo.x - m_ListenerA[i].vPosition.x; - float dy = m_StreamingAudioInfo.y - m_ListenerA[i].vPosition.y; - float dz = m_StreamingAudioInfo.z - m_ListenerA[i].vPosition.z; - float dist = sqrtf(dx*dx + dy*dy + dz*dz); + // Update volume if master volume changed + if (m_musicStreamActive) + { + float finalVolume = m_StreamingAudioInfo.volume * masterVolume; + ma_sound_set_volume(&m_musicStream, finalVolume); + } + } + else + { + // Music disc playing (jukebox) + if (m_StreamingAudioInfo.bIs3D && m_validListenerCount > 1) + { + // For split‑screen, we need to position the sound relative to the closest listener. + // (The engine's listener is set to the origin; we simulate distance by moving the sound.) + int iClosestListener = 0; + float fClosestDist = 1e6f; - if (dist < fClosestDist) - { - fClosestDist = dist; - iClosestListener = i; - } - } - } + for (size_t i = 0; i < MAX_LOCAL_PLAYERS; ++i) + { + if (m_ListenerA[i].bValid) + { + float dx = m_StreamingAudioInfo.x - m_ListenerA[i].vPosition.x; + float dy = m_StreamingAudioInfo.y - m_ListenerA[i].vPosition.y; + float dz = m_StreamingAudioInfo.z - m_ListenerA[i].vPosition.z; + float dist = sqrtf(dx * dx + dy * dy + dz * dz); - float relX = m_StreamingAudioInfo.x - m_ListenerA[iClosestListener].vPosition.x; - float relY = m_StreamingAudioInfo.y - m_ListenerA[iClosestListener].vPosition.y; - float relZ = m_StreamingAudioInfo.z - m_ListenerA[iClosestListener].vPosition.z; + if (dist < fClosestDist) + { + fClosestDist = dist; + iClosestListener = i; + } + } + } - if (m_musicStreamActive) - { - ma_sound_set_position(&m_musicStream, relX, relY, relZ); - } - } - } + // Compute sound position relative to that listener + float relX = m_StreamingAudioInfo.x - m_ListenerA[iClosestListener].vPosition.x; + float relY = m_StreamingAudioInfo.y - m_ListenerA[iClosestListener].vPosition.y; + float relZ = m_StreamingAudioInfo.z - m_ListenerA[iClosestListener].vPosition.z; - break; + if (m_musicStreamActive) + { + ma_sound_set_position(&m_musicStream, relX, relY, relZ); + } + } + } + break; + } - case eMusicStreamState_Completed: - { - // random delay of up to 3 minutes for music - m_iMusicDelay = random->nextInt(20 * 60 * 3);//random->nextInt(20 * 60 * 10) + 20 * 60 * 10; - // Check if we have a local player in The Nether or in The End, and play that music if they are - Minecraft *pMinecraft=Minecraft::GetInstance(); - bool playerInEnd=false; - bool playerInNether=false; + //--------------------------------------------------------------------- + // COMPLETED – the current track reached its end naturally + //--------------------------------------------------------------------- + case eMusicStreamState_Completed: + { + // Set a random delay (0–3 minutes) before playing the next track + m_iMusicDelay = random->nextInt(20 * 60 * 3); - for(unsigned int i=0;ilocalplayers[i]!=nullptr) - { - if(pMinecraft->localplayers[i]->dimension==LevelData::DIMENSION_END) - { - playerInEnd=true; - } - else if(pMinecraft->localplayers[i]->dimension==LevelData::DIMENSION_NETHER) - { - playerInNether=true; - } - } - } - if(playerInEnd) - { - m_musicID = getMusicID(LevelData::DIMENSION_END); - SetIsPlayingEndMusic(true); - SetIsPlayingNetherMusic(false); - } - else if(playerInNether) - { - m_musicID = getMusicID(LevelData::DIMENSION_NETHER); - SetIsPlayingNetherMusic(true); - SetIsPlayingEndMusic(false); - } - else - { - m_musicID = getMusicID(LevelData::DIMENSION_OVERWORLD); - SetIsPlayingNetherMusic(false); - SetIsPlayingEndMusic(false); - } + // Determine the current musical context + m_currentMusicDomain = determineCurrentMusicDomain(); - m_StreamState=eMusicStreamState_Idle; - } - break; - } + // Select a new track ID appropriate for that domain + m_musicID = getTrackForDomain(m_currentMusicDomain); - // check the status of the stream - this is for when a track completes rather than is stopped by the user action + // Update the flags that track which domain is active (for compatibility) + // These are used elsewhere; we keep them in sync. + SetIsPlayingMenuMusic(m_currentMusicDomain == MusicTrackManager::Domain::Menu); + SetIsPlayingEndMusic(m_currentMusicDomain == MusicTrackManager::Domain::End); + SetIsPlayingNetherMusic(m_currentMusicDomain == MusicTrackManager::Domain::Nether); - if (m_musicStreamActive) - { - if (!ma_sound_is_playing(&m_musicStream) && ma_sound_at_end(&m_musicStream)) - { - ma_sound_uninit(&m_musicStream); - m_musicStreamActive = false; + m_StreamState = eMusicStreamState_Idle; + break; + } - SetIsPlayingStreamingCDMusic(false); - SetIsPlayingStreamingGameMusic(false); + //--------------------------------------------------------------------- + // States that require no action (STOPPING, PLAY – unused) + //--------------------------------------------------------------------- + case eMusicStreamState_Stopping: + case eMusicStreamState_Play: + break; +} - m_StreamState = eMusicStreamState_Completed; - } - } + //------------------------------------------------------------------------- + // End-of‑stream detection (common to all states) + // If the stream is active but has finished playing, transition to Completed. + //------------------------------------------------------------------------- + if (m_musicStreamActive) + { + if (!ma_sound_is_playing(&m_musicStream) && ma_sound_at_end(&m_musicStream)) + { + ma_sound_uninit(&m_musicStream); + m_musicStreamActive = false; + + SetIsPlayingStreamingCDMusic(false); + SetIsPlayingStreamingGameMusic(false); + + m_StreamState = eMusicStreamState_Completed; + } + } } diff --git a/Minecraft.Client/Common/Audio/SoundEngine.h b/Minecraft.Client/Common/Audio/SoundEngine.h index 2134c491c..3d382362e 100644 --- a/Minecraft.Client/Common/Audio/SoundEngine.h +++ b/Minecraft.Client/Common/Audio/SoundEngine.h @@ -3,6 +3,7 @@ class Mob; class Options; using namespace std; #include "..\..\Minecraft.World\SoundTypes.h" +#include "MusicTrackManager.h" #include "miniaudio.h" @@ -17,6 +18,10 @@ enum eMUSICFILES eStream_Overworld_hal4, eStream_Overworld_nuance1, eStream_Overworld_nuance2, + eStream_Overworld_piano1, + eStream_Overworld_piano2, + eStream_Overworld_piano3, // <-- make piano3 the last overworld one + #ifndef _XBOX // Add the new music tracks eStream_Overworld_Creative1, @@ -25,14 +30,12 @@ enum eMUSICFILES eStream_Overworld_Creative4, eStream_Overworld_Creative5, eStream_Overworld_Creative6, + eStream_Overworld_Menu1, eStream_Overworld_Menu2, eStream_Overworld_Menu3, eStream_Overworld_Menu4, #endif - eStream_Overworld_piano1, - eStream_Overworld_piano2, - eStream_Overworld_piano3, // <-- make piano3 the last overworld one // Nether eStream_Nether1, eStream_Nether2, @@ -106,6 +109,7 @@ extern std::vector m_activeSounds; class SoundEngine : public ConsoleSoundEngine { static const int MAX_SAME_SOUNDS_PLAYING = 8; // 4J added + public: SoundEngine(); void destroy() override; @@ -125,10 +129,11 @@ public: void addMusic(const wstring& name, File *file) override; void addStreaming(const wstring& name, File *file) override; char *ConvertSoundPathToName(const wstring& name, bool bConvertSpaces=false) override; - bool isStreamingWavebankReady(); // 4J Added - int getMusicID(int iDomain); + bool isStreamingWavebankReady(); + MusicTrackManager::Domain determineCurrentMusicDomain() const; + int getTrackForDomain(MusicTrackManager::Domain domain); int getMusicID(const wstring& name); - void SetStreamingSounds(int iOverworldMin, int iOverWorldMax, int iNetherMin, int iNetherMax, int iEndMin, int iEndMax, int iCD1); + void SetStreamingSounds(int iMenuMin, int iMenuMax, int iOverworldSurvivalMin, int iOverWorldSurvivalMax, int iOverworldCreativeMin, int iOverWorldCreativeMax, int iNetherMin, int iNetherMax, int iEndMin, int iEndMax, int iCD1); void updateMiniAudio(); void playMusicUpdate(); @@ -141,8 +146,10 @@ private: int initAudioHardware(int iMinSpeakers) override { return iMinSpeakers;} #endif - - int GetRandomishTrack(int iStart,int iEnd); + + Random* random; + MusicTrackManager m_musicTrackManager; + MusicTrackManager::Domain m_currentMusicDomain; ma_engine m_engine; ma_engine_config m_engineConfig; @@ -157,8 +164,6 @@ private: AUDIO_LISTENER m_ListenerA[MAX_LOCAL_PLAYERS]; int m_validListenerCount; - - Random *random; int m_musicID; int m_iMusicDelay; int m_StreamState; @@ -174,12 +179,7 @@ private: char m_szStreamName[255]; int CurrentSoundsPlaying[eSoundType_MAX+eSFX_MAX]; - // streaming music files - will be different for mash-up packs - int m_iStream_Overworld_Min,m_iStream_Overworld_Max; - int m_iStream_Nether_Min,m_iStream_Nether_Max; - int m_iStream_End_Min,m_iStream_End_Max; int m_iStream_CD_1; - bool *m_bHeardTrackA; #ifdef __ORBIS__ int32_t m_hBGMAudio; diff --git a/Minecraft.Client/Common/Consoles_App.cpp b/Minecraft.Client/Common/Consoles_App.cpp index c3a623d5f..204ded30b 100644 --- a/Minecraft.Client/Common/Consoles_App.cpp +++ b/Minecraft.Client/Common/Consoles_App.cpp @@ -3754,10 +3754,12 @@ void CMinecraftApp::HandleXuiActions(void) // need to stop the streaming audio - by playing streaming audio from the default texture pack now // reset the streaming sounds back to the normal ones #ifndef _XBOX - pMinecraft->soundEngine->SetStreamingSounds(eStream_Overworld_Calm1,eStream_Overworld_piano3, - eStream_Nether1,eStream_Nether4, - eStream_end_dragon,eStream_end_end, - eStream_CD_1); + pMinecraft->soundEngine->SetStreamingSounds(eStream_Overworld_Menu1, eStream_Overworld_Menu4, + eStream_Overworld_Calm1, eStream_Overworld_piano3, + eStream_Overworld_Creative1, eStream_Overworld_Creative6, + eStream_Nether1, eStream_Nether4, + eStream_end_dragon, eStream_end_end, + eStream_CD_1); #endif pMinecraft->soundEngine->playStreaming(L"", 0, 0, 0, 1, 1); diff --git a/Minecraft.Client/Common/UI/UIController.cpp b/Minecraft.Client/Common/UI/UIController.cpp index b12ea5e73..4d4242493 100644 --- a/Minecraft.Client/Common/UI/UIController.cpp +++ b/Minecraft.Client/Common/UI/UIController.cpp @@ -2064,10 +2064,12 @@ void UIController::NavigateToHomeMenu() { // need to stop the streaming audio - by playing streaming audio from the default texture pack now // reset the streaming sounds back to the normal ones - pMinecraft->soundEngine->SetStreamingSounds(eStream_Overworld_Calm1,eStream_Overworld_piano3, - eStream_Nether1,eStream_Nether4, - eStream_end_dragon,eStream_end_end, - eStream_CD_1); + pMinecraft->soundEngine->SetStreamingSounds(eStream_Overworld_Menu1, eStream_Overworld_Menu4, + eStream_Overworld_Calm1, eStream_Overworld_piano3, + eStream_Overworld_Creative1, eStream_Overworld_Creative6, + eStream_Nether1, eStream_Nether4, + eStream_end_dragon, eStream_end_end, + eStream_CD_1); pMinecraft->soundEngine->playStreaming(L"", 0, 0, 0, 1, 1); // if(pDLCTexPack->m_pStreamedWaveBank!=nullptr) diff --git a/Minecraft.Client/DLCTexturePack.cpp b/Minecraft.Client/DLCTexturePack.cpp index f1304a9ef..69c75d632 100644 --- a/Minecraft.Client/DLCTexturePack.cpp +++ b/Minecraft.Client/DLCTexturePack.cpp @@ -483,7 +483,7 @@ int DLCTexturePack::packMounted(LPVOID pParam,int iPad,DWORD dwErr,DWORD dwLicen iEndStart=iOverworldC+iNetherC; iEndC=dlcFile->GetCountofType(DLCAudioFile::e_AudioType_End); - Minecraft::GetInstance()->soundEngine->SetStreamingSounds(iOverworldStart,iOverworldStart+iOverworldC-1, + Minecraft::GetInstance()->soundEngine->SetStreamingSounds(iOverworldStart,iOverworldStart+iOverworldC-1, iOverworldStart, iOverworldStart + iOverworldC - 1, iOverworldStart, iOverworldStart + iOverworldC - 1, iNetherStart,iNetherStart+iNetherC-1,iEndStart,iEndStart+iEndC-1,iEndStart+iEndC); // push the CD start to after } #endif