Files
assaultcube-/source/src/audiomanager.cpp
T
ac-stef d0b309320b really cleanup sound
- collect most sound infos in one table and use it to generate the menus
- no more sound category checks with ranges
- now only PAINx and FOOTSTEP* sounds use hardcoded index numbers

enumsounds <list of categories>

  Lists sound index numbers and short descriptions for all sounds of one
  or more categories. The argument should contain one or more of PAIN,
  OWNPAIN, WEAPON, PICKUP, MOVEMENT, BULLET, OTHER, VOICECOM, TEAM,
  PUBLIC, FFA, FLAGONLY. All sounds with at least one of the flags will
  be listed. Flag names prefixed with "!" will exclude all matching
  sound from the list.

  Example: /echo (enumsounds "VOICECOM !PUBLIC") will list all voicecom
  sounds that are for teams only.

DOCREF enumsounds
2017-05-22 17:30:51 +02:00

1105 lines
31 KiB
C++

// audio interface of the engine
#include "cube.h"
#define DEBUGCOND (audiodebug==1)
VARF(audio, 0, 1, 1, initwarning("sound configuration", INIT_RESET, CHANGE_SOUND));
VARP(audiodebug, 0, 0, 1);
char *musicdonecmd = NULL;
VARFP(musicvol, 0, 128, 255, audiomgr.setmusicvol(musicvol));
// audio manager
audiomanager::audiomanager()
{
nosound = true;
currentpitch = 1.0f;
device = NULL;
context = NULL;
}
void audiomanager::initsound()
{
if(!audio)
{
conoutf("audio is disabled");
return;
}
nosound = true;
device = NULL;
context = NULL;
// list available devices
if(alcIsExtensionPresent(NULL, "ALC_ENUMERATION_EXT"))
{
const ALCchar *devices = alcGetString(NULL, ALC_DEVICE_SPECIFIER);
if(devices)
{
string d;
copystring(d, "Audio devices: ");
// null separated device string
for(const ALchar *c = devices; *c; c += strlen(c)+1)
{
if(c!=devices) concatstring(d, ", ");
concatstring(d, c);
}
conoutf("%s", d);
}
}
// open device
const char *devicename = getalias("openaldevice");
device = alcOpenDevice(devicename && devicename[0] ? devicename : NULL);
if(device)
{
context = alcCreateContext(device, NULL);
if(context)
{
alcMakeContextCurrent(context);
alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED);
// backend infos
conoutf("Sound: %s / %s (%s)", alcGetString(device, ALC_DEVICE_SPECIFIER), alGetString(AL_RENDERER), alGetString(AL_VENDOR));
conoutf("Driver: %s", alGetString(AL_VERSION));
// allocate OpenAL resources
sourcescheduler::instance().init(16);
// let the stream get the first source from the scheduler
gamemusic = new oggstream();
if(!gamemusic->valid) DELETEP(gamemusic);
setmusicvol(musicvol);
nosound = false;
}
}
if(nosound)
{
ALERR;
if(context) alcDestroyContext(context);
if(device) alcCloseDevice(device);
conoutf("sound initialization failed!");
}
}
void audiomanager::music(char *name, int millis, char *cmd)
{
if(nosound) return;
stopsound();
if(musicvol && *name)
{
if(cmd[0]) musicdonecmd = newstring(cmd);
if(gamemusic->open(name))
{
// fade
if(millis > 0)
{
const int fadetime = 1000;
gamemusic->fadein(lastmillis, fadetime);
gamemusic->fadeout(lastmillis+millis, fadetime);
}
// play
bool loop = cmd && cmd[0];
if(!gamemusic->playback(loop))
{
conoutf("could not play music: %s", name);
return;
}
setmusicvol(musicvol);
}
else conoutf("could not open music: %s", name);
}
}
void audiomanager::musicpreload(int id)
{
if(nosound) return;
stopsound();
if(musicvol && (id>=M_FLAGGRAB && id<=M_LASTMINUTE2) && musics.inrange(id))
{
char *name = musics[id];
conoutf("preloading music #%d : %s", id, name);
if(gamemusic->open(name))
{
/*defformatstring(whendone)("musicvol %d", musicvol);
musicdonecmd = newstring(whendone);
conoutf("when done: %s", musicdonecmd);*/
const int preloadfadetime = 3;
gamemusic->fadein(lastmillis, preloadfadetime);
gamemusic->fadeout(lastmillis+2*preloadfadetime, preloadfadetime);
if(!gamemusic->playback(false))
{
conoutf("could not play music: %s", name);
return;
}
setmusicvol(1); // not 0 !
}
else conoutf("could not open music: %s", name);
}
else setmusicvol(musicvol); // call "musicpreload -1" to ensure musicdonecmd runs - but it should w/o that
}
void audiomanager::musicsuggest(int id, int millis, bool rndofs) // play bg music if nothing else is playing
{
if(nosound || !gamemusic) return;
if(gamemusic->playing()) return;
if(!musicvol) return;
if(!musics.inrange(id))
{
conoutf("\f3music %d not registered", id);
return;
}
char *name = musics[id];
if(gamemusic->open(name))
{
gamemusic->fadein(lastmillis, 1000);
gamemusic->fadeout(millis ? lastmillis+millis : 0, 1000);
if(rndofs) gamemusic->seek(millis ? (double)rnd(millis)/2.0f : (double)lastmillis);
if(!gamemusic->playback(rndofs)) conoutf("could not play music: %s", name);
}
else conoutf("could not open music: %s", name);
}
void audiomanager::musicfadeout(int id)
{
if(nosound || !gamemusic) return;
if(!gamemusic->playing() || !musics.inrange(id)) return;
if(!strcmp(musics[id], gamemusic->name)) gamemusic->fadeout(lastmillis+1000, 1000);
}
void audiomanager::setmusicvol(int musicvol)
{
if(gamemusic) gamemusic->setvolume(musicvol > 0 ? musicvol/255.0f : 0);
}
void reloadmapsoundconfig();
void audiomanager::setlistenervol(int vol)
{
static int oldvol = 0;
if(!nosound)
{
alListenerf(AL_GAIN, vol/255.0f);
if(vol && !oldvol && !mapsounds.length()) reloadmapsoundconfig();
}
oldvol = vol;
}
void audiomanager::registermusic(char *name)
{
if(nosound||!musicvol) return;
if(!name || !name[0]) return;
musics.add(newstring(name));
}
int audiomanager::findsound(const char *name, int vol, vector<soundconfig> &sounds)
{
loopv(sounds)
{
if(!strcmp(sounds[i].buf->name, name) && (!vol || sounds[i].vol==vol)) return i;
}
return -1;
}
int audiomanager::addsound(const char *name, int vol, int maxuses, bool loop, vector<soundconfig> &sounds, bool load, int audibleradius)
{
if(nosound) return -1;
if(!soundvol) return -1;
// check if the sound was already registered
int index = findsound(name, vol, sounds);
if(index > -1) return index;
sbuffer *b = bufferpool.find(name);
if(!b)
{
conoutf("\f3failed to allocate sample %s", name);
return -1;
}
if(load && !b->load()) conoutf("\f3failed to load sample %s", name);
soundconfig s(b, vol, maxuses, loop, audibleradius);
sounds.add(s);
return sounds.length()-1;
}
bool audiomanager::preloadmapsound(entity &e, bool trydl)
{
if(e.type!=SOUND || !mapsounds.inrange(e.attr1)) return false;
sbuffer *buf = mapsounds[e.attr1].buf;
if(!buf->load(trydl) && !trydl) { conoutf("\f3failed to load sample %s", buf->name); return false; }
return true;
}
bool audiomanager::preloadmapsounds(bool trydl)
{
int missing = 0;
loopv(ents)
{
entity &e = ents[i];
if(e.type == SOUND && !preloadmapsound(e, trydl)) missing++;
}
return !missing;
}
void audiomanager::applymapsoundchanges() // during map editing, drop all mapsounds so they can be re-added
{
loopv(locations)
{
location *l = locations[i];
if(l && l->ref && l->ref->type==worldobjreference::WR_ENTITY) l->drop();
}
}
void audiomanager::setchannels(int num)
{
if(!nosound) sourcescheduler::instance().init(num);
};
// called at game exit
void audiomanager::soundcleanup()
{
if(nosound) return;
// destroy consuming code
stopsound();
DELETEP(gamemusic);
mapsounds.shrink(0);
locations.deletecontents();
gamesounds.shrink(0);
// kill scheduler
sourcescheduler::instance().reset();
bufferpool.clear();
// shutdown openal
alcMakeContextCurrent(NULL);
if(context) alcDestroyContext(context);
if(device) alcCloseDevice(device);
}
// clear world-related sounds, called on mapchange
void audiomanager::clearworldsounds(bool fullclean)
{
stopsound();
if(fullclean) mapsounds.shrink(0);
locations.deleteworldobjsounds();
}
void audiomanager::mapsoundreset()
{
mapsounds.shrink(0);
locations.deleteworldobjsounds();
}
VARP(footsteps, 0, 1, 1);
VARP(localfootsteps, 0, 1, 1);
void audiomanager::updateplayerfootsteps(playerent *p)
{
if(!p) return;
const int footstepradius = 20;
// find existing footstep sounds
physentreference ref(p);
location *locs[] =
{
locations.find(S_FOOTSTEPS, &ref, gamesounds),
locations.find(S_FOOTSTEPSCROUCH, &ref, gamesounds),
locations.find(S_WATERFOOTSTEPS, &ref, gamesounds),
locations.find(S_WATERFOOTSTEPSCROUCH, &ref, gamesounds)
};
bool local = (p == camera1);
bool inrange = footsteps && (local || (camera1->o.dist(p->o) < footstepradius));
if(!footsteps || (local && !localfootsteps) || !inrange || p->state != CS_ALIVE || lastmillis-p->lastpain < 300 || (!p->onfloor && p->timeinair>50) || (!p->move && !p->strafe) || p->inwater)
{
const int minplaytime = 200;
loopi(sizeof(locs)/sizeof(locs[0]))
{
location *l = locs[i];
if(!l) continue;
if(l->playmillis+minplaytime>totalmillis) continue; // tolerate short interruptions by enforcing a minimal playtime
l->drop();
}
}
else
{
// play footsteps
float grounddist = 0;
if(!((int(p->o.x) | int(p->o.y)) & ~(ssize - 1))) grounddist = waterlevel - S((int)p->o.x, (int)p->o.y)->floor;
bool water = p->o.z - p->eyeheight+0.25f < waterlevel;
if(water && grounddist > p->eyeheight) return; // don't play step sound when jumping into water
int stepsound;
if(p->crouching) stepsound = water ? S_WATERFOOTSTEPSCROUCH : S_FOOTSTEPSCROUCH; // crouch
else stepsound = water ? S_WATERFOOTSTEPS : S_FOOTSTEPS; // normal
// proc existing sounds
bool isplaying = false;
loopi(sizeof(locs)/sizeof(locs[0]))
{
location *l = locs[i];
if(!l) continue;
if(i+S_FOOTSTEPS==stepsound) isplaying = true; // already playing
else l->drop(); // different footstep sound, drop it
}
if(!isplaying)
{
// play
float rndoffset = float(rnd(500))/500.0f;
audiomgr._playsound(stepsound, ref, local ? SP_HIGH : SP_LOW, rndoffset);
}
}
}
// manage looping sounds
location *audiomanager::updateloopsound(int sound, bool active, float vol)
{
location *l = locations.find(sound, NULL, gamesounds);
if(!l && active) l = _playsound(sound, camerareference(), SP_HIGH, 0.0f, true);
else if(l && !active) l->drop();
if(l && vol != 1.0f) l->src->gain(vol);
return l;
}
VARP(mapsoundrefresh, 0, 10, 1000);
void audiomanager::mutesound(int n, int off)
{
bool mute = (off == 0);
if(!gamesounds.inrange(n))
{
conoutf("\f3could not %s sound #%d", mute ? "silence" : "unmute", n);
return;
}
gamesounds[n].muted = mute;
}
void audiomanager::unmuteallsounds()
{
loopv(gamesounds) gamesounds[i].muted = false;
}
int audiomanager::soundmuted(int n)
{
return gamesounds.inrange(n) && !gamesounds[n].muted ? 0 : 1;
}
void audiomanager::writesoundconfig(stream *f)
{
bool wrotesound = false;
loopv(gamesounds)
{
if(gamesounds[i].muted)
{
if(!wrotesound)
{
f->printf("// sound settings\n\n");
wrotesound = true;
}
f->printf("mutesound %d\n", i);
}
}
}
void voicecom(char *sound, char *text)
{
if(!sound || !sound[0]) return;
if(!text || !text[0]) return;
static int last = 0;
if(!last || lastmillis-last > 2000)
{
defformatstring(soundpath)("voicecom/%s", sound);
int s = audiomgr.findsound(soundpath, 0, gamesounds);
if(!gamesound_isvoicecom(s)) return;
if(voicecomsounds>0) audiomgr.playsound(s, SP_HIGH);
if(gamesound_ispublicvoicecom(s)) // public
{
addmsg(SV_VOICECOM, "ri", s);
toserver(text);
}
else // team
{
addmsg(SV_VOICECOMTEAM, "ri", s);
defformatstring(teamtext)("%c%s", '%', text);
toserver(teamtext);
}
last = lastmillis;
}
}
COMMAND(voicecom, "ss");
void soundtest()
{
loopi(S_NULL) audiomgr.playsound(i, rnd(SP_HIGH+1));
}
COMMAND(soundtest, "");
// sound configuration
soundconfig::soundconfig(sbuffer *b, int vol, int maxuses, bool loop, int audibleradius)
{
buf = b;
this->vol = vol > 0 ? vol : 100;
this->maxuses = maxuses;
this->loop = loop;
this->audibleradius = audibleradius;
this->model = audibleradius > 0 ? DM_LINEAR : DM_DEFAULT; // use linear model when radius is configured
uses = 0;
muted = false;
}
void soundconfig::onattach()
{
uses++;
}
void soundconfig::ondetach()
{
uses--;
}
vector<soundconfig> gamesounds, mapsounds;
void audiomanager::detachsounds(playerent *owner)
{
if(nosound) return;
// make all dependent locations static
locations.replaceworldobjreference(physentreference(owner), staticreference(owner->o));
}
VARP(maxsoundsatonce, 0, 32, 100);
location *audiomanager::_playsound(int n, const worldobjreference &r, int priority, float offset, bool loop)
{
if(nosound || !soundvol) return NULL;
if(soundmuted(n)) return NULL;
DEBUGVAR(n);
DEBUGVAR(priority);
if(r.type!=worldobjreference::WR_ENTITY)
{
// avoid bursts of sounds with heavy packetloss and in sp
static int soundsatonce = 0, lastsoundmillis = 0;
if(totalmillis==lastsoundmillis) soundsatonce++; else soundsatonce = 1;
lastsoundmillis = totalmillis;
if(maxsoundsatonce && soundsatonce>maxsoundsatonce)
{
DEBUGVAR(soundsatonce);
return NULL;
}
}
location *loc = new location(n, r, priority);
locations.add(loc);
if(!loc->stale)
{
if(offset>0) loc->offset(offset);
if(currentpitch!=1.0f) loc->pitch(currentpitch);
loc->play(loop);
}
return loc;
}
void audiomanager::playsound(int n, int priority) { _playsound(n, camerareference(), priority); }
void audiomanager::playsound(int n, physent *p, int priority) { if(p) _playsound(n, physentreference(p), priority); }
void audiomanager::playsound(int n, entity *e, int priority) { if(e) _playsound(n, entityreference(e), priority); }
void audiomanager::playsound(int n, const vec *v, int priority) { if(v) _playsound(n, staticreference(*v), priority); }
void audiomanager::playsoundname(char *s, const vec *loc, int vol)
{
if(!nosound) return;
if(vol <= 0) vol = 100;
int id = findsound(s, vol, gamesounds);
if(id < 0) id = addsound(s, vol, 0, false, gamesounds, true, 0);
playsound(id, loc, SP_NORMAL);
}
void audiomanager::playsoundc(int n, physent *p, int priority)
{
if(p && p!=player1) playsound(n, p, priority);
else
{
addmsg(SV_SOUND, "i", n);
playsound(n, priority);
}
}
void audiomanager::stopsound()
{
if(nosound) return;
DELETEA(musicdonecmd);
if(gamemusic) gamemusic->reset();
}
VARP(heartbeat, 0, 0, 99);
// main audio update routine
void audiomanager::updateaudio()
{
if(nosound) return;
alcSuspendContext(context); // don't process sounds while we mess around
bool alive = player1->state==CS_ALIVE;
bool firstperson = camera1==player1 || (player1->isspectating() && player1->spectatemode==SM_DEATHCAM);
// footsteps
updateplayerfootsteps(player1);
loopv(players)
{
playerent *p = players[i];
if(!p) continue;
updateplayerfootsteps(p);
}
// water
bool underwater = /*alive &&*/ firstperson && waterlevel > player1->o.z + player1->aboveeye;
updateloopsound(S_UNDERWATER, underwater);
// tinnitus
bool tinnitus = alive && firstperson && player1->eardamagemillis>0 && lastmillis<=player1->eardamagemillis;
location *tinnitusloc = updateloopsound(S_TINNITUS, tinnitus);
// heartbeat
bool heartbeatsound = heartbeat && alive && firstperson && !m_osok && player1->health <= heartbeat;
updateloopsound(S_HEARTBEAT, heartbeatsound);
// pitch fx
const float lowpitch = 0.65f;
bool pitchfx = underwater || tinnitus;
if(pitchfx && currentpitch!=lowpitch)
{
currentpitch = lowpitch;
locations.forcepitch(currentpitch);
if(tinnitusloc) tinnitusloc->pitch(1.9f); // super high pitched tinnitus
}
else if(!pitchfx && currentpitch==lowpitch)
{
currentpitch = 1.0f;
locations.forcepitch(currentpitch);
}
// update map sounds
static int lastmapsound = 0;
if(!lastmapsound || totalmillis-lastmapsound>mapsoundrefresh || !mapsoundrefresh)
{
loopv(ents)
{
entity &e = ents[i];
vec o(e.x, e.y, e.z);
if(e.type!=SOUND) continue;
int sound = e.attr1;
int radius = e.attr2;
bool hearable = (radius==0 || camera1->o.dist(o)<radius);
entityreference entref(&e);
// search existing sound loc
location *loc = locations.find(sound, &entref, mapsounds);
if(hearable && !loc) // play
{
_playsound(sound, entref, SP_LOW, 0.0f, true);
}
else if(!hearable && loc) // stop
{
loc->drop();
}
}
lastmapsound = totalmillis;
}
// update all sound locations
locations.updatelocations();
// update background music
if(gamemusic)
{
if(!gamemusic->update())
{
// music ended, exec command
if(musicdonecmd)
{
char *cmd = musicdonecmd;
musicdonecmd = NULL;
setcontext("hook", "musicdonecmd");
execute(cmd);
resetcontext();
delete[] cmd;
}
}
}
// listener
vec o[2];
o[0].x = (float)(cosf(RAD*(camera1->yaw-90)));
o[0].y = (float)(sinf(RAD*(camera1->yaw-90)));
o[0].z = 0.0f;
o[1].x = o[1].y = 0.0f;
o[1].z = -1.0f;
alListenerfv(AL_ORIENTATION, (ALfloat *) &o);
alListenerfv(AL_POSITION, (ALfloat *) &camera1->o);
alcProcessContext(context);
}
// binding of sounds to the 3D world
// camera
camerareference::camerareference() : worldobjreference(WR_CAMERA) {}
worldobjreference *camerareference::clone() const
{
return new camerareference(*this);
}
const vec &camerareference::currentposition() const
{
return camera1->o;
}
bool camerareference::nodistance()
{
return true;
}
bool camerareference::operator==(const worldobjreference &other)
{
return type==other.type;
}
// physent
physentreference::physentreference(physent *ref) : worldobjreference(WR_PHYSENT)
{
ASSERT(ref);
phys = ref;
}
worldobjreference *physentreference::clone() const
{
return new physentreference(*this);
}
const vec &physentreference::currentposition() const
{
return phys->o;
}
bool physentreference::nodistance()
{
return phys==camera1;
}
bool physentreference::operator==(const worldobjreference &other)
{
return type==other.type && phys==((physentreference &)other).phys;
}
// entity
entityreference::entityreference(entity *ref) : worldobjreference(WR_ENTITY)
{
ASSERT(ref);
ent = ref;
}
worldobjreference *entityreference::clone() const
{
return new entityreference(*this);
}
const vec &entityreference::currentposition() const
{
static vec tmp;
tmp = vec(ent->x, ent->y, ent->z);
return tmp;
}
bool entityreference::nodistance() { return ent->attr3>0; }
bool entityreference::operator==(const worldobjreference &other) { return type==other.type && ent==((entityreference &)other).ent; }
// static
staticreference::staticreference(const vec &ref) : worldobjreference(WR_STATICPOS)
{
pos = ref;
}
worldobjreference *staticreference::clone() const
{
return new staticreference(*this);
}
const vec &staticreference::currentposition() const
{
return pos;
}
bool staticreference::nodistance()
{
return false;
}
bool staticreference::operator==(const worldobjreference &other)
{
return type==other.type && pos==((staticreference &)other).pos;
}
// instance
audiomanager audiomgr;
const char *soundprioritynames[] = {"LOW", "NORMAL", "HIGH", "HIGHEST", ""}; // keep in sync with enum { SP_LOW = 0, SP_NORMAL, SP_HIGH, SP_HIGHEST }
COMMANDF(sound, "is", (int *n, char *priorityname)
{
int priority = getlistindex(priorityname, soundprioritynames, true, SP_NORMAL);
audiomgr.playsound(*n, priority);
});
COMMANDF(applymapsoundchanges, "", (){
if(m_coop || !multiplayer("applymapsoundchanges")) audiomgr.applymapsoundchanges();
});
COMMANDF(unmuteallsounds, "", () {
audiomgr.unmuteallsounds();
});
COMMANDF(mutesound, "ii", (int *n, int *off)
{
audiomgr.mutesound(*n, *off);
});
COMMANDF(soundmuted, "i", (int *n)
{
intret(audiomgr.soundmuted(*n));
});
VAR(mapsoundchanged, 0, 0, 1);
COMMANDF(mapsoundreset, "", ()
{
audiomgr.mapsoundreset();
mapconfigdata.mapsoundlines.shrink(0);
mapsoundchanged = 1;
flagmapconfigchange();
});
VARF(soundchannels, 4, 128, 1024, audiomgr.setchannels(soundchannels));
VARFP(soundvol, 0, 128, 255, audiomgr.setlistenervol(soundvol));
COMMANDF(registersound, "siii", (char *name, int *vol, int *loop, int *audibleradius)
{
intret(audiomgr.addsound(name, *vol, -1, *loop != 0, gamesounds, true, *audibleradius));
});
void registerdefaultsounds()
{
loopi(S_NULL)
{
ASSERT(soundcfg[i].key == i);
audiomgr.addsound(soundcfg[i].name, soundcfg[i].vol, -1, soundcfg[i].loop != 0, gamesounds, true, soundcfg[i].audibleradius);
}
}
const char *soundcategories[SC_NUM + 1] = { "PAIN", "OWNPAIN", "WEAPON", "PICKUP", "MOVEMENT", "BULLET", "OTHER", "VOICECOM", "TEAM", "PUBLIC", "FFA", "FLAGONLY", "" };
void enumsounds(char *what)
{
vector<char *> w;
explodelist(what, w);
int flags = 0, nflags = 0;
loopv(w)
{
if(w[i][0] == '!') nflags |= 1 << getlistindex(w[i] + 1, soundcategories, false, 0);
else flags |= 1 << getlistindex(w[i], soundcategories, false, 0);
delstring(w[i]);
}
vector<char> res;
loopi(S_NULL) if((soundcfg[i].flags & flags) && !(soundcfg[i].flags & nflags)) cvecprintf(res, "%d %s\n", i, escapestring(soundcfg[i].desc));
resultcharvector(res, -1);
}
COMMAND(enumsounds, "s");
const char *mapsoundbasepath = "packages/audio/", *mapsoundfinalpath = "ambience/";
const int mapsoundbasepath_n = strlen(mapsoundbasepath), mapsoundfinalpath_n = strlen(mapsoundfinalpath);
COMMANDF(mapsound, "si", (char *name, int *maxuses)
{
filtertext(name, name, FTXT__MEDIAFILEPATH);
defformatstring(stripped)("%s%s", mapsoundbasepath, name[0] == '.' && name[1] == '/' ? name + 2 : name);
unixpath(path(stripped));
if(mapconfigdata.mapsoundlines.length() > 254)
{
conoutf("\f3error: too many mapsounds");
flagmapconfigerror(LWW_CONFIGERR);
scripterr();
}
else if(!strncmp(stripped, mapsoundbasepath, mapsoundbasepath_n))
{
name = stripped + mapsoundbasepath_n;
audiomgr.addsound(name, 255, *maxuses, true, mapsounds, false, 0);
intret(mapconfigdata.mapsoundlines.length());
if(!strncmp(name, mapsoundfinalpath, mapsoundfinalpath_n)) name += mapsoundfinalpath_n; // base path for mapsoundlines is "packages/audio/ambience"
copystring(mapconfigdata.mapsoundlines.add().name, name);
mapconfigdata.mapsoundlines.last().maxuses = *maxuses;
mapsoundchanged = 1;
flagmapconfigchange();
}
else
{
conoutf("\f3error: mapsound \"%s\" outside packages/audio/", stripped);
flagmapconfigerror(LWW_CONFIGERR);
scripterr();
}
});
COMMANDF(registermusic, "s", (char *name)
{
audiomgr.registermusic(name);
});
COMMANDF(musicpreload, "i", (int *id)
{
audiomgr.musicpreload(*id);
});
COMMANDF(music, "sis", (char *name, int *millis, char *cmd)
{
audiomgr.music(name, *millis, cmd);
});
void mapsoundslotusage(int *n) // returns all entity indices that use a mapsound of slot n
{
string res = "";
loopv(ents) if(ents[i].type == SOUND && ents[i].attr1 == *n) concatformatstring(res, "%s%d", i ? " " : "", i);
result(res);
}
COMMAND(mapsoundslotusage, "i");
void reloadmapsoundconfig()
{
vector<char> sc;
getcurrentmapconfig(sc, true); // only soundconfig
execute(sc.getbuf()); // cheap and easy way ;)
}
void deletemapsoundslot(int *n, char *opt) // delete mapsound slot - only if unused or "purge" is specified
{
EDITMP("deletemapsoundslot");
if(!mapconfigdata.mapsoundlines.inrange(*n)) return;
bool purgeall = !strcmp(opt, "purge"), slotused = false;
loopv(ents) if(ents[i].type == SOUND && ents[i].attr1 == *n) slotused = true;
if(!purgeall && slotused) { conoutf("mapsound slot #%d is in use: can't delete", *n); return; }
audiomgr.mapsoundreset();
int deld = 0;
loopv(ents) if(ents[i].type == SOUND)
{
entity &e = ents[i];
if(e.attr1 == *n)
{ // delete entity
deleted_ents.add(e);
deletesoundentity(e);
memset(&e, 0, sizeof(persistent_entity));
e.type = NOTUSED;
deld++;
}
else if(e.attr1 > *n) e.attr1--; // adjust models in higher slots
}
mapconfigdata.mapsoundlines.remove(*n);
reloadmapsoundconfig();
defformatstring(s)(" (%d mapsounds purged)", deld);
conoutf("mapsound slot #%d deleted%s", *n, deld ? s : "");
mapsoundchanged = 1;
unsavededits++;
hdr.flags |= MHF_AUTOMAPCONFIG; // requires automapcfg
}
COMMAND(deletemapsoundslot, "is");
void editmapsoundslot(int *n, char *name, char *maxuses) // edit slot parameters != ""
{
EDITMP("editmapsoundslot");
string res = "";
if(mapconfigdata.mapsoundlines.inrange(*n))
{
mapsoundline &msl = mapconfigdata.mapsoundlines[*n];
if(*name || *maxuses)
{ // change attributes
if(*maxuses) msl.maxuses = strtol(maxuses, NULL, 0);
if(*name) copystring(msl.name, strncmp(name, mapsoundfinalpath, mapsoundfinalpath_n) ? name : name + mapsoundfinalpath_n);
reloadmapsoundconfig();
mapsoundchanged = 1;
unsavededits++;
hdr.flags |= MHF_AUTOMAPCONFIG; // requires automapcfg
}
formatstring(res)("\"%s\" %d", msl.name, msl.maxuses); // give back all current attributes
}
result(res);
}
COMMAND(editmapsoundslot, "iss");
void getmapsoundorigin(char *fname)
{
defformatstring(s)("packages/audio/ambience/%s", fname);
findfile(path(s), "r");
const char *res = s;
switch(findfilelocation)
{
case FFL_ZIP: res = "zip"; break;
case FFL_WORKDIR: res = fileexists(s, "r") ? "official" : "<file not found>"; break;
case FFL_HOME: res = "custom"; break;
default: formatstring(s)("package dir #%d", findfilelocation); break;
}
result(res);
}
COMMAND(getmapsoundorigin, "s");
void mapsoundslotbyname(char *name) // returns the slot(s) that a certain mapsound file is used in
{
string res = "";
loopv(mapconfigdata.mapsoundlines) if(!strcmp(name, mapconfigdata.mapsoundlines[i].name)) concatformatstring(res, "%s%d", *res ? " " : "", i);
result(res);
}
COMMAND(mapsoundslotbyname, "s");
void getmapsoundlist() // create a list of mapsound filenames
{
vector<char *> files;
listfilesrecursive("packages/audio/ambience", files);
files.sort(stringsort);
loopvrev(files) if(files.inrange(i + 1) && !strcmp(files[i], files[i + 1])) delstring(files.remove(i + 1)); // remove doubles
int pn = strlen("packages/audio/ambience/");
vector<char> res;
loopv(files)
{
if(!strncmp(files[i], "packages/audio/ambience/", pn))
{
const char *s = files[i] + pn;
int sn = strlen(s);
if(sn > 4)
{
const char *e = s + sn - 4;
if(!strcmp(e, ".ogg") || !strcmp(e, ".wav")) cvecprintf(res, "\"%s\"\n", s);
}
}
}
files.deletearrays();
resultcharvector(res, -1);
}
COMMAND(getmapsoundlist, "");
struct tempmsslot { mapsoundline m; vector<int> oldslot; bool used; };
int tempmscmp(tempmsslot *a, tempmsslot *b)
{
int n = strcmp(a->m.name, b->m.name);
if(n) return n;
return a->m.maxuses - b->m.maxuses;
}
int tempmssort(tempmsslot *a, tempmsslot *b)
{
int n = tempmscmp(a, b);
return n ? n : a->oldslot[0] - b->oldslot[0];
}
int tempmsunsort(tempmsslot *a, tempmsslot *b)
{
return a->oldslot[0] - b->oldslot[0];
}
void sortmapsoundslots(char **args, int numargs)
{
bool nomerge = false, mergeused = false, nosort = false, unknownarg = false;
loopi(numargs) if(args[i][0])
{
if(!strcasecmp(args[i], "nomerge")) nomerge = true;
else if(!strcasecmp(args[i], "nosort")) nosort = true;
else if(!strcasecmp(args[i], "mergeused")) mergeused = true;
else { conoutf("sortmapsoundslots: unknown argument \"%s\"", args[i]); unknownarg = true; }
}
EDITMP("sortmapsoundslots");
if(unknownarg || mapconfigdata.mapsoundlines.length() < 3) return;
vector<tempmsslot> tempslots;
loopv(mapconfigdata.mapsoundlines)
{
tempslots.add().m = mapconfigdata.mapsoundlines[i];
tempslots.last().oldslot.add(i);
tempslots.last().used = false;
}
loopv(ents) if(ents[i].type == SOUND && tempslots.inrange(ents[i].attr1)) tempslots[ents[i].attr1].used = true;
tempslots.sort(tempmssort);
// remove double entries
if(!nomerge) loopvrev(tempslots) if(i > 0)
{
tempmsslot &s1 = tempslots[i], &s0 = tempslots[i - 1];
if(!tempmscmp(&s0, &s1) && (mergeused || !s0.used || !s1.used))
{
if(s1.used) s0.used = true;
loopvj(s1.oldslot) s0.oldslot.add(s1.oldslot[j]);
tempslots.remove(i);
}
}
if(nosort) tempslots.sort(tempmsunsort);
// create translation table
uchar newslot[256];
loopk(256) newslot[k] = k;
loopv(tempslots)
{
tempmsslot &t = tempslots[i];
loopvj(t.oldslot)
{
if(t.oldslot[j] < 256) newslot[t.oldslot[j]] = i;
}
}
// translate all mapsound entities
loopv(ents) if(ents[i].type == SOUND) ents[i].attr1 = newslot[ents[i].attr1 & 255];
conoutf("%d mapsound slots%s, %d %sslots merged", tempslots.length(), nosort ? "" : " sorted", mapconfigdata.mapsoundlines.length() - tempslots.length(), mergeused ? "" : "unused ");
// rewrite mapmodel slot list
mapconfigdata.mapsoundlines.shrink(tempslots.length());
loopv(tempslots)
{
mapconfigdata.mapsoundlines[i] = tempslots[i].m;
}
reloadmapsoundconfig();
unsavededits++;
mapsoundchanged = 1;
hdr.flags |= MHF_AUTOMAPCONFIG; // requires automapcfg
}
COMMAND(sortmapsoundslots, "v");