| 1 | #include "mapsounds.h" |
| 2 | |
| 3 | #include <base/log.h> |
| 4 | |
| 5 | #include <engine/demo.h> |
| 6 | #include <engine/sound.h> |
| 7 | |
| 8 | #include <game/client/components/camera.h> |
| 9 | #include <game/client/components/sounds.h> |
| 10 | #include <game/client/gameclient.h> |
| 11 | #include <game/layers.h> |
| 12 | #include <game/localization.h> |
| 13 | #include <game/mapitems.h> |
| 14 | |
| 15 | CMapSounds::CMapSounds() |
| 16 | { |
| 17 | m_Count = 0; |
| 18 | } |
| 19 | |
| 20 | void CMapSounds::Play(int Channel, int SoundId) |
| 21 | { |
| 22 | if(SoundId < 0 || SoundId >= m_Count) |
| 23 | return; |
| 24 | |
| 25 | GameClient()->m_Sounds.PlaySample(Channel, SampleId: m_aSounds[SoundId], Flags: 0, Volume: 1.0f); |
| 26 | } |
| 27 | |
| 28 | void CMapSounds::PlayAt(int Channel, int SoundId, vec2 Position) |
| 29 | { |
| 30 | if(SoundId < 0 || SoundId >= m_Count) |
| 31 | return; |
| 32 | |
| 33 | GameClient()->m_Sounds.PlaySampleAt(Channel, SampleId: m_aSounds[SoundId], Flags: 0, Volume: 1.0f, Position); |
| 34 | } |
| 35 | |
| 36 | void CMapSounds::OnMapLoad() |
| 37 | { |
| 38 | IMap *pMap = Kernel()->RequestInterface<IMap>(); |
| 39 | |
| 40 | Clear(); |
| 41 | |
| 42 | if(!Sound()->IsSoundEnabled()) |
| 43 | return; |
| 44 | |
| 45 | // load samples |
| 46 | int Start; |
| 47 | pMap->GetType(Type: MAPITEMTYPE_SOUND, pStart: &Start, pNum: &m_Count); |
| 48 | |
| 49 | m_Count = std::clamp<int>(val: m_Count, lo: 0, hi: MAX_MAPSOUNDS); |
| 50 | |
| 51 | // load new samples |
| 52 | bool ShowWarning = false; |
| 53 | for(int i = 0; i < m_Count; i++) |
| 54 | { |
| 55 | CMapItemSound *pSound = (CMapItemSound *)pMap->GetItem(Index: Start + i); |
| 56 | const char *pName = pMap->GetDataString(Index: pSound->m_SoundName); |
| 57 | if(pName == nullptr || pName[0] == '\0') |
| 58 | { |
| 59 | if(pSound->m_External) |
| 60 | { |
| 61 | log_error("mapsounds" , "Failed to load map sound %d: failed to load name." , i); |
| 62 | ShowWarning = true; |
| 63 | continue; |
| 64 | } |
| 65 | pName = "(error)" ; |
| 66 | } |
| 67 | |
| 68 | if(pSound->m_External) |
| 69 | { |
| 70 | char aBuf[IO_MAX_PATH_LENGTH]; |
| 71 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "mapres/%s.opus" , pName); |
| 72 | m_aSounds[i] = Sound()->LoadOpus(pFilename: aBuf); |
| 73 | pMap->UnloadData(Index: pSound->m_SoundName); |
| 74 | } |
| 75 | else |
| 76 | { |
| 77 | const void *pData = pMap->GetData(Index: pSound->m_SoundData); |
| 78 | if(pData == nullptr) |
| 79 | { |
| 80 | log_error("mapsounds" , "Failed to load map sound %d: failed to load data." , i); |
| 81 | ShowWarning = true; |
| 82 | continue; |
| 83 | } |
| 84 | const int SoundDataSize = pMap->GetDataSize(Index: pSound->m_SoundData); |
| 85 | m_aSounds[i] = Sound()->LoadOpusFromMem(pData, DataSize: SoundDataSize, ForceLoad: false, pContextName: pName); |
| 86 | pMap->UnloadData(Index: pSound->m_SoundData); |
| 87 | } |
| 88 | ShowWarning = ShowWarning || m_aSounds[i] == -1; |
| 89 | } |
| 90 | if(ShowWarning) |
| 91 | { |
| 92 | Client()->AddWarning(Warning: SWarning(Localize(pStr: "Some map sounds could not be loaded. Check the local console for details." ))); |
| 93 | } |
| 94 | |
| 95 | // enqueue sound sources |
| 96 | for(int GroupIndex = 0; GroupIndex < Layers()->NumGroups(); GroupIndex++) |
| 97 | { |
| 98 | const CMapItemGroup *pGroup = Layers()->GetGroup(Index: GroupIndex); |
| 99 | if(!pGroup) |
| 100 | continue; |
| 101 | |
| 102 | for(int LayerIndex = 0; LayerIndex < pGroup->m_NumLayers; LayerIndex++) |
| 103 | { |
| 104 | const CMapItemLayer *pLayer = Layers()->GetLayer(Index: pGroup->m_StartLayer + LayerIndex); |
| 105 | if(!pLayer) |
| 106 | continue; |
| 107 | if(pLayer->m_Type != LAYERTYPE_SOUNDS) |
| 108 | continue; |
| 109 | |
| 110 | const CMapItemLayerSounds *pSoundLayer = reinterpret_cast<const CMapItemLayerSounds *>(pLayer); |
| 111 | if(pSoundLayer->m_Version < 1 || pSoundLayer->m_Version > 2) |
| 112 | continue; |
| 113 | if(pSoundLayer->m_Sound < 0 || pSoundLayer->m_Sound >= m_Count || m_aSounds[pSoundLayer->m_Sound] == -1) |
| 114 | continue; |
| 115 | |
| 116 | const CSoundSource *pSources = static_cast<CSoundSource *>(Layers()->Map()->GetDataSwapped(Index: pSoundLayer->m_Data)); |
| 117 | if(!pSources) |
| 118 | continue; |
| 119 | |
| 120 | const size_t NumSources = minimum<size_t>(a: pSoundLayer->m_NumSources, b: Layers()->Map()->GetDataSize(Index: pSoundLayer->m_Data) / sizeof(CSoundSource)); |
| 121 | for(size_t SourceIndex = 0; SourceIndex < NumSources; SourceIndex++) |
| 122 | { |
| 123 | CSourceQueueEntry Source; |
| 124 | Source.m_Sound = pSoundLayer->m_Sound; |
| 125 | Source.m_HighDetail = pLayer->m_Flags & LAYERFLAG_DETAIL; |
| 126 | Source.m_pGroup = pGroup; |
| 127 | Source.m_pSource = &pSources[SourceIndex]; |
| 128 | m_vSourceQueue.push_back(x: Source); |
| 129 | } |
| 130 | } |
| 131 | } |
| 132 | } |
| 133 | |
| 134 | void CMapSounds::OnRender() |
| 135 | { |
| 136 | if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK) |
| 137 | return; |
| 138 | |
| 139 | bool DemoPlayerPaused = Client()->State() == IClient::STATE_DEMOPLAYBACK && DemoPlayer()->BaseInfo()->m_Paused; |
| 140 | |
| 141 | // enqueue sounds |
| 142 | for(auto &Source : m_vSourceQueue) |
| 143 | { |
| 144 | static float s_Time = 0.0f; |
| 145 | if(GameClient()->m_Snap.m_pGameInfoObj) |
| 146 | { |
| 147 | s_Time = mix(a: (Client()->PrevGameTick(Conn: g_Config.m_ClDummy) - GameClient()->m_Snap.m_pGameInfoObj->m_RoundStartTick) / (float)Client()->GameTickSpeed(), |
| 148 | b: (Client()->GameTick(Conn: g_Config.m_ClDummy) - GameClient()->m_Snap.m_pGameInfoObj->m_RoundStartTick) / (float)Client()->GameTickSpeed(), |
| 149 | amount: Client()->IntraGameTick(Conn: g_Config.m_ClDummy)); |
| 150 | } |
| 151 | float Offset = s_Time - Source.m_pSource->m_TimeDelay; |
| 152 | if(!DemoPlayerPaused && Offset >= 0.0f && g_Config.m_SndEnable && (g_Config.m_GfxHighDetail || !Source.m_HighDetail)) |
| 153 | { |
| 154 | if(Source.m_Voice.IsValid()) |
| 155 | { |
| 156 | // currently playing, set offset |
| 157 | Sound()->SetVoiceTimeOffset(Voice: Source.m_Voice, TimeOffset: Offset); |
| 158 | } |
| 159 | else |
| 160 | { |
| 161 | // need to enqueue |
| 162 | int Flags = 0; |
| 163 | if(Source.m_pSource->m_Loop) |
| 164 | Flags |= ISound::FLAG_LOOP; |
| 165 | if(!Source.m_pSource->m_Pan) |
| 166 | Flags |= ISound::FLAG_NO_PANNING; |
| 167 | |
| 168 | Source.m_Voice = GameClient()->m_Sounds.PlaySampleAt(Channel: CSounds::CHN_MAPSOUND, SampleId: m_aSounds[Source.m_Sound], Flags, Volume: 1.0f, Position: vec2(fx2f(v: Source.m_pSource->m_Position.x), fx2f(v: Source.m_pSource->m_Position.y))); |
| 169 | Sound()->SetVoiceTimeOffset(Voice: Source.m_Voice, TimeOffset: Offset); |
| 170 | Sound()->SetVoiceFalloff(Voice: Source.m_Voice, Falloff: Source.m_pSource->m_Falloff / 255.0f); |
| 171 | switch(Source.m_pSource->m_Shape.m_Type) |
| 172 | { |
| 173 | case CSoundShape::SHAPE_CIRCLE: |
| 174 | { |
| 175 | Sound()->SetVoiceCircle(Voice: Source.m_Voice, Radius: Source.m_pSource->m_Shape.m_Circle.m_Radius); |
| 176 | break; |
| 177 | } |
| 178 | |
| 179 | case CSoundShape::SHAPE_RECTANGLE: |
| 180 | { |
| 181 | Sound()->SetVoiceRectangle(Voice: Source.m_Voice, Width: fx2f(v: Source.m_pSource->m_Shape.m_Rectangle.m_Width), Height: fx2f(v: Source.m_pSource->m_Shape.m_Rectangle.m_Height)); |
| 182 | break; |
| 183 | } |
| 184 | }; |
| 185 | } |
| 186 | } |
| 187 | else |
| 188 | { |
| 189 | // stop voice |
| 190 | Sound()->StopVoice(Voice: Source.m_Voice); |
| 191 | Source.m_Voice = ISound::CVoiceHandle(); |
| 192 | } |
| 193 | } |
| 194 | |
| 195 | const vec2 Center = GameClient()->m_Camera.m_Center; |
| 196 | for(const auto &Source : m_vSourceQueue) |
| 197 | { |
| 198 | if(!Source.m_Voice.IsValid()) |
| 199 | continue; |
| 200 | |
| 201 | ColorRGBA Position = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f); |
| 202 | CEnvelopeState &EnvEvaluator = GameClient()->m_MapLayersBackground.EnvEvaluator(); |
| 203 | EnvEvaluator.EnvelopeEval(TimeOffsetMillis: Source.m_pSource->m_PosEnvOffset, Env: Source.m_pSource->m_PosEnv, Result&: Position, Channels: 2); |
| 204 | |
| 205 | float x = fx2f(v: Source.m_pSource->m_Position.x) + Position.r; |
| 206 | float y = fx2f(v: Source.m_pSource->m_Position.y) + Position.g; |
| 207 | |
| 208 | x += Center.x * (1.0f - Source.m_pGroup->m_ParallaxX / 100.0f); |
| 209 | y += Center.y * (1.0f - Source.m_pGroup->m_ParallaxY / 100.0f); |
| 210 | |
| 211 | x -= Source.m_pGroup->m_OffsetX; |
| 212 | y -= Source.m_pGroup->m_OffsetY; |
| 213 | |
| 214 | Sound()->SetVoicePosition(Voice: Source.m_Voice, Position: vec2(x, y)); |
| 215 | |
| 216 | ColorRGBA Volume = ColorRGBA(1.0f, 0.0f, 0.0f, 0.0f); |
| 217 | EnvEvaluator.EnvelopeEval(TimeOffsetMillis: Source.m_pSource->m_SoundEnvOffset, Env: Source.m_pSource->m_SoundEnv, Result&: Volume, Channels: 1); |
| 218 | if(Volume.r < 1.0f) |
| 219 | { |
| 220 | Sound()->SetVoiceVolume(Voice: Source.m_Voice, Volume: Volume.r); |
| 221 | } |
| 222 | } |
| 223 | } |
| 224 | |
| 225 | void CMapSounds::Clear() |
| 226 | { |
| 227 | // unload all samples |
| 228 | m_vSourceQueue.clear(); |
| 229 | for(int i = 0; i < m_Count; i++) |
| 230 | { |
| 231 | Sound()->UnloadSample(SampleId: m_aSounds[i]); |
| 232 | m_aSounds[i] = -1; |
| 233 | } |
| 234 | m_Count = 0; |
| 235 | } |
| 236 | |
| 237 | void CMapSounds::OnStateChange(int NewState, int OldState) |
| 238 | { |
| 239 | if(NewState < IClient::STATE_ONLINE) |
| 240 | Clear(); |
| 241 | } |
| 242 | |