OpenGothic
Open source reimplementation of Gothic I and II
Loading...
Searching...
No Matches
worldsound.cpp
Go to the documentation of this file.
1#include "worldsound.h"
2
3#include <Tempest/SoundEffect>
4
5#include "camera.h"
7#include "game/gamesession.h"
9#include "world/objects/npc.h"
10#include "world/objects/sound.h"
11#include "sound/soundfx.h"
12#include "utils/string_frm.h"
13#include "world.h"
14#include "gamemusic.h"
15#include "gothic.h"
16#include "resources.h"
17
18const float WorldSound::maxDist = 7000; // 70 meters
19const float WorldSound::talkRange = 2000;
20
21struct WorldSound::WSound final {
23 const SoundFx* eff0 = nullptr;
24 const SoundFx* eff1 = nullptr;
25
26 std::string vobName;
27 Tempest::Vec3 pos;
28 float sndRadius = 2500;
29
30 bool loop = false;
31 bool active = false;
32 uint64_t delay = 0;
33 uint64_t delayVar = 0;
34 uint64_t restartTimeout = 0;
35
38 };
39
40struct WorldSound::Zone final {
41 Tempest::Vec3 bbox[2]={};
42 std::string name;
43 bool checkPos(float x,float y,float z) const {
44 return
45 bbox[0].x <= x && x<bbox[1].x &&
46 bbox[0].y <= y && y<bbox[1].y &&
47 bbox[0].z <= z && z<bbox[1].z;
48 }
49 };
50
51void WorldSound::Effect::setOcclusion(float v) {
52 occ = v;
53 eff.setVolume(occ*vol);
54 }
55
56void WorldSound::Effect::setVolume(float v) {
57 vol = v;
58 eff.setVolume(occ*vol);
59 }
60
62 :game(game), owner(owner) {
63 plPos = {-1000000,-1000000,-1000000};
64 effect.reserve(256);
65 }
66
69
70void WorldSound::setDefaultZone(const zenkit::VZoneMusic &vob) {
71 def.reset(new Zone());
72 def->bbox[0] = {vob.bbox.min.x, vob.bbox.min.y, vob.bbox.min.z};
73 def->bbox[1] = {vob.bbox.max.x, vob.bbox.max.y, vob.bbox.max.z};
74 def->name = vob.vob_name;
75 }
76
77void WorldSound::addZone(const zenkit::VZoneMusic &vob) {
78 Zone z;
79 z.bbox[0] = {vob.bbox.min.x, vob.bbox.min.y, vob.bbox.min.z};
80 z.bbox[1] = {vob.bbox.max.x, vob.bbox.max.y, vob.bbox.max.z};
81 z.name = vob.vob_name;
82
83 zones.emplace_back(std::move(z));
84 }
85
86void WorldSound::addSound(const zenkit::VSound &vob) {
87 WSound s;
88 s.vobName = vob.vob_name;
89 s.loop = vob.mode==zenkit::SoundMode::LOOP;
90 s.active = vob.initially_playing;
91 s.delay = uint64_t(vob.random_delay * 1000);
92 s.delayVar = uint64_t(vob.random_delay_var * 1000);
93 s.eff0 = Gothic::inst().loadSoundFx(vob.sound_name);
94
95 s.pos = {vob.position.x,vob.position.y,vob.position.z};
96 s.sndRadius = vob.radius;
97
98 if(vob.type==zenkit::VirtualObjectType::zCVobSoundDaytime) {
99 auto& prDay = (const zenkit::VSoundDaytime&) vob;
100 float b = prDay.start_time;
101 float e = prDay.end_time;
102
103 s.sndStart = gtime(int(b),int(b*60)%60);
104 s.sndEnd = gtime(int(e),int(e*60)%60);
105 s.eff1 = Gothic::inst().loadSoundFx(prDay.sound_name2);
106 } else {
107 s.sndStart = gtime(0,0);
108 s.sndEnd = gtime(24,0);
109 }
110
111 worldEff.emplace_back(std::move(s));
112 }
113
114Sound WorldSound::addDlgSound(std::string_view s, const Tempest::Vec3& pos, float range, uint64_t& timeLen) {
115 if(!isInListenerRange(pos,range))
116 return Sound();
117 auto snd = Resources::loadSoundBuffer(s);
118 if(snd.isEmpty())
119 return Sound();
120
121 auto ret = implAddSound(game.loadSound(snd), pos,range);
122 if(ret.isEmpty())
123 return Sound();
124
125 std::lock_guard<std::mutex> guard(sync);
126 initSlot(*ret.val);
127 timeLen = snd.timeLength();
128 effect.emplace_back(ret.val);
129 return ret;
130 }
131
132Sound WorldSound::implAddSound(const SoundFx& eff, const Tempest::Vec3& pos, float rangeMax) {
133 bool loop = false;
134 auto ret = implAddSound(game.loadSound(eff,loop), pos,rangeMax);
135 ret.setLooping(loop);
136 return ret;
137 }
138
139Sound WorldSound::implAddSound(Tempest::SoundEffect&& eff, const Tempest::Vec3& pos, float rangeMax) {
140 if(eff.isEmpty())
141 return Sound();
142 auto ex = std::make_shared<Effect>();
143 eff.setPosition(pos);
144 eff.setMaxDistance(rangeMax);
145
146 ex->eff = std::move(eff);
147 ex->pos = pos;
148 ex->vol = ex->eff.volume();
149 ex->maxDist = rangeMax;
150 ex->setOcclusion(0);
151
152 return Sound(ex);
153 }
154
155void WorldSound::tick(Npc& player) {
156 std::lock_guard<std::mutex> guard(sync);
157
158 auto cx = game.camera().listenerPosition();
159 plPos = cx.pos;
160
161 game.updateListenerPos(cx);
162
163 for(auto& i:worldEff) {
164 if(!i.active || !i.current.isFinished())
165 continue;
166 if(i.current.isFinished())
167 i.current = Sound();
168
169 if(i.restartTimeout>owner.tickCount() && !i.loop)
170 continue;
171
172 if(!isInListenerRange(i.pos,i.sndRadius))
173 continue;
174
175 auto time = owner.time();
176 time = gtime(0,time.hour(),time.minute());
177
178 const SoundFx* snd = nullptr;
179 if(i.sndStart<= time && time<i.sndEnd) {
180 snd = i.eff0;
181 } else {
182 snd = i.eff1;
183 }
184
185 if(snd==nullptr)
186 continue;
187
188 i.current = implAddSound(*snd,i.pos,i.sndRadius);
189 if(!i.current.isEmpty()) {
190 effect.emplace_back(i.current.val);
191 i.current.play();
192 }
193
194 i.restartTimeout = owner.tickCount() + i.delay;
195 if(i.delayVar>0)
196 i.restartTimeout += uint64_t(std::rand())%i.delayVar;
197
198 if(!i.loop)
199 i.active = false;
200 }
201
202 tickSlot(effect);
203 tickSlot(effect3d);
204 for(auto& i:freeSlot)
205 tickSlot(*i.second);
206 tickSoundZone(player);
207 }
208
210 bool emitted=false;
211 for(auto& i:worldEff)
212 if(i.vobName==e.target) {
213 i.active = true;
214 emitted = true;
215 }
216 return emitted;
217 }
218
219void WorldSound::tickSoundZone(Npc& player) {
220 if(owner.tickCount()<nextSoundUpdate)
221 return;
222 nextSoundUpdate = owner.tickCount()+5*1000;
223
224 Zone* zone = def.get();
225 if(currentZone!=nullptr &&
226 currentZone->checkPos(plPos.x,plPos.y+player.translateY(),plPos.z)){
227 zone = currentZone;
228 } else {
229 for(auto& z:zones) {
230 if(z.checkPos(plPos.x,plPos.y+player.translateY(),plPos.z)) {
231 zone = &z;
232 }
233 }
234 }
235
236 gtime time = owner.time().timeInDay();
237 bool isDay = (gtime(4,0)<=time && time<=gtime(21,0));
238 bool isFgt = owner.isTargeted(player) || player.isDead();
239
241 if(isFgt) {
242 if(player.weaponState()==WeaponState::NoWeapon) {
243 mode = GameMusic::Thr;
244 } else {
245 mode = GameMusic::Fgt;
246 }
247 }
248 GameMusic::Tags tags = GameMusic::mkTags(isDay ? GameMusic::Day : GameMusic::Ngt,mode);
249
250 if(currentZone==zone && currentTags==tags)
251 return;
252
253 currentZone = zone;
254 currentTags = tags;
255
256 Zone* zTry[] = {zone, def.get()};
259
260 // multi-fallback strategy
261 for(auto zone:zTry)
262 for(auto day:dayTry)
263 for(auto mode:modeTry) {
264 const size_t sep = zone->name.find('_');
265 const char* tag = zone->name.c_str();
266 if(sep!=std::string::npos)
267 tag = tag+sep+1;
268
269 tags = GameMusic::mkTags(day,mode);
270 if(setMusic(tag,tags))
271 return;
272 }
273 }
274
275void WorldSound::tickSlot(std::vector<PEffect>& effect) {
276 for(size_t i=0;i<effect.size();) {
277 auto& e = *effect[i];
278 if(e.eff.isFinished() && !(e.loop && e.active)){
279 effect[i]=std::move(effect.back());
280 effect.pop_back();
281 } else {
282 ++i;
283 }
284 }
285 for(auto& i:effect) {
286 tickSlot(*i);
287 }
288 }
289
290void WorldSound::tickSlot(Effect& slot) {
291 if(slot.eff.isFinished()) {
292 if(!slot.loop)
293 return;
294 slot.eff.play();
295 }
296
297 if(slot.ambient) {
298 slot.setOcclusion(1.f);
299 } else {
300 auto dyn = owner.physic();
301 auto head = plPos;
302 auto pos = slot.pos;
303 float occ = 1;
304
305 if((pos-head).quadLength()<slot.maxDist*slot.maxDist)
306 occ = dyn->soundOclusion(head, pos);
307 slot.setOcclusion(std::max(0.f,1.f-occ));
308 }
309 }
310
311void WorldSound::initSlot(WorldSound::Effect& slot) {
312 auto dyn = owner.physic();
313 auto pos = slot.pos;
314 float occ = dyn->soundOclusion(plPos, pos);
315 slot.setOcclusion(std::max(0.f,1.f-occ));
316 }
317
318bool WorldSound::setMusic(std::string_view zone, GameMusic::Tags tags) {
319 bool isDay = (tags&GameMusic::Ngt)==0;
320 std::string_view smode = "STD";
321 if(tags&GameMusic::Thr)
322 smode = "THR";
323 if(tags&GameMusic::Fgt)
324 smode = "FGT";
325
326 string_frm name(zone,'_',(isDay ? "DAY" : "NGT"),'_',smode);
327 if(auto* theme = Gothic::musicDef()[name]) {
328 GameMusic::inst().setMusic(*theme,tags);
329 return true;
330 }
331 return false;
332 }
333
334bool WorldSound::isInListenerRange(const Tempest::Vec3& pos, float sndRgn) const {
335 float dist = sndRgn+800;
336 return (pos-plPos).quadLength()<dist*dist;
337 }
338
339bool WorldSound::canSeeSource(const Tempest::Vec3& p) const {
340 auto dyn = owner.physic();
341 for(auto& i:effect3d) {
342 auto rc = dyn->ray(p, i->pos);
343 if(!rc.hasCol)
344 return true;
345 }
346 return false;
347 }
348
349void WorldSound::aiOutput(const Tempest::Vec3& pos, std::string_view outputname) {
351 std::lock_guard<std::mutex> guard(sync);
353 }
354 }
ListenerPos listenerPosition() const
Definition camera.cpp:446
float soundOclusion(const Tempest::Vec3 &from, const Tempest::Vec3 &to) const
RayLandResult ray(const Tempest::Vec3 &from, const Tempest::Vec3 &to) const
static Tags mkTags(Tags daytime, Tags mode)
static GameMusic & inst()
void setMusic(Music m)
Camera & camera()
Definition gamesession.h:48
void updateListenerPos(const Camera::ListenerPos &lpos)
auto loadSound(const Tempest::Sound &raw) -> Tempest::SoundEffect
static Gothic & inst()
Definition gothic.cpp:249
SoundFx * loadSoundFx(std::string_view name)
Definition gothic.cpp:343
static const MusicDefinitions & musicDef()
Definition gothic.cpp:659
void emitGlobalSound(std::string_view sfx)
Definition gothic.cpp:395
Definition npc.h:25
auto weaponState() const -> WeaponState
Definition npc.cpp:3621
float translateY() const
Definition npc.cpp:680
bool isDead() const
Definition npc.cpp:3962
static Tempest::Sound loadSoundBuffer(std::string_view name)
Definition sound.h:5
std::string target
void tick(Npc &player)
bool canSeeSource(const Tempest::Vec3 &npc) const
Sound addDlgSound(std::string_view s, const Tempest::Vec3 &pos, float range, uint64_t &timeLen)
friend class Sound
Definition worldsound.h:92
void setDefaultZone(const zenkit::VZoneMusic &vob)
bool isInListenerRange(const Tempest::Vec3 &pos, float sndRgn) const
WorldSound(GameSession &game, World &world)
bool execTriggerEvent(const TriggerEvent &e)
static const float talkRange
Definition worldsound.h:40
void addSound(const zenkit::VSound &vob)
void addZone(const zenkit::VZoneMusic &vob)
void aiOutput(const Tempest::Vec3 &pos, std::string_view outputname)
Definition world.h:31
DynamicWorld * physic() const
Definition world.h:81
bool isTargeted(Npc &npc)
Definition world.cpp:586
uint64_t tickCount() const
Definition world.cpp:387
gtime time() const
Definition world.cpp:405
gtime timeInDay() const
Definition gametime.h:18
const SoundFx * eff0
Tempest::Vec3 pos
const SoundFx * eff1
std::string vobName
uint64_t restartTimeout
bool checkPos(float x, float y, float z) const
Tempest::Vec3 bbox[2]
std::string name