feat: minecart sound effects

This commit is contained in:
Fireblade 2026-05-08 03:41:44 -04:00
parent a8f31211bf
commit d82e1afd59
16 changed files with 388 additions and 53 deletions

View file

@ -109,6 +109,7 @@ enum EGameHostOptionWorldSize
#define GAMESETTING_EXCLUSIVEFULLSCREEN 0x02000000
#define GAMESETTING_CLASSICCRAFTING 0x04000000
#define GAMESETTING_CAVESOUNDS 0x08000000
#define GAMESETTING_MINECARTSOUNDS 0x10000000
// defines for languages

View file

@ -185,6 +185,7 @@ enum eGameSetting
//TU25
eGameSetting_ClassicCrafting,
eGameSetting_CaveSounds,
eGameSetting_MinecartSounds,
};

View file

@ -605,34 +605,8 @@ void SoundEngine::play(int iSound, float x, float y, float z, float volume, floa
m_activeSounds.push_back(s);
}
/////////////////////////////////////////////
//
//
// startElytraSound / stopElytraSound
// Manages a single persistent looping sound for elytra gliding.
// Call startElytraSound every tick while gliding (it no-ops if already running,
// just updates volume). Call stopElytraSound when gliding ends.
//
// IMPORTANT: m_elytraLoopingSound is NOT added to m_activeSounds.
// The tick() cleanup loop deletes sounds where is_playing()==false.
// A looping sound briefly reports is_playing()==false at the loop point,
// which would cause tick() to free it and leave m_elytraLoopingSound dangling.
//
/////////////////////////////////////////////
void SoundEngine::startElytraSound(float x, float y, float z, float volume, float pitch)
MiniAudioSound* SoundEngine::startLoopingSound(const wstring& name, float x, float y, float z, float volume, float pitch, bool bIs3D)
{
// If already initialized just update volume and pitch - never reinitialize mid-flight.
if (m_elytraLoopingSound != nullptr)
{
float finalVolume = volume * m_MasterEffectsVolume * SFX_VOLUME_MULTIPLIER;
if (finalVolume > SFX_MAX_GAIN) finalVolume = SFX_MAX_GAIN;
ma_sound_set_volume(&m_elytraLoopingSound->sound, finalVolume);
ma_sound_set_pitch(&m_elytraLoopingSound->sound, pitch);
return;
}
// Resolve file path using the same logic as play().
wstring name = wchSoundNames[eSoundType_ITEM_ELYTRA_FLYING];
char* soundName = ConvertSoundPathToName(name);
char basePath[256];
sprintf_s(basePath, "Windows64Media/Sound/Minecraft/%s", soundName);
@ -652,42 +626,100 @@ void SoundEngine::startElytraSound(float x, float y, float z, float volume, floa
break;
}
}
if (!found) return;
if (!found)
{
return nullptr;
}
MiniAudioSound* s = new MiniAudioSound();
memset(&s->info, 0, sizeof(AUDIO_INFO));
s->info.volume = volume; s->info.pitch = pitch;
s->info.bIs3D = false;
s->info.iSound = eSoundType_ITEM_ELYTRA_FLYING + eSFX_MAX;
s->info.x = x;
s->info.y = y;
s->info.z = z;
s->info.volume = volume;
s->info.pitch = pitch;
s->info.bIs3D = bIs3D;
s->info.bUseSoundsPitchVal = false;
// Synchronous load so the sound is immediately ready - no ASYNC gap.
if (ma_sound_init_from_file(&m_engine, finalPath, 0,
nullptr, nullptr, &s->sound) != MA_SUCCESS)
if (ma_sound_init_from_file(&m_engine, finalPath, 0, nullptr, nullptr, &s->sound) != MA_SUCCESS)
{
delete s;
return;
return nullptr;
}
ma_sound_set_spatialization_enabled(&s->sound, MA_FALSE);
ma_sound_set_spatialization_enabled(&s->sound, bIs3D ? MA_TRUE : MA_FALSE);
ma_sound_set_looping(&s->sound, MA_TRUE);
float finalVolume = volume * m_MasterEffectsVolume * SFX_VOLUME_MULTIPLIER;
if (finalVolume > SFX_MAX_GAIN) finalVolume = SFX_MAX_GAIN;
if (finalVolume > SFX_MAX_GAIN)
finalVolume = SFX_MAX_GAIN;
ma_sound_set_volume(&s->sound, finalVolume);
ma_sound_set_pitch(&s->sound, pitch);
if (bIs3D)
{
ma_sound_set_position(&s->sound, x, y, z);
}
ma_sound_start(&s->sound);
return s;
}
void SoundEngine::updateLoopingSound(MiniAudioSound* sound, float x, float y, float z, float volume, float pitch)
{
if (sound == nullptr)
{
return;
}
float finalVolume = volume * m_MasterEffectsVolume * SFX_VOLUME_MULTIPLIER;
if (finalVolume > SFX_MAX_GAIN)
finalVolume = SFX_MAX_GAIN;
ma_sound_set_volume(&sound->sound, finalVolume);
ma_sound_set_pitch(&sound->sound, pitch);
ma_sound_set_position(&sound->sound, x, y, z);
}
void SoundEngine::stopLoopingSound(MiniAudioSound* sound)
{
if (sound == nullptr)
{
return;
}
ma_sound_stop(&sound->sound);
ma_sound_uninit(&sound->sound);
delete sound;
}
/////////////////////////////////////////////
//
//
// startElytraSound / stopElytraSound
// Manages a single persistent looping sound for elytra gliding.
// Call startElytraSound every tick while gliding (it no-ops if already running,
// just updates volume). Call stopElytraSound when gliding ends.
//
// IMPORTANT: m_elytraLoopingSound is NOT added to m_activeSounds.
// The tick() cleanup loop deletes sounds where is_playing()==false.
// A looping sound briefly reports is_playing()==false at the loop point,
// which would cause tick() to free it and leave m_elytraLoopingSound dangling.
//
/////////////////////////////////////////////
void SoundEngine::startElytraSound(float x, float y, float z, float volume, float pitch)
{
// If already initialized just update volume and pitch - never reinitialize mid-flight.
if (m_elytraLoopingSound != nullptr)
{
updateLoopingSound(m_elytraLoopingSound, x, y, z, volume, pitch);
return;
}
// NOT added to m_activeSounds - tick() cleanup would delete it at loop boundaries.
m_elytraLoopingSound = s;
m_elytraLoopingSound = startLoopingSound(wchSoundNames[eSoundType_ITEM_ELYTRA_FLYING], x, y, z, volume, pitch, false);
}
void SoundEngine::stopElytraSound()
{
if (m_elytraLoopingSound == nullptr) return;
ma_sound_stop(&m_elytraLoopingSound->sound);
ma_sound_uninit(&m_elytraLoopingSound->sound);
delete m_elytraLoopingSound;
stopLoopingSound(m_elytraLoopingSound);
m_elytraLoopingSound = nullptr;
}
/////////////////////////////////////////////

