aboutsummaryrefslogtreecommitdiff
path: root/src/sound_openal.cpp
blob: cb4c7b5818dde9e90746f7843b622240ef1189dd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
/*
Minetest
Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
OpenAL support based on work by:
Copyright (C) 2011 Sebastian 'Bahamada' Rühl
Copyright (C) 2011 Cyriaque 'Cisoun' Skrapits <cysoun@gmail.com>
Copyright (C) 2011 Giuseppe Bilotta <giuseppe.bilotta@gmail.com>

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License along
with this program; ifnot, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/

#include "sound_openal.h"

#if defined(_WIN32)
	#include <al.h>
	#include <alc.h>
	//#include <alext.h>
#elif defined(__APPLE__)
	#include <OpenAL/al.h>
	#include <OpenAL/alc.h>
	//#include <OpenAL/alext.h>
#else
	#include <AL/al.h>
	#include <AL/alc.h>
	#include <AL/alext.h>
#endif
#include <vorbis/vorbisfile.h>
#include <assert.h>
#include "log.h"
#include "filesys.h"
#include "util/numeric.h" // myrand()
#include "porting.h"
#include <map>
#include <vector>
#include <fstream>

#define BUFFER_SIZE 30000

static const char *alcErrorString(ALCenum err)
{
	switch (err) {
	case ALC_NO_ERROR:
		return "no error";
	case ALC_INVALID_DEVICE:
		return "invalid device";
	case ALC_INVALID_CONTEXT:
		return "invalid context";
	case ALC_INVALID_ENUM:
		return "invalid enum";
	case ALC_INVALID_VALUE:
		return "invalid value";
	case ALC_OUT_OF_MEMORY:
		return "out of memory";
	default:
		return "<unknown OpenAL error>";
	}
}

static const char *alErrorString(ALenum err)
{
	switch (err) {
	case AL_NO_ERROR:
		return "no error";
	case AL_INVALID_NAME:
		return "invalid name";
	case AL_INVALID_ENUM:
		return "invalid enum";
	case AL_INVALID_VALUE:
		return "invalid value";
	case AL_INVALID_OPERATION:
		return "invalid operation";
	case AL_OUT_OF_MEMORY:
		return "out of memory";
	default:
		return "<unknown OpenAL error>";
	}
}

static ALenum warn_if_error(ALenum err, const char *desc)
{
	if(err == AL_NO_ERROR)
		return err;
	errorstream<<"WARNING: "<<desc<<": "<<alErrorString(err)<<std::endl;
	return err;
}

void f3_set(ALfloat *f3, v3f v)
{
	f3[0] = v.X;
	f3[1] = v.Y;
	f3[2] = v.Z;
}

struct SoundBuffer
{
	ALenum format;
	ALsizei freq;
	ALuint buffer_id;
	std::vector<char> buffer;
};

SoundBuffer* loadOggFile(const std::string &filepath)
{
	int endian = 0; // 0 for Little-Endian, 1 for Big-Endian
	int bitStream;
	long bytes;
	char array[BUFFER_SIZE]; // Local fixed size array
	vorbis_info *pInfo;
	OggVorbis_File oggFile;
	
	// Do a dumb-ass static string copy for old versions of ov_fopen
	// because they expect a non-const char*
	char nonconst[10000];
	snprintf(nonconst, 10000, "%s", filepath.c_str());
	// Try opening the given file
	//if(ov_fopen(filepath.c_str(), &oggFile) != 0)
	if(ov_fopen(nonconst, &oggFile) != 0)
	{
		infostream<<"Audio: Error opening "<<filepath<<" for decoding"<<std::endl;
		return NULL;
	}

	SoundBuffer *snd = new SoundBuffer;

	// Get some information about the OGG file
	pInfo = ov_info(&oggFile, -1);

	// Check the number of channels... always use 16-bit samples
	if(pInfo->channels == 1)
		snd->format = AL_FORMAT_MONO16;
	else
		snd->format = AL_FORMAT_STEREO16;

	// The frequency of the sampling rate
	snd->freq = pInfo->rate;

	// Keep reading until all is read
	do
	{
		// Read up to a buffer's worth of decoded sound data
		bytes = ov_read(&oggFile, array, BUFFER_SIZE, endian, 2, 1, &bitStream);

		if(bytes < 0)
		{
			ov_clear(&oggFile);
			infostream<<"Audio: Error decoding "<<filepath<<std::endl;
			return NULL;
		}

		// Append to end of buffer
		snd->buffer.insert(snd->buffer.end(), array, array + bytes);
	} while (bytes > 0);

	alGenBuffers(1, &snd->buffer_id);
	alBufferData(snd->buffer_id, snd->format,
			&(snd->buffer[0]), snd->buffer.size(),
			snd->freq);

	ALenum error = alGetError();

	if(error != AL_NO_ERROR){
		infostream<<"Audio: OpenAL error: "<<alErrorString(error)
				<<"preparing sound buffer"<<std::endl;
	}

	infostream<<"Audio file "<<filepath<<" loaded"<<std::endl;

	// Clean up!
	ov_clear(&oggFile);

	return snd;
}

struct PlayingSound
{
	ALuint source_id;
	bool loop;
};

class OpenALSoundManager: public ISoundManager
{
private:
	OnDemandSoundFetcher *m_fetcher;
	ALCdevice *m_device;
	ALCcontext *m_context;
	bool m_can_vorbis;
	int m_next_id;
	std::map<std::string, std::vector<SoundBuffer*> > m_buffers;
	std::map<int, PlayingSound*> m_sounds_playing;
	v3f m_listener_pos;
public:
	bool m_is_initialized;
	OpenALSoundManager(OnDemandSoundFetcher *fetcher):
		m_fetcher(fetcher),
		m_device(NULL),
		m_context(NULL),
		m_can_vorbis(false),
		m_next_id(1),
		m_is_initialized(false)
	{
		ALCenum error = ALC_NO_ERROR;
		
		infostream<<"Audio: Initializing..."<<std::endl;

		m_device = alcOpenDevice(NULL);
		if(!m_device){
			infostream<<"Audio: No audio device available, audio system "
				<<"not initialized"<<std::endl;
			return;
		}

		if(alcIsExtensionPresent(m_device, "EXT_vorbis")){
			infostream<<"Audio: Vorbis extension present"<<std::endl;
			m_can_vorbis = true;
		} else{
			infostream<<"Audio: Vorbis extension NOT present"<<std::endl;
			m_can_vorbis = false;
		}

		m_context = alcCreateContext(m_device, NULL);
		if(!m_context){
			error = alcGetError(m_device);
			infostream<<"Audio: Unable to initialize audio context, "
					<<"aborting audio initialization ("<<alcErrorString(error)
					<<")"<<std::endl;
			alcCloseDevice(m_device);
			m_device = NULL;
			return;
		}

		if(!alcMakeContextCurrent(m_context) ||
				(error = alcGetError(m_device) != ALC_NO_ERROR))
		{
			infostream<<"Audio: Error setting audio context, aborting audio "
					<<"initialization ("<<alcErrorString(error)<<")"<<std::endl;
			alcDestroyContext(m_context);
			m_context = NULL;
			alcCloseDevice(m_device);
			m_device = NULL;
			return;
		}

		alDistanceModel(AL_EXPONENT_DISTANCE);

		infostream<<"Audio: Initialized: OpenAL "<<alGetString(AL_VERSION)
				<<", using "<<alcGetString(m_device, ALC_DEVICE_SPECIFIER)
				<<std::endl;

		m_is_initialized = true;
	}

	~OpenALSoundManager()
	{
		infostream<<"Audio: Deinitializing..."<<std::endl;
		// KABOOM!
		// TODO: Clear SoundBuffers
		alcMakeContextCurrent(NULL);
		alcDestroyContext(m_context);
		m_context = NULL;
		alcCloseDevice(m_device);
		m_device = NULL;

		for (std::map<std::string, std::vector<SoundBuffer*> >::iterator i = m_buffers.begin();
				i != m_buffers.end(); i++) {
			for (std::vector<SoundBuffer*>::iterator iter = (*i).second.begin();
					iter != (*i).second.end(); iter++) {
				delete *iter;
			}
			(*i).second.clear();
		}
		m_buffers.clear();
		infostream<<"Audio: Deinitialized."<<std::endl;
	}
	
	void addBuffer(const std::string &name, SoundBuffer *buf)
	{
		std::map<std::string, std::vector<SoundBuffer*> >::iterator i =
				m_buffers.find(name);
		if(i != m_buffers.end()){
			i->second.push_back(buf);
			return;
		}
		std::vector<SoundBuffer*> bufs;
		bufs.push_back(buf);
		m_buffers[name] = bufs;
		return;
	}

	SoundBuffer* getBuffer(const std::string &name)
	{
		std::map<std::string, std::vector<SoundBuffer*> >::iterator i =
				m_buffers.find(name);
		if(i == m_buffers.end())
			return NULL;
		std::vector<SoundBuffer*> &bufs = i->second;
		int j = myrand() % bufs.size();
		return bufs[j];
	}

	PlayingSound* createPlayingSound(SoundBuffer *buf, bool loop,
			float volume)
	{
		infostream<<"OpenALSoundManager: Creating playing sound"<<std::endl;
		assert(buf);
		PlayingSound *sound = new PlayingSound;
		assert(sound);
		warn_if_error(alGetError(), "before createPlayingSound");
		alGenSources(1, &sound->source_id);
		alSourcei(sound->source_id, AL_BUFFER, buf->buffer_id);
		alSourcei(sound->source_id, AL_SOURCE_RELATIVE, true);
		alSource3f(sound->source_id, AL_POSITION, 0, 0, 0);
		alSource3f(sound->source_id, AL_VELOCITY, 0, 0, 0);
		alSourcei(sound->source_id, AL_LOOPING, loop ? AL_TRUE : AL_FALSE);
		volume = MYMAX(0.0, volume);
		alSourcef(sound->source_id, AL_GAIN, volume);
		alSourcePlay(sound->source_id);
		warn_if_error(alGetError(), "createPlayingSound");
		return sound;
	}

	PlayingSound* createPlayingSoundAt(SoundBuffer *buf, bool loop,
			float volume, v3f pos)
	{
		infostream<<"OpenALSoundManager: Creating positional playing sound"
				<<std::endl;
		assert(buf);
		PlayingSound *sound = new PlayingSound;
		assert(sound);
		warn_if_error(alGetError(), "before createPlayingSoundAt");
		alGenSources(1, &sound->source_id);
		alSourcei(sound->source_id, AL_BUFFER, buf->buffer_id);
		alSourcei(sound->source_id, AL_SOURCE_RELATIVE, false);
		alSource3f(sound->source_id, AL_POSITION, pos.X, pos.Y, pos.Z);
		alSource3f(sound->source_id, AL_VELOCITY, 0, 0, 0);
		//alSourcef(sound->source_id, AL_ROLLOFF_FACTOR, 0.7);
		alSourcef(sound->source_id, AL_REFERENCE_DISTANCE, 30.0);
		alSourcei(sound->source_id, AL_LOOPING, loop ? AL_TRUE : AL_FALSE);
		volume = MYMAX(0.0, volume);
		alSourcef(sound->source_id, AL_GAIN, volume);
		alSourcePlay(sound->source_id);
		warn_if_error(alGetError(), "createPlayingSoundAt");
		return sound;
	}

	int playSoundRaw(SoundBuffer *buf, bool loop, float volume)
	{
		assert(buf);
		PlayingSound *sound = createPlayingSound(buf, loop, volume);
		if(!sound)
			return -1;
		int id = m_next_id++;
		m_sounds_playing[id] = sound;
		return id;
	}

	int playSoundRawAt(SoundBuffer *buf, bool loop, float volume, v3f pos)
	{
		assert(buf);
		PlayingSound *sound = createPlayingSoundAt(buf, loop, volume, pos);
		if(!sound)
			return -1;
		int id = m_next_id++;
		m_sounds_playing[id] = sound;
		return id;
	}
	
	void deleteSound(int id)
	{
		std::map<int, PlayingSound*>::iterator i =
				m_sounds_playing.find(id);
		if(i == m_sounds_playing.end())
			return;
		PlayingSound *sound = i->second;
		
		alDeleteSources(1, &sound->source_id);

		delete sound;
		m_sounds_playing.erase(id);
	}

	/* If buffer does not exist, consult the fetcher */
	SoundBuffer* getFetchBuffer(const std::string &name)
	{
		SoundBuffer *buf = getBuffer(name);
		if(buf)
			return buf;
		if(!m_fetcher)
			return NULL;
		std::set<std::string> paths;
		std::set<std::string> datas;
		m_fetcher->fetchSounds(name, paths, datas);
		for(std::set<std::string>::iterator i = paths.begin();
				i != paths.end(); i++){
			loadSoundFile(name, *i);
		}
		for(std::set<std::string>::iterator i = datas.begin();
				i != datas.end(); i++){
			loadSoundData(name, *i);
		}
		return getBuffer(name);
	}
	
	// Remove stopped sounds
	void maintain()
	{
		verbosestream<<"OpenALSoundManager::maintain(): "
				<<m_sounds_playing.size()<<" playing sounds, "
				<<m_buffers.size()<<" sound names loaded"<<std::endl;
		std::set<int> del_list;
		for(std::map<int, PlayingSound*>::iterator
				i = m_sounds_playing.begin();
				i != m_sounds_playing.end(); i++)
		{
			int id = i->first;
			PlayingSound *sound = i->second;
			// If not playing, remove it
			{
				ALint state;
				alGetSourcei(sound->source_id, AL_SOURCE_STATE, &state);
				if(state != AL_PLAYING){
					del_list.insert(id);
				}
			}
		}
		if(!del_list.empty())
			verbosestream<<"OpenALSoundManager::maintain(): deleting "
					<<del_list.size()<<" playing sounds"<<std::endl;
		for(std::set<int>::iterator i = del_list.begin();
				i != del_list.end(); i++)
		{
			deleteSound(*i);
		}
	}

	/* Interface */

	bool loadSoundFile(const std::string &name,
			const std::string &filepath)
	{
		SoundBuffer *buf = loadOggFile(filepath);
		if(buf)
			addBuffer(name, buf);
		return false;
	}
	bool loadSoundData(const std::string &name,
			const std::string &filedata)
	{
		// The vorbis API sucks; just write it to a file and use vorbisfile
		// TODO: Actually load it directly from memory
		std::string basepath = porting::path_user + DIR_DELIM + "cache" +
				DIR_DELIM + "tmp";
		std::string path = basepath + DIR_DELIM + "tmp.ogg";
		verbosestream<<"OpenALSoundManager::loadSoundData(): Writing "
				<<"temporary file to ["<<path<<"]"<<std::endl;
		fs::CreateAllDirs(basepath);
		std::ofstream of(path.c_str(), std::ios::binary);
		of.write(filedata.c_str(), filedata.size());
		of.close();
		return loadSoundFile(name, path);
	}

	void updateListener(v3f pos, v3f vel, v3f at, v3f up)
	{
		m_listener_pos = pos;
		alListener3f(AL_POSITION, pos.X, pos.Y, pos.Z);
		alListener3f(AL_VELOCITY, vel.X, vel.Y, vel.Z);
		ALfloat f[6];
		f3_set(f, at);
		f3_set(f+3, -up);
		alListenerfv(AL_ORIENTATION, f);
		warn_if_error(alGetError(), "updateListener");
	}
	
	void setListenerGain(float gain)
	{
		alListenerf(AL_GAIN, gain);
	}

	int playSound(const std::string &name, bool loop, float volume)
	{
		maintain();
		if(name == "")
			return 0;
		SoundBuffer *buf = getFetchBuffer(name);
		if(!buf){
			infostream<<"OpenALSoundManager: \""<<name<<"\" not found."
					<<std::endl;
			return -1;
		}
		return playSoundRaw(buf, loop, volume);
	}
	int playSoundAt(const std::string &name, bool loop, float volume, v3f pos)
	{
		maintain();
		if(name == "")
			return 0;
		SoundBuffer *buf = getFetchBuffer(name);
		if(!buf){
			infostream<<"OpenALSoundManager: \""<<name<<"\" not found."
					<<std::endl;
			return -1;
		}
		return playSoundRawAt(buf, loop, volume, pos);
	}
	void stopSound(int sound)
	{
		maintain();
		deleteSound(sound);
	}
	bool soundExists(int sound)
	{
		maintain();
		return (m_sounds_playing.count(sound) != 0);
	}
	void updateSoundPosition(int id, v3f pos)
	{
		std::map<int, PlayingSound*>::iterator i =
				m_sounds_playing.find(id);
		if(i == m_sounds_playing.end())
			return;
		PlayingSound *sound = i->second;

		alSourcei(sound->source_id, AL_SOURCE_RELATIVE, false);
		alSource3f(sound->source_id, AL_POSITION, pos.X, pos.Y, pos.Z);
		alSource3f(sound->source_id, AL_VELOCITY, 0, 0, 0);
		alSourcef(sound->source_id, AL_REFERENCE_DISTANCE, 30.0);
	}
};

