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
15CMapSounds::CMapSounds()
16{
17 m_Count = 0;
18}
19
20void 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
28void 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
36void 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
134void 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
225void 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
237void CMapSounds::OnStateChange(int NewState, int OldState)
238{
239 if(NewState < IClient::STATE_ONLINE)
240 Clear();
241}
242