View file

@ -127,6 +127,9 @@ public:
void GetSoundName(char *szSoundName,int iSound);
#endif
void play(int iSound, float x, float y, float z, float volume, float pitch) override;
MiniAudioSound* startLoopingSound(const wstring& name, float x, float y, float z, float volume, float pitch, bool bIs3D = true);
void updateLoopingSound(MiniAudioSound* sound, float x, float y, float z, float volume, float pitch);
void stopLoopingSound(MiniAudioSound* sound);
void startElytraSound(float x, float y, float z, float volume, float pitch);
void stopElytraSound();
void playStreaming(const wstring& name, float x, float y , float z, float volume, float pitch, bool bMusicDelay=true) override;

View file

@ -1043,6 +1043,7 @@ int CMinecraftApp::SetDefaultOptions(C_4JProfile::PROFILESETTINGS *pSettings,con
//TU25
SetGameSettings(iPad, eGameSetting_ClassicCrafting, 0);
SetGameSettings(iPad, eGameSetting_CaveSounds, 1);
SetGameSettings(iPad, eGameSetting_MinecartSounds, 1);
// 4J-PB - leave these in, or remove from everywhere they are referenced!
// Although probably best to leave in unless we split the profile settings into platform specific classes - having different meaning per platform for the same bitmask could get confusing
@ -1504,6 +1505,7 @@ void CMinecraftApp::ApplyGameSettingsChanged(int iPad)
//TU25
ActionGameSettings(iPad, eGameSetting_ClassicCrafting);
ActionGameSettings(iPad, eGameSetting_CaveSounds);
ActionGameSettings(iPad, eGameSetting_MinecartSounds);
}
void CMinecraftApp::ActionGameSettings(int iPad,eGameSetting eVal)
@ -2529,6 +2531,21 @@ void CMinecraftApp::SetGameSettings(int iPad,eGameSetting eVal,unsigned char ucV
GameSettingsA[iPad]->bSettingsChanged = true;
}
break;
case eGameSetting_MinecartSounds:
if ((GameSettingsA[iPad]->uiBitmaskValues & GAMESETTING_MINECARTSOUNDS) != (ucVal & 0x01) << 28)
{
if (ucVal == 1)
{
GameSettingsA[iPad]->uiBitmaskValues |= GAMESETTING_MINECARTSOUNDS;
}
else
{
GameSettingsA[iPad]->uiBitmaskValues &= ~GAMESETTING_MINECARTSOUNDS;
}
ActionGameSettings(iPad, eVal);
GameSettingsA[iPad]->bSettingsChanged = true;
}
break;
}
}
@ -2670,6 +2687,9 @@ unsigned char CMinecraftApp::GetGameSettings(int iPad,eGameSetting eVal)
case eGameSetting_CaveSounds:
return (GameSettingsA[iPad]->uiBitmaskValues & GAMESETTING_CAVESOUNDS) >> 27;
case eGameSetting_MinecartSounds:
return (GameSettingsA[iPad]->uiBitmaskValues & GAMESETTING_MINECARTSOUNDS) >> 28;
case eGameSetting_VSync:
return (GameSettingsA[iPad]->uiBitmaskValues&GAMESETTING_VSYNC)>>24;