ISoundManager *createOpenALSoundManager(OnDemandSoundFetcher *fetcher)
{
	OpenALSoundManager *m = new OpenALSoundManager(fetcher);
	if(m->m_is_initialized)
		return m;
	delete m;
	return NULL;
};

"database/database-dummy.h" #include "database/database-files.h" #include "database/database-sqlite3.h" #if USE_POSTGRESQL #include "database/database-postgresql.h" #endif #include <algorithm> #define LBM_NAME_ALLOWED_CHARS "abcdefghijklmnopqrstuvwxyz0123456789_:" // A number that is much smaller than the timeout for particle spawners should/could ever be #define PARTICLE_SPAWNER_NO_EXPIRY -1024.f /* ABMWithState */ ABMWithState::ABMWithState(ActiveBlockModifier *abm_): abm(abm_) { // Initialize timer to random value to spread processing float itv = abm->getTriggerInterval(); itv = MYMAX(0.001, itv); // No less than 1ms int minval = MYMAX(-0.51*itv, -60); // Clamp to int maxval = MYMIN(0.51*itv, 60); // +-60 seconds timer = myrand_range(minval, maxval); } /* LBMManager */ void LBMContentMapping::deleteContents() { for (auto &it : lbm_list) { delete it; } } void LBMContentMapping::addLBM(LoadingBlockModifierDef *lbm_def, IGameDef *gamedef) { // Add the lbm_def to the LBMContentMapping. // Unknown names get added to the global NameIdMapping. const NodeDefManager *nodedef = gamedef->ndef(); lbm_list.push_back(lbm_def); for (const std::string &nodeTrigger: lbm_def->trigger_contents) { std::vector<content_t> c_ids; bool found = nodedef->getIds(nodeTrigger, c_ids); if (!found) { content_t c_id = gamedef->allocateUnknownNodeId(nodeTrigger); if (c_id == CONTENT_IGNORE) { // Seems it can't be allocated. warningstream << "Could not internalize node name \"" << nodeTrigger << "\" while loading LBM \"" << lbm_def->name << "\"." << std::endl; continue; } c_ids.push_back(c_id); } for (content_t c_id : c_ids) { map[c_id].push_back(lbm_def); } } } const std::vector<LoadingBlockModifierDef *> * LBMContentMapping::lookup(content_t c) const { lbm_map::const_iterator it = map.find(c); if (it == map.end()) return NULL; // This first dereferences the iterator, returning // a std::vector<LoadingBlockModifierDef *> // reference, then we convert it to a pointer. return &(it->second); } LBMManager::~LBMManager() { for (auto &m_lbm_def : m_lbm_defs) { delete m_lbm_def.second; } for (auto &it : m_lbm_lookup) { (it.second).deleteContents(); } } void LBMManager::addLBMDef(LoadingBlockModifierDef *lbm_def) { // Precondition, in query mode the map isn't used anymore FATAL_ERROR_IF(m_query_mode, "attempted to modify LBMManager in query mode"); if (!string_allowed(lbm_def->name, LBM_NAME_ALLOWED_CHARS)) { throw ModError("Error adding LBM \"" + lbm_def->name + "\": Does not follow naming conventions: " "Only characters [a-z0-9_:] are allowed."); } m_lbm_defs[lbm_def->name] = lbm_def; } void LBMManager::loadIntroductionTimes(const std::string &times, IGameDef *gamedef, u32 now) { m_query_mode = true; // name -> time map. // Storing it in a map first instead of // handling the stuff directly in the loop // removes all duplicate entries. // TODO make this std::unordered_map std::map<std::string, u32> introduction_times; /* The introduction times string consists of name~time entries, with each entry terminated by a semicolon. The time is decimal. */ size_t idx = 0; size_t idx_new; while ((idx_new = times.find(';', idx)) != std::string::npos) { std::string entry = times.substr(idx, idx_new - idx); std::vector<std::string> components = str_split(entry, '~'); if (components.size() != 2) throw SerializationError("Introduction times entry \"" + entry + "\" requires exactly one '~'!"); const std::string &name = components[0]; u32 time = from_string<u32>(components[1]); introduction_times[name] = time; idx = idx_new + 1; } // Put stuff from introduction_times into m_lbm_lookup for (std::map<std::string, u32>::const_iterator it = introduction_times.begin(); it != introduction_times.end(); ++it) { const std::string &name = it->first; u32 time = it->second; std::map<std::string, LoadingBlockModifierDef *>::iterator def_it = m_lbm_defs.find(name); if (def_it == m_lbm_defs.end()) { // This seems to be an LBM entry for // an LBM we haven't loaded. Discard it. continue; } LoadingBlockModifierDef *lbm_def = def_it->second; if (lbm_def->run_at_every_load) { // This seems to be an LBM entry for // an LBM that runs at every load. // Don't add it just yet. continue; } m_lbm_lookup[time].addLBM(lbm_def, gamedef); // Erase the entry so that we know later // what elements didn't get put into m_lbm_lookup m_lbm_defs.erase(name); } // Now also add the elements from m_lbm_defs to m_lbm_lookup // that weren't added in the previous step. // They are introduced first time to this world, // or are run at every load (introducement time hardcoded to U32_MAX). LBMContentMapping &lbms_we_introduce_now = m_lbm_lookup[now]; LBMContentMapping &lbms_running_always = m_lbm_lookup[U32_MAX]; for (auto &m_lbm_def : m_lbm_defs) { if (m_lbm_def.second->run_at_every_load) { lbms_running_always.addLBM(m_lbm_def.second, gamedef); } else { lbms_we_introduce_now.addLBM(m_lbm_def.second, gamedef); } } // Clear the list, so that we don't delete remaining elements // twice in the destructor m_lbm_defs.clear(); } std::string LBMManager::createIntroductionTimesString() { // Precondition, we must be in query mode FATAL_ERROR_IF(!m_query_mode, "attempted to query on non fully set up LBMManager"); std::ostringstream oss; for (const auto &it : m_lbm_lookup) { u32 time = it.first; const std::vector<LoadingBlockModifierDef *> &lbm_list = it.second.lbm_list; for (const auto &lbm_def : lbm_list) { // Don't add if the LBM runs at every load, // then introducement time is hardcoded // and doesn't need to be stored if (lbm_def->run_at_every_load) continue; oss << lbm_def->name << "~" << time << ";"; } } return oss.str(); } void LBMManager::applyLBMs(ServerEnvironment *env, MapBlock *block, u32 stamp) { // Precondition, we need m_lbm_lookup to be initialized FATAL_ERROR_IF(!m_query_mode, "attempted to query on non fully set up LBMManager"); v3s16 pos_of_block = block->getPosRelative(); v3s16 pos; MapNode n; content_t c; lbm_lookup_map::const_iterator it = getLBMsIntroducedAfter(stamp); for (; it != m_lbm_lookup.end(); ++it) { // Cache previous version to speedup lookup which has a very high performance // penalty on each call content_t previous_c{}; std::vector<LoadingBlockModifierDef *> *lbm_list = nullptr; for (pos.X = 0; pos.X < MAP_BLOCKSIZE; pos.X++) for (pos.Y = 0; pos.Y < MAP_BLOCKSIZE; pos.Y++) for (pos.Z = 0; pos.Z < MAP_BLOCKSIZE; pos.Z++) { n = block->getNodeNoEx(pos); c = n.getContent(); // If content_t are not matching perform an LBM lookup if (previous_c != c) { lbm_list = (std::vector<LoadingBlockModifierDef *> *) it->second.lookup(c); previous_c = c; } if (!lbm_list) continue; for (auto lbmdef : *lbm_list) { lbmdef->trigger(env, pos + pos_of_block, n); } } } } /* ActiveBlockList */ void fillRadiusBlock(v3s16 p0, s16 r, std::set<v3s16> &list) { v3s16 p; for(p.X=p0.X-r; p.X<=p0.X+r; p.X++) for(p.Y=p0.Y-r; p.Y<=p0.Y+r; p.Y++) for(p.Z=p0.Z-r; p.Z<=p0.Z+r; p.Z++) { // limit to a sphere if (p.getDistanceFrom(p0) <= r) { // Set in list list.insert(p); } } } void fillViewConeBlock(v3s16 p0, const s16 r, const v3f camera_pos, const v3f camera_dir, const float camera_fov, std::set<v3s16> &list) { v3s16 p; const s16 r_nodes = r * BS * MAP_BLOCKSIZE; for (p.X = p0.X - r; p.X <= p0.X+r; p.X++) for (p.Y = p0.Y - r; p.Y <= p0.Y+r; p.Y++) for (p.Z = p0.Z - r; p.Z <= p0.Z+r; p.Z++) { if (isBlockInSight(p, camera_pos, camera_dir, camera_fov, r_nodes)) { list.insert(p); } } } void ActiveBlockList::update(std::vector<PlayerSAO*> &active_players, s16 active_block_range, s16 active_object_range, std::set<v3s16> &blocks_removed, std::set<v3s16> &blocks_added) { /* Create the new list */ std::set<v3s16> newlist = m_forceloaded_list; m_abm_list = m_forceloaded_list; for (const PlayerSAO *playersao : active_players) { v3s16 pos = getNodeBlockPos(floatToInt(playersao->getBasePosition(), BS)); fillRadiusBlock(pos, active_block_range, m_abm_list); fillRadiusBlock(pos, active_block_range, newlist); s16 player_ao_range = std::min(active_object_range, playersao->getWantedRange()); // only do this if this would add blocks if (player_ao_range > active_block_range) { v3f camera_dir = v3f(0,0,1); camera_dir.rotateYZBy(playersao->getLookPitch()); camera_dir.rotateXZBy(playersao->getRotation().Y); fillViewConeBlock(pos, player_ao_range, playersao->getEyePosition(), camera_dir, playersao->getFov(), newlist); } } /* Find out which blocks on the old list are not on the new list */ // Go through old list for (v3s16 p : m_list) { // If not on new list, it's been removed if (newlist.find(p) == newlist.end()) blocks_removed.insert(p); } /* Find out which blocks on the new list are not on the old list */ // Go through new list for (v3s16 p : newlist) { // If not on old list, it's been added if(m_list.find(p) == m_list.end()) blocks_added.insert(p); } /* Update m_list */ m_list.clear(); for (v3s16 p : newlist) { m_list.insert(p); } } /* ServerEnvironment */ // Random device to seed pseudo random generators. static std::random_device seed; ServerEnvironment::ServerEnvironment(ServerMap *map, ServerScripting *scriptIface, Server *server, const std::string &path_world): Environment(server), m_map(map), m_script(scriptIface), m_server(server), m_path_world(path_world), m_rgen(seed()) { // Determine which database backend to use std::string conf_path = path_world + DIR_DELIM + "world.mt"; Settings conf; std::string player_backend_name = "sqlite3"; std::string auth_backend_name = "sqlite3"; bool succeeded = conf.readConfigFile(conf_path.c_str()); // If we open world.mt read the backend configurations. if (succeeded) { // Read those values before setting defaults bool player_backend_exists = conf.exists("player_backend"); bool auth_backend_exists = conf.exists("auth_backend"); // player backend is not set, assume it's legacy file backend. if (!player_backend_exists) { // fall back to files conf.set("player_backend", "files"); player_backend_name = "files"; if (!conf.updateConfigFile(conf_path.c_str())) { errorstream << "ServerEnvironment::ServerEnvironment(): " << "Failed to update world.mt!" << std::endl; } } else { conf.getNoEx("player_backend", player_backend_name); } // auth backend is not set, assume it's legacy file backend. if (!auth_backend_exists) { conf.set("auth_backend", "files"); auth_backend_name = "files"; if (!conf.updateConfigFile(conf_path.c_str())) { errorstream << "ServerEnvironment::ServerEnvironment(): " << "Failed to update world.mt!" << std::endl; } } else { conf.getNoEx("auth_backend", auth_backend_name); } } if (player_backend_name == "files") { warningstream << "/!\\ You are using old player file backend. " << "This backend is deprecated and will be removed in a future release /!\\" << std::endl << "Switching to SQLite3 or PostgreSQL is advised, " << "please read http://wiki.minetest.net/Database_backends." << std::endl; } if (auth_backend_name == "files") { warningstream << "/!\\ You are using old auth file backend. " << "This backend is deprecated and will be removed in a future release /!\\" << std::endl << "Switching to SQLite3 is advised, " << "please read http://wiki.minetest.net/Database_backends." << std::endl; } m_player_database = openPlayerDatabase(player_backend_name, path_world, conf); m_auth_database = openAuthDatabase(auth_backend_name, path_world, conf); } ServerEnvironment::~ServerEnvironment() { // Clear active block list. // This makes the next one delete all active objects. m_active_blocks.clear(); // Convert all objects to static and delete the active objects deactivateFarObjects(true); // Drop/delete map m_map->drop(); // Delete ActiveBlockModifiers for (ABMWithState &m_abm : m_abms) { delete m_abm.abm; } // Deallocate players for (RemotePlayer *m_player : m_players) { delete m_player; } delete m_player_database; delete m_auth_database; } Map & ServerEnvironment::getMap() { return *m_map; } ServerMap & ServerEnvironment::getServerMap() { return *m_map; } RemotePlayer *ServerEnvironment::getPlayer(const session_t peer_id) { for (RemotePlayer *player : m_players) { if (player->getPeerId() == peer_id) return player; } return NULL; } RemotePlayer *ServerEnvironment::getPlayer(const char* name) { for (RemotePlayer *player : m_players) { if (strcmp(player->getName(), name) == 0) return player; } return NULL; } void ServerEnvironment::addPlayer(RemotePlayer *player) { /* Check that peer_ids are unique. Also check that names are unique. Exception: there can be multiple players with peer_id=0 */ // If peer id is non-zero, it has to be unique. if (player->getPeerId() != PEER_ID_INEXISTENT) FATAL_ERROR_IF(getPlayer(player->getPeerId()) != NULL, "Peer id not unique"); // Name has to be unique. FATAL_ERROR_IF(getPlayer(player->getName()) != NULL, "Player name not unique"); // Add. m_players.push_back(player); } void ServerEnvironment::removePlayer(RemotePlayer *player) { for (std::vector<RemotePlayer *>::iterator it = m_players.begin(); it != m_players.end(); ++it) { if ((*it) == player) { delete *it; m_players.erase(it); return; } } } bool ServerEnvironment::removePlayerFromDatabase(const std::string &name) { return m_player_database->removePlayer(name); } bool ServerEnvironment::line_of_sight(v3f pos1, v3f pos2, v3s16 *p) { // Iterate trough nodes on the line voxalgo::VoxelLineIterator iterator(pos1 / BS, (pos2 - pos1) / BS); do { MapNode n = getMap().getNode(iterator.m_current_node_pos); // Return non-air if (n.param0 != CONTENT_AIR) { if (p) *p = iterator.m_current_node_pos; return false; } iterator.next(); } while (iterator.m_current_index <= iterator.m_last_index); return true; } void ServerEnvironment::kickAllPlayers(AccessDeniedCode reason, const std::string &str_reason, bool reconnect) { for (RemotePlayer *player : m_players) { m_server->DenyAccessVerCompliant(player->getPeerId(), player->protocol_version, reason, str_reason, reconnect); } } void ServerEnvironment::saveLoadedPlayers(bool force) { for (RemotePlayer *player : m_players) { if (force || player->checkModified() || (player->getPlayerSAO() && player->getPlayerSAO()->getMeta().isModified())) { try { m_player_database->savePlayer(player); } catch (DatabaseException &e) { errorstream << "Failed to save player " << player->getName() << " exception: " << e.what() << std::endl; throw; } } } } void ServerEnvironment::savePlayer(RemotePlayer *player) { try { m_player_database->savePlayer(player); } catch (DatabaseException &e) { errorstream << "Failed to save player " << player->getName() << " exception: " << e.what() << std::endl; throw; } } PlayerSAO *ServerEnvironment::loadPlayer(RemotePlayer *player, bool *new_player, session_t peer_id, bool is_singleplayer) { PlayerSAO *playersao = new PlayerSAO(this, player, peer_id, is_singleplayer); // Create player if it doesn't exist if (!m_player_database->loadPlayer(player, playersao)) { *new_player = true; // Set player position infostream << "Server: Finding spawn place for player \"" << player->getName() << "\"" << std::endl; playersao->setBasePosition(m_server->findSpawnPos()); // Make sure the player is saved player->setModified(true); } else { // If the player exists, ensure that they respawn inside legal bounds // This fixes an assert crash when the player can't be added // to the environment if (objectpos_over_limit(playersao->getBasePosition())) { actionstream << "Respawn position for player \"" << player->getName() << "\" outside limits, resetting" << std::endl; playersao->setBasePosition(m_server->findSpawnPos()); } } // Add player to environment addPlayer(player); /* Clean up old HUD elements from previous sessions */ player->clearHud(); /* Add object to environment */ addActiveObject(playersao); return playersao; } void ServerEnvironment::saveMeta() { std::string path = m_path_world + DIR_DELIM "env_meta.txt"; // Open file and serialize std::ostringstream ss(std::ios_base::binary); Settings args; args.setU64("game_time", m_game_time); args.setU64("time_of_day", getTimeOfDay()); args.setU64("last_clear_objects_time", m_last_clear_objects_time); args.setU64("lbm_introduction_times_version", 1); args.set("lbm_introduction_times", m_lbm_mgr.createIntroductionTimesString()); args.setU64("day_count", m_day_count); args.writeLines(ss); ss<<"EnvArgsEnd\n"; if(!fs::safeWriteToFile(path, ss.str())) { infostream<<"ServerEnvironment::saveMeta(): Failed to write " <<path<<std::endl; throw SerializationError("Couldn't save env meta"); } } void ServerEnvironment::loadMeta() { // If file doesn't exist, load default environment metadata if (!fs::PathExists(m_path_world + DIR_DELIM "env_meta.txt")) { infostream << "ServerEnvironment: Loading default environment metadata" << std::endl; loadDefaultMeta(); return; } infostream << "ServerEnvironment: Loading environment metadata" << std::endl; std::string path = m_path_world + DIR_DELIM "env_meta.txt"; // Open file and deserialize std::ifstream is(path.c_str(), std::ios_base::binary); if (!is.good()) { infostream << "ServerEnvironment::loadMeta(): Failed to open " << path << std::endl; throw SerializationError("Couldn't load env meta"); } Settings args; if (!args.parseConfigLines(is, "EnvArgsEnd")) { throw SerializationError("ServerEnvironment::loadMeta(): " "EnvArgsEnd not found!"); } try { m_game_time = args.getU64("game_time"); } catch (SettingNotFoundException &e) { // Getting this is crucial, otherwise timestamps are useless throw SerializationError("Couldn't load env meta game_time"); } setTimeOfDay(args.exists("time_of_day") ? // set day to early morning by default args.getU64("time_of_day") : 5250); m_last_clear_objects_time = args.exists("last_clear_objects_time") ? // If missing, do as if clearObjects was never called args.getU64("last_clear_objects_time") : 0; std::string lbm_introduction_times; try { u64 ver = args.getU64("lbm_introduction_times_version"); if (ver == 1) { lbm_introduction_times = args.get("lbm_introduction_times"); } else { infostream << "ServerEnvironment::loadMeta(): Non-supported" << " introduction time version " << ver << std::endl; } } catch (SettingNotFoundException &e) { // No problem, this is expected. Just continue with an empty string } m_lbm_mgr.loadIntroductionTimes(lbm_introduction_times, m_server, m_game_time); m_day_count = args.exists("day_count") ? args.getU64("day_count") : 0; } /** * called if env_meta.txt doesn't exist (e.g. new world) */ void ServerEnvironment::loadDefaultMeta() { m_lbm_mgr.loadIntroductionTimes("", m_server, m_game_time); } struct ActiveABM { ActiveBlockModifier *abm; int chance; std::vector<content_t> required_neighbors; bool check_required_neighbors; // false if required_neighbors is known to be empty }; class ABMHandler { private: ServerEnvironment *m_env; std::vector<std::vector<ActiveABM> *> m_aabms; public: ABMHandler(std::vector<ABMWithState> &abms, float dtime_s, ServerEnvironment *env, bool use_timers): m_env(env) { if(dtime_s < 0.001) return; const NodeDefManager *ndef = env->getGameDef()->ndef(); for (ABMWithState &abmws : abms) { ActiveBlockModifier *abm = abmws.abm; float trigger_interval = abm->getTriggerInterval(); if(trigger_interval < 0.001) trigger_interval = 0.001; float actual_interval = dtime_s; if(use_timers){ abmws.timer += dtime_s; if(abmws.timer < trigger_interval) continue; abmws.timer -= trigger_interval; actual_interval = trigger_interval; } float chance = abm->getTriggerChance(); if(chance == 0) chance = 1; ActiveABM aabm; aabm.abm = abm; if (abm->getSimpleCatchUp()) { float intervals = actual_interval / trigger_interval; if(intervals == 0) continue; aabm.chance = chance / intervals; if(aabm.chance == 0) aabm.chance = 1; } else { aabm.chance = chance; } // Trigger neighbors const std::vector<std::string> &required_neighbors_s = abm->getRequiredNeighbors(); for (const std::string &required_neighbor_s : required_neighbors_s) { ndef->getIds(required_neighbor_s, aabm.required_neighbors); } aabm.check_required_neighbors = !required_neighbors_s.empty(); // Trigger contents const std::vector<std::string> &contents_s = abm->getTriggerContents(); for (const std::string &content_s : contents_s) { std::vector<content_t> ids; ndef->getIds(content_s, ids); for (content_t c : ids) { if (c >= m_aabms.size()) m_aabms.resize(c + 256, NULL); if (!m_aabms[c]) m_aabms[c] = new std::vector<ActiveABM>; m_aabms[c]->push_back(aabm); } } } } ~ABMHandler() { for (auto &aabms : m_aabms) delete aabms; } // Find out how many objects the given block and its neighbours contain. // Returns the number of objects in the block, and also in 'wider' the // number of objects in the block and all its neighbours. The latter // may an estimate if any neighbours are unloaded. u32 countObjects(MapBlock *block, ServerMap * map, u32 &wider) { wider = 0; u32 wider_unknown_count = 0; for(s16 x=-1; x<=1; x++) for(s16 y=-1; y<=1; y++) for(s16 z=-1; z<=1; z++) { MapBlock *block2 = map->getBlockNoCreateNoEx( block->getPos() + v3s16(x,y,z)); if(block2==NULL){ wider_unknown_count++; continue; } wider += block2->m_static_objects.m_active.size() + block2->m_static_objects.m_stored.size(); } // Extrapolate u32 active_object_count = block->m_static_objects.m_active.size(); u32 wider_known_count = 3*3*3 - wider_unknown_count; wider += wider_unknown_count * wider / wider_known_count; return active_object_count; } void apply(MapBlock *block, int &blocks_scanned, int &abms_run, int &blocks_cached) { if(m_aabms.empty() || block->isDummy()) return; // Check the content type cache first // to see whether there are any ABMs // to be run at all for this block. if (block->contents_cached) { blocks_cached++; bool run_abms = false; for (content_t c : block->contents) { if (c < m_aabms.size() && m_aabms[c]) { run_abms = true; break; } } if (!run_abms) return; } else { // Clear any caching block->contents.clear(); } blocks_scanned++; ServerMap *map = &m_env->getServerMap(); u32 active_object_count_wider; u32 active_object_count = this->countObjects(block, map, active_object_count_wider); m_env->m_added_objects = 0; v3s16 p0; for(p0.X=0; p0.X<MAP_BLOCKSIZE; p0.X++) for(p0.Y=0; p0.Y<MAP_BLOCKSIZE; p0.Y++) for(p0.Z=0; p0.Z<MAP_BLOCKSIZE; p0.Z++) { const MapNode &n = block->getNodeUnsafe(p0); content_t c = n.getContent(); // Cache content types as we go if (!block->contents_cached && !block->do_not_cache_contents) { block->contents.insert(c); if (block->contents.size() > 64) { // Too many different nodes... don't try to cache block->do_not_cache_contents = true; block->contents.clear(); } } if (c >= m_aabms.size() || !m_aabms[c]) continue; v3s16 p = p0 + block->getPosRelative(); for (ActiveABM &aabm : *m_aabms[c]) { if (myrand() % aabm.chance != 0) continue; // Check neighbors if (aabm.check_required_neighbors) { v3s16 p1; for(p1.X = p0.X-1; p1.X <= p0.X+1; p1.X++) for(p1.Y = p0.Y-1; p1.Y <= p0.Y+1; p1.Y++) for(p1.Z = p0.Z-1; p1.Z <= p0.Z+1; p1.Z++) { if(p1 == p0) continue; content_t c; if (block->isValidPosition(p1)) { // if the neighbor is found on the same map block // get it straight from there const MapNode &n = block->getNodeUnsafe(p1); c = n.getContent(); } else { // otherwise consult the map MapNode n = map->getNode(p1 + block->getPosRelative()); c = n.getContent(); } if (CONTAINS(aabm.required_neighbors, c)) goto neighbor_found; } // No required neighbor found continue; } neighbor_found: abms_run++; // Call all the trigger variations aabm.abm->trigger(m_env, p, n); aabm.abm->trigger(m_env, p, n, active_object_count, active_object_count_wider); // Count surrounding objects again if the abms added any if(m_env->m_added_objects > 0) { active_object_count = countObjects(block, map, active_object_count_wider); m_env->m_added_objects = 0; } } } block->contents_cached = !block->do_not_cache_contents; } }; void ServerEnvironment::activateBlock(MapBlock *block, u32 additional_dtime) { // Reset usage timer immediately, otherwise a block that becomes active // again at around the same time as it would normally be unloaded will // get unloaded incorrectly. (I think this still leaves a small possibility // of a race condition between this and server::AsyncRunStep, which only // some kind of synchronisation will fix, but it at least reduces the window // of opportunity for it to break from seconds to nanoseconds) block->resetUsageTimer(); // Get time difference u32 dtime_s = 0; u32 stamp = block->getTimestamp(); if (m_game_time > stamp && stamp != BLOCK_TIMESTAMP_UNDEFINED) dtime_s = m_game_time - stamp; dtime_s += additional_dtime; /*infostream<<"ServerEnvironment::activateBlock(): block timestamp: " <<stamp<<", game time: "<<m_game_time<<std::endl;*/ // Remove stored static objects if clearObjects was called since block's timestamp if (stamp == BLOCK_TIMESTAMP_UNDEFINED || stamp < m_last_clear_objects_time) { block->m_static_objects.m_stored.clear(); // do not set changed flag to avoid unnecessary mapblock writes } // Set current time as timestamp block->setTimestampNoChangedFlag(m_game_time); /*infostream<<"ServerEnvironment::activateBlock(): block is " <<dtime_s<<" seconds old."<<std::endl;*/ // Activate stored objects activateObjects(block, dtime_s); /* Handle LoadingBlockModifiers */ m_lbm_mgr.applyLBMs(this, block, stamp); // Run node timers std::vector<NodeTimer> elapsed_timers = block->m_node_timers.step((float)dtime_s); if (!elapsed_timers.empty()) { MapNode n; for (const NodeTimer &elapsed_timer : elapsed_timers) { n = block->getNodeNoEx(elapsed_timer.position); v3s16 p = elapsed_timer.position + block->getPosRelative(); if (m_script->node_on_timer(p, n, elapsed_timer.elapsed)) block->setNodeTimer(NodeTimer(elapsed_timer.timeout, 0, elapsed_timer.position)); } } } void ServerEnvironment::addActiveBlockModifier(ActiveBlockModifier *abm) { m_abms.emplace_back(abm); } void ServerEnvironment::addLoadingBlockModifierDef(LoadingBlockModifierDef *lbm) { m_lbm_mgr.addLBMDef(lbm); } bool ServerEnvironment::setNode(v3s16 p, const MapNode &n) { const NodeDefManager *ndef = m_server->ndef(); MapNode n_old = m_map->getNode(p); const ContentFeatures &cf_old = ndef->get(n_old); // Call destructor if (cf_old.has_on_destruct) m_script->node_on_destruct(p, n_old); // Replace node if (!m_map->addNodeWithEvent(p, n)) return false; // Update active VoxelManipulator if a mapgen thread m_map->updateVManip(p); // Call post-destructor if (cf_old.has_after_destruct) m_script->node_after_destruct(p, n_old); // Retrieve node content features // if new node is same as old, reuse old definition to prevent a lookup const ContentFeatures &cf_new = n_old == n ? cf_old : ndef->get(n); // Call constructor if (cf_new.has_on_construct) m_script->node_on_construct(p, n); return true; } bool ServerEnvironment::removeNode(v3s16 p) { const NodeDefManager *ndef = m_server->ndef(); MapNode n_old = m_map->getNode(p); // Call destructor if (ndef->get(n_old).has_on_destruct) m_script->node_on_destruct(p, n_old); // Replace with air // This is slightly optimized compared to addNodeWithEvent(air) if (!m_map->removeNodeWithEvent(p)) return false; // Update active VoxelManipulator if a mapgen thread m_map->updateVManip(p); // Call post-destructor if (ndef->get(n_old).has_after_destruct) m_script->node_after_destruct(p, n_old); // Air doesn't require constructor return true; } bool ServerEnvironment::swapNode(v3s16 p, const MapNode &n) { if (!m_map->addNodeWithEvent(p, n, false)) return false; // Update active VoxelManipulator if a mapgen thread m_map->updateVManip(p); return true; } void ServerEnvironment::clearObjects(ClearObjectsMode mode) { infostream << "ServerEnvironment::clearObjects(): " << "Removing all active objects" << std::endl; auto cb_removal = [this] (ServerActiveObject *obj, u16 id) { if (obj->getType() == ACTIVEOBJECT_TYPE_PLAYER) return false; // Delete static object if block is loaded deleteStaticFromBlock(obj, id, MOD_REASON_CLEAR_ALL_OBJECTS, true); // If known by some client, don't delete immediately if (obj->m_known_by_count > 0) { obj->m_pending_removal = true; return false; } // Tell the object about removal obj->removingFromEnvironment(); // Deregister in scripting api m_script->removeObjectReference(obj); // Delete active object if (obj->environmentDeletes()) delete obj; return true; }; m_ao_manager.clear(cb_removal); // Get list of loaded blocks std::vector<v3s16> loaded_blocks; infostream << "ServerEnvironment::clearObjects(): " << "Listing all loaded blocks" << std::endl; m_map->listAllLoadedBlocks(loaded_blocks); infostream << "ServerEnvironment::clearObjects(): " << "Done listing all loaded blocks: " << loaded_blocks.size()<<std::endl; // Get list of loadable blocks std::vector<v3s16> loadable_blocks; if (mode == CLEAR_OBJECTS_MODE_FULL) { infostream << "ServerEnvironment::clearObjects(): " << "Listing all loadable blocks" << std::endl; m_map->listAllLoadableBlocks(loadable_blocks); infostream << "ServerEnvironment::clearObjects(): " << "Done listing all loadable blocks: " << loadable_blocks.size() << std::endl; } else { loadable_blocks = loaded_blocks; } actionstream << "ServerEnvironment::clearObjects(): " << "Now clearing objects in " << loadable_blocks.size() << " blocks" << std::endl; // Grab a reference on each loaded block to avoid unloading it for (v3s16 p : loaded_blocks) { MapBlock *block = m_map->getBlockNoCreateNoEx(p); assert(block != NULL); block->refGrab(); } // Remove objects in all loadable blocks u32 unload_interval = U32_MAX; if (mode == CLEAR_OBJECTS_MODE_FULL) { unload_interval = g_settings->getS32("max_clearobjects_extra_loaded_blocks"); unload_interval = MYMAX(unload_interval, 1); } u32 report_interval = loadable_blocks.size() / 10; u32 num_blocks_checked = 0; u32 num_blocks_cleared = 0; u32 num_objs_cleared = 0; for (auto i = loadable_blocks.begin(); i != loadable_blocks.end(); ++i) { v3s16 p = *i; MapBlock *block = m_map->emergeBlock(p, false); if (!block) { errorstream << "ServerEnvironment::clearObjects(): " << "Failed to emerge block " << PP(p) << std::endl; continue; } u32 num_stored = block->m_static_objects.m_stored.size(); u32 num_active = block->m_static_objects.m_active.size(); if (num_stored != 0 || num_active != 0) { block->m_static_objects.m_stored.clear(); block->m_static_objects.m_active.clear(); block->raiseModified(MOD_STATE_WRITE_NEEDED, MOD_REASON_CLEAR_ALL_OBJECTS); num_objs_cleared += num_stored + num_active; num_blocks_cleared++; } num_blocks_checked++; if (report_interval != 0 && num_blocks_checked % report_interval == 0) { float percent = 100.0 * (float)num_blocks_checked / loadable_blocks.size(); actionstream << "ServerEnvironment::clearObjects(): " << "Cleared " << num_objs_cleared << " objects" << " in " << num_blocks_cleared << " blocks (" << percent << "%)" << std::endl; } if (num_blocks_checked % unload_interval == 0) { m_map->unloadUnreferencedBlocks(); } } m_map->unloadUnreferencedBlocks(); // Drop references that were added above for (v3s16 p : loaded_blocks) { MapBlock *block = m_map->getBlockNoCreateNoEx(p); assert(block); block->refDrop(); } m_last_clear_objects_time = m_game_time; actionstream << "ServerEnvironment::clearObjects(): " << "Finished: Cleared " << num_objs_cleared << " objects" << " in " << num_blocks_cleared << " blocks" << std::endl; } void ServerEnvironment::step(float dtime) { ScopeProfiler sp2(g_profiler, "ServerEnv::step()", SPT_AVG); /* Step time of day */ stepTimeOfDay(dtime); // Update this one // NOTE: This is kind of funny on a singleplayer game, but doesn't // really matter that much. static thread_local const float server_step = g_settings->getFloat("dedicated_server_step"); m_recommended_send_interval = server_step; /* Increment game time */ { m_game_time_fraction_counter += dtime; u32 inc_i = (u32)m_game_time_fraction_counter; m_game_time += inc_i; m_game_time_fraction_counter -= (float)inc_i; } /* Handle players */ { ScopeProfiler sp(g_profiler, "ServerEnv: move players", SPT_AVG); for (RemotePlayer *player : m_players) { // Ignore disconnected players if (player->getPeerId() == PEER_ID_INEXISTENT) continue; // Move player->move(dtime, this, 100 * BS); } } /* Manage active block list */ if (m_active_blocks_management_interval.step(dtime, m_cache_active_block_mgmt_interval)) { ScopeProfiler sp(g_profiler, "ServerEnv: update active blocks", SPT_AVG); /* Get player block positions */ std::vector<PlayerSAO*> players; for (RemotePlayer *player: m_players) { // Ignore disconnected players if (player->getPeerId() == PEER_ID_INEXISTENT) continue; PlayerSAO *playersao = player->getPlayerSAO(); assert(playersao); players.push_back(playersao); } /* Update list of active blocks, collecting changes */ // use active_object_send_range_blocks since that is max distance // for active objects sent the client anyway static thread_local const s16 active_object_range = g_settings->getS16("active_object_send_range_blocks"); static thread_local const s16 active_block_range = g_settings->getS16("active_block_range"); std::set<v3s16> blocks_removed; std::set<v3s16> blocks_added; m_active_blocks.update(players, active_block_range, active_object_range, blocks_removed, blocks_added); /* Handle removed blocks */ // Convert active objects that are no more in active blocks to static deactivateFarObjects(false); for (const v3s16 &p: blocks_removed) { MapBlock *block = m_map->getBlockNoCreateNoEx(p); if (!block) continue; // Set current time as timestamp (and let it set ChangedFlag) block->setTimestamp(m_game_time); } /* Handle added blocks */ for (const v3s16 &p: blocks_added) { MapBlock *block = m_map->getBlockOrEmerge(p); if (!block) { m_active_blocks.m_list.erase(p); m_active_blocks.m_abm_list.erase(p); continue; } activateBlock(block); } } /* Mess around in active blocks */ if (m_active_blocks_nodemetadata_interval.step(dtime, m_cache_nodetimer_interval)) { ScopeProfiler sp(g_profiler, "ServerEnv: Run node timers", SPT_AVG); float dtime = m_cache_nodetimer_interval; for (const v3s16 &p: m_active_blocks.m_list) { MapBlock *block = m_map->getBlockNoCreateNoEx(p); if (!block) continue; // Reset block usage timer block->resetUsageTimer(); // Set current time as timestamp block->setTimestampNoChangedFlag(m_game_time); // If time has changed much from the one on disk, // set block to be saved when it is unloaded if(block->getTimestamp() > block->getDiskTimestamp() + 60) block->raiseModified(MOD_STATE_WRITE_AT_UNLOAD, MOD_REASON_BLOCK_EXPIRED); // Run node timers std::vector<NodeTimer> elapsed_timers = block->m_node_timers.step(dtime); if (!elapsed_timers.empty()) { MapNode n; v3s16 p2; for (const NodeTimer &elapsed_timer: elapsed_timers) { n = block->getNodeNoEx(elapsed_timer.position); p2 = elapsed_timer.position + block->getPosRelative(); if (m_script->node_on_timer(p2, n, elapsed_timer.elapsed)) { block->setNodeTimer(NodeTimer( elapsed_timer.timeout, 0, elapsed_timer.position)); } } } } } if (m_active_block_modifier_interval.step(dtime, m_cache_abm_interval)) { ScopeProfiler sp(g_profiler, "SEnv: modify in blocks avg per interval", SPT_AVG); TimeTaker timer("modify in active blocks per interval"); // Initialize handling of ActiveBlockModifiers ABMHandler abmhandler(m_abms, m_cache_abm_interval, this, true); int blocks_scanned = 0; int abms_run = 0; int blocks_cached = 0; std::vector<v3s16> output(m_active_blocks.m_abm_list.size()); // Shuffle the active blocks so that each block gets an equal chance // of having its ABMs run. std::copy(m_active_blocks.m_abm_list.begin(), m_active_blocks.m_abm_list.end(), output.begin()); std::shuffle(output.begin(), output.end(), m_rgen); int i = 0; // The time budget for ABMs is 20%. u32 max_time_ms = m_cache_abm_interval * 1000 / 5; for (const v3s16 &p : output) { MapBlock *block = m_map->getBlockNoCreateNoEx(p); if (!block) continue; i++; // Set current time as timestamp block->setTimestampNoChangedFlag(m_game_time); /* Handle ActiveBlockModifiers */ abmhandler.apply(block, blocks_scanned, abms_run, blocks_cached); u32 time_ms = timer.getTimerTime(); if (time_ms > max_time_ms) { warningstream << "active block modifiers took " << time_ms << "ms (processed " << i << " of " << output.size() << " active blocks)" << std::endl; break; } } g_profiler->avg("ServerEnv: active blocks", m_active_blocks.m_abm_list.size()); g_profiler->avg("ServerEnv: active blocks cached", blocks_cached); g_profiler->avg("ServerEnv: active blocks scanned for ABMs", blocks_scanned); g_profiler->avg("ServerEnv: ABMs run", abms_run); timer.stop(true); } /* Step script environment (run global on_step()) */ m_script->environment_Step(dtime); /* Step active objects */ { ScopeProfiler sp(g_profiler, "ServerEnv: Run SAO::step()", SPT_AVG); // This helps the objects to send data at the same time bool send_recommended = false; m_send_recommended_timer += dtime; if (m_send_recommended_timer > getSendRecommendedInterval()) { m_send_recommended_timer -= getSendRecommendedInterval(); send_recommended = true; } auto cb_state = [this, dtime, send_recommended] (ServerActiveObject *obj) { if (obj->isGone()) return; // Step object obj->step(dtime, send_recommended); // Read messages from object while (!obj->m_messages_out.empty()) { this->m_active_object_messages.push(obj->m_messages_out.front()); obj->m_messages_out.pop(); } }; m_ao_manager.step(dtime, cb_state); } /* Manage active objects */ if (m_object_management_interval.step(dtime, 0.5)) { removeRemovedObjects(); } /* Manage particle spawner expiration */ if (m_particle_management_interval.step(dtime, 1.0)) { for (std::unordered_map<u32, float>::iterator i = m_particle_spawners.begin(); i != m_particle_spawners.end(); ) { //non expiring spawners if (i->second == PARTICLE_SPAWNER_NO_EXPIRY) { ++i; continue; } i->second -= 1.0f; if (i->second <= 0.f) m_particle_spawners.erase(i++); else ++i; } } // Send outdated player inventories for (RemotePlayer *player : m_players) { if (player->getPeerId() == PEER_ID_INEXISTENT) continue; PlayerSAO *sao = player->getPlayerSAO(); if (sao && player->inventory.checkModified()) m_server->SendInventory(sao, true); } // Send outdated detached inventories m_server->sendDetachedInventories(PEER_ID_INEXISTENT, true); } u32 ServerEnvironment::addParticleSpawner(float exptime) { // Timers with lifetime 0 do not expire float time = exptime > 0.f ? exptime : PARTICLE_SPAWNER_NO_EXPIRY; u32 id = 0; for (;;) { // look for unused particlespawner id id++; std::unordered_map<u32, float>::iterator f = m_particle_spawners.find(id); if (f == m_particle_spawners.end()) { m_particle_spawners[id] = time; break; } } return id; } u32 ServerEnvironment::addParticleSpawner(float exptime, u16 attached_id) { u32 id = addParticleSpawner(exptime); m_particle_spawner_attachments[id] = attached_id; if (ServerActiveObject *obj = getActiveObject(attached_id)) { obj->attachParticleSpawner(id); } return id; } void ServerEnvironment::deleteParticleSpawner(u32 id, bool remove_from_object) { m_particle_spawners.erase(id); const auto &it = m_particle_spawner_attachments.find(id); if (it != m_particle_spawner_attachments.end()) { u16 obj_id = it->second; ServerActiveObject *sao = getActiveObject(obj_id); if (sao != NULL && remove_from_object) { sao->detachParticleSpawner(id); } m_particle_spawner_attachments.erase(id); } } u16 ServerEnvironment::addActiveObject(ServerActiveObject *object) { assert(object); // Pre-condition m_added_objects++; u16 id = addActiveObjectRaw(object, true, 0); return id; } /* Finds out what new objects have been added to inside a radius around a position */ void ServerEnvironment::getAddedActiveObjects(PlayerSAO *playersao, s16 radius, s16 player_radius,