View file

@ -16,6 +16,8 @@ UIScene_SettingsAudioMenu::UIScene_SettingsAudioMenu(int iPad, void *initData, U
m_checkboxCaveSounds.init(L"Cave Sounds",eControl_CaveSounds,(app.GetGameSettings(m_iPad,eGameSetting_CaveSounds)!=0));
m_checkboxMinecartSounds.init(L"Minecart Sounds",eControl_MinecartSounds,(app.GetGameSettings(m_iPad,eGameSetting_MinecartSounds)!=0));
doHorizontalResizeCheck();
if(app.GetLocalPlayerCount()>1)
@ -124,5 +126,8 @@ void UIScene_SettingsAudioMenu::handleCheckboxToggled(F64 controlId, bool select
case eControl_CaveSounds:
app.SetGameSettings(m_iPad, eGameSetting_CaveSounds, selected ? 1 : 0);
break;
case eControl_MinecartSounds:
app.SetGameSettings(m_iPad, eGameSetting_MinecartSounds, selected ? 1 : 0);
break;
}
}

View file

@ -9,15 +9,18 @@ private:
{
eControl_Music,
eControl_Sound,
eControl_CaveSounds
eControl_CaveSounds,
eControl_MinecartSounds
};
UIControl_Slider m_sliderMusic, m_sliderSound; // Sliders
UIControl_CheckBox m_checkboxCaveSounds; // Checkboxes
UIControl_CheckBox m_checkboxMinecartSounds;
UI_BEGIN_MAP_ELEMENTS_AND_NAMES(UIScene)
UI_MAP_ELEMENT( m_sliderMusic, "Music")
UI_MAP_ELEMENT( m_sliderSound, "Sound")
UI_MAP_ELEMENT( m_checkboxCaveSounds, "CaveSounds")
UI_MAP_ELEMENT( m_checkboxMinecartSounds, "MinecartSounds")
UI_END_MAP_ELEMENTS_AND_NAMES()
public:

View file

@ -6582,6 +6582,10 @@ Would you like to install the mash-up pack or texture pack now?</value>
<value>Cave Sounds</value>
</data>
<data name="IDS_CHECKBOX_MINECART_SOUNDS">
<value>Minecart Sounds</value>
</data>
<data name="IDS_TEXT_SAVEOPTIONS">
<value>What would you like to do with this save game?</value>
</data>

View file

@ -13,6 +13,7 @@
#include "../Minecraft.Client/ServerLevel.h"
#include "com.mojang.nbt.h"
#include "Minecart.h"
#include "MinecartSoundInstance.h"
#include "SharedConstants.h"
@ -44,7 +45,8 @@ void Minecart::_init()
blocksBuilding = true;
setSize(0.98f, 0.7f);
heightOffset = bbHeight / 2.0f;
soundUpdater = nullptr;
m_rollingSound = nullptr;
m_ridingSound = nullptr;
name = L"";
//
@ -61,26 +63,43 @@ Minecart::Minecart(Level *level) : Entity( level )
Minecart::~Minecart()
{
delete soundUpdater;
delete m_rollingSound;
delete m_ridingSound;
}
shared_ptr<Minecart> Minecart::createMinecart(Level *level, double x, double y, double z, int type)
{
shared_ptr<Minecart> minecart;
switch (type)
{
case TYPE_CHEST:
return std::make_shared<MinecartChest>(level, x, y, z);
minecart = std::make_shared<MinecartChest>(level, x, y, z);
break;
case TYPE_FURNACE:
return std::make_shared<MinecartFurnace>(level, x, y, z);
minecart = std::make_shared<MinecartFurnace>(level, x, y, z);
break;
case TYPE_TNT:
return std::make_shared<MinecartTNT>(level, x, y, z);
minecart = std::make_shared<MinecartTNT>(level, x, y, z);
break;
case TYPE_SPAWNER:
return std::make_shared<MinecartSpawner>(level, x, y, z);
minecart = std::make_shared<MinecartSpawner>(level, x, y, z);
break;
case TYPE_HOPPER:
return std::make_shared<MinecartHopper>(level, x, y, z);
minecart = std::make_shared<MinecartHopper>(level, x, y, z);
break;
default:
return std::make_shared<MinecartRideable>(level, x, y, z);
minecart = std::make_shared<MinecartRideable>(level, x, y, z);
break;
}
if (level != nullptr && level->isClientSide)
{
minecart->m_rollingSound = new MinecartSoundInstance(minecart);
minecart->m_ridingSound = new RidingMinecartSoundInstance(minecart);
}
return minecart;
}
bool Minecart::makeStepSound()
@ -207,11 +226,36 @@ bool Minecart::isPickable()
void Minecart::remove()
{
Entity::remove();
// clean up sounds after invalidation
if (m_rollingSound)
{
delete m_rollingSound;
m_rollingSound = nullptr;
}
if (m_ridingSound)
{
delete m_ridingSound;
m_ridingSound = nullptr;
}
//if (soundUpdater != nullptr) soundUpdater->tick();
}
void Minecart::tick()
{
// minecart tick handler
if (level->isClientSide)
{
if (m_rollingSound)
{
m_rollingSound->tick();
}
if (m_ridingSound)
{
m_ridingSound->tick();
}
}
//if (soundUpdater != nullptr) soundUpdater->tick();
// 4J - make minecarts (server-side) tick twice, to put things back to how they were when we were accidently ticking them twice
for( int i = 0; i < 2; i++ )

View file

@ -2,7 +2,8 @@
#include "Entity.h"
class DamageSource;
class Tickable;
class MinecartSoundInstance;
class RidingMinecartSoundInstance;
class Minecart : public Entity
{
@ -30,7 +31,8 @@ private:
static const int DATA_ID_CUSTOM_DISPLAY = 22;
bool flipped;
Tickable *soundUpdater;
MinecartSoundInstance *m_rollingSound;
RidingMinecartSoundInstance *m_ridingSound;
wstring name;
protected:

View file

@ -0,0 +1,175 @@
#include "stdafx.h"
#include "MinecartSoundInstance.h"
#include "Minecart.h"
#include "net.minecraft.world.level.h"
#include "../Minecraft.Client/Minecraft.h"
#include "../Minecraft.Client/Common/Audio/SoundEngine.h"
// MinecartSoundInstance
MinecartSoundInstance::MinecartSoundInstance(shared_ptr<Minecart> minecart)
: m_minecart(minecart), m_bIsCurrentlyPlaying(false), m_sound(nullptr), m_volume(0.0f), m_pitch(1.0f)
{
}
MinecartSoundInstance::~MinecartSoundInstance()
{
if (m_sound && Minecraft::GetInstance() && Minecraft::GetInstance()->soundEngine)
{
Minecraft::GetInstance()->soundEngine->stopLoopingSound(m_sound);
}
m_sound = nullptr;
}
void MinecartSoundInstance::tick()
{
if (!m_minecart || m_minecart->removed)
{
if (m_sound && Minecraft::GetInstance() && Minecraft::GetInstance()->soundEngine)
{
Minecraft::GetInstance()->soundEngine->stopLoopingSound(m_sound);
m_sound = nullptr;
}
m_bIsCurrentlyPlaying = false;
return;
}
// minecart sound functionality check
if (!app.GetGameSettings(0, eGameSetting_MinecartSounds))
{
if (m_sound && Minecraft::GetInstance() && Minecraft::GetInstance()->soundEngine)
{
Minecraft::GetInstance()->soundEngine->stopLoopingSound(m_sound);
m_sound = nullptr;
}
m_bIsCurrentlyPlaying = false;
return;
}
// volume + pitch calculations
// relative to minecart velocity
double xd = m_minecart->xd;
double zd = m_minecart->zd;
double velocity = sqrt(xd * xd + zd * zd);
if (velocity >= 0.01)
{
float clampedVel = (float)(velocity > 1.0 ? 1.0 : (velocity < 0.0 ? 0.0 : velocity));
m_volume = clampedVel * 0.75f;
m_pitch = 1.0f;
if (!m_bIsCurrentlyPlaying)
{
m_bIsCurrentlyPlaying = true;
if (Minecraft::GetInstance() && Minecraft::GetInstance()->soundEngine)
{
m_sound = Minecraft::GetInstance()->soundEngine->startLoopingSound(L"mob.minecart.rolling", (float)m_minecart->x, (float)m_minecart->y, (float)m_minecart->z, m_volume, m_pitch, true);
}
}
else if (m_sound && Minecraft::GetInstance() && Minecraft::GetInstance()->soundEngine)
{
Minecraft::GetInstance()->soundEngine->updateLoopingSound(m_sound, (float)m_minecart->x, (float)m_minecart->y, (float)m_minecart->z, m_volume, m_pitch);
}
}
else
{
if (m_sound && Minecraft::GetInstance() && Minecraft::GetInstance()->soundEngine)
{
Minecraft::GetInstance()->soundEngine->stopLoopingSound(m_sound);
m_sound = nullptr;
}
m_volume = 0.0f;
m_pitch = 0.0f;
m_bIsCurrentlyPlaying = false;
}
}
// RidingMinecartSoundInstance
RidingMinecartSoundInstance::RidingMinecartSoundInstance(shared_ptr<Minecart> minecart)
: m_minecart(minecart), m_bIsCurrentlyPlaying(false), m_sound(nullptr), m_volume(0.0f), m_pitch(0.0f)
{
}
RidingMinecartSoundInstance::~RidingMinecartSoundInstance()
{
if (m_sound && Minecraft::GetInstance() && Minecraft::GetInstance()->soundEngine)
{
Minecraft::GetInstance()->soundEngine->stopLoopingSound(m_sound);
}
m_sound = nullptr;
}
void RidingMinecartSoundInstance::tick()
{
if (!m_minecart || m_minecart->removed)
{
if (m_sound && Minecraft::GetInstance() && Minecraft::GetInstance()->soundEngine)
{
Minecraft::GetInstance()->soundEngine->stopLoopingSound(m_sound);
m_sound = nullptr;
}
m_bIsCurrentlyPlaying = false;
return;
}
// minecart sound functionality check
if (!app.GetGameSettings(0, eGameSetting_MinecartSounds))
{
if (m_sound && Minecraft::GetInstance() && Minecraft::GetInstance()->soundEngine)
{
Minecraft::GetInstance()->soundEngine->stopLoopingSound(m_sound);
m_sound = nullptr;
}
m_bIsCurrentlyPlaying = false;
return;
}
// minecart passenger check
if (m_minecart->rider.lock() == nullptr)
{
if (m_sound && Minecraft::GetInstance() && Minecraft::GetInstance()->soundEngine)
{
Minecraft::GetInstance()->soundEngine->stopLoopingSound(m_sound);
m_sound = nullptr;
}
m_bIsCurrentlyPlaying = false;
return;
}
// volume + pitch calculations
// relative to minecart velocity
double xd = m_minecart->xd;
double zd = m_minecart->zd;
double velocity = sqrt(xd * xd + zd * zd);
if (velocity >= 0.01)
{
float clampedVel = (float)(velocity > 1.0 ? 1.0 : (velocity < 0.0 ? 0.0 : velocity));
m_volume = clampedVel * 0.75f;
m_pitch = 1.0f;
if (!m_bIsCurrentlyPlaying)
{
m_bIsCurrentlyPlaying = true;
if (Minecraft::GetInstance() && Minecraft::GetInstance()->soundEngine)
{
m_sound = Minecraft::GetInstance()->soundEngine->startLoopingSound(L"mob.minecart.inside", (float)m_minecart->x, (float)m_minecart->y, (float)m_minecart->z, m_volume, m_pitch, false);
}
}
else if (m_sound && Minecraft::GetInstance() && Minecraft::GetInstance()->soundEngine)
{
Minecraft::GetInstance()->soundEngine->updateLoopingSound(m_sound, (float)m_minecart->x, (float)m_minecart->y, (float)m_minecart->z, m_volume, m_pitch);
}
}
else
{
if (m_sound && Minecraft::GetInstance() && Minecraft::GetInstance()->soundEngine)
{
Minecraft::GetInstance()->soundEngine->stopLoopingSound(m_sound);
m_sound = nullptr;
}
m_volume = 0.0f;
m_bIsCurrentlyPlaying = false;
}
}

View file

@ -0,0 +1,43 @@
#pragma once
#include <memory>
class Minecart;
class SoundEngine;
struct MiniAudioSound;
// minecart rolling sound
class MinecartSoundInstance
{
protected:
shared_ptr<Minecart> m_minecart;
bool m_bIsCurrentlyPlaying;
MiniAudioSound* m_sound;
float m_volume;
float m_pitch;
public:
MinecartSoundInstance(shared_ptr<Minecart> minecart);
virtual ~MinecartSoundInstance();
virtual void tick();
bool isCurrentlyPlaying() const { return m_bIsCurrentlyPlaying; }
};
// minecart passenger sound
class RidingMinecartSoundInstance
{
protected:
shared_ptr<Minecart> m_minecart;
bool m_bIsCurrentlyPlaying;
MiniAudioSound* m_sound;
float m_volume;
float m_pitch;
public:
RidingMinecartSoundInstance(shared_ptr<Minecart> minecart);
virtual ~RidingMinecartSoundInstance();
virtual void tick();
bool isCurrentlyPlaying() const { return m_bIsCurrentlyPlaying; }
};

View file

@ -838,6 +838,8 @@ set(_MINECRAFT_WORLD_COMMON_NET_MINECRAFT_WORLD_ENTITY_ITEM
"${CMAKE_CURRENT_SOURCE_DIR}/ItemEntity.h"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecart.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Minecart.h"
"${CMAKE_CURRENT_SOURCE_DIR}/MinecartSoundInstance.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/MinecartSoundInstance.h"
"${CMAKE_CURRENT_SOURCE_DIR}/MinecartChest.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/MinecartChest.h"
"${CMAKE_CURRENT_SOURCE_DIR}/MinecartContainer.cpp"