// serverfiles.h // abuse globals to register server parameters extern int addservparint(const char *name, int minval, int cur, int maxval, const char *list[], int *storage, int *shadowstorage, void (*fun)(), bool logchanges, bool fromfile, const char *desc); extern bool addservparstr(const char *name, int minlen, int maxlen, int filt, const char *cur, char *storage, char *shadowstorage, void (*fun)(), bool logchanges, bool fromfile, const char *desc); #ifdef _DEBUG #define SERVPAR(name, min, cur, max, desc) int last_##name, name = addservparint(#name, min, cur, max, NULL, &name, &last_##name, NULL, false, true, desc) #define SERVPARLIST(name, min, cur, max, list, desc) int last_##name, name = addservparint(#name, min, cur, max, list, &name, &last_##name, NULL, false, true, desc) #define SERVPARF(name, min, cur, max, fun, desc) extern void fun(); int last_##name, name = addservparint(#name, min, cur, max, NULL,&name, &last_##name, fun, false, true, desc) #define SERVPARLISTF(name, min, cur, max, list, fun, desc) extern void fun(); int last_##name, name = addservparint(#name, min, cur, max, list, &name, &last_##name, fun, false, true, desc) #define SERVSTR(name, cur, min, max, filt, desc) char last_##name[max+1], name[max+1]; bool __sdummy_##name = addservparstr(#name, min, max, filt, cur, name, last_##name, NULL, false, true, desc) #define SERVSTRF(name, cur, min, max, filt, fun, desc) char last_##name[max+1], name[max+1]; extern void fun(); bool __sdummy_##name = addservparstr(#name, min, max, filt, cur, name, last_##name, fun, false, true, desc) #define SERVSTAT(name, cur, desc) int last_##name, name = addservparint(#name, 0, cur, 0, NULL, &name, &last_##name, NULL, false, false, desc) #define SERVSTATLOG(name, cur, level, desc) int last_##name, name = addservparint(#name, level, cur, 0, NULL, &name, &last_##name, NULL, true, false, desc) #else #define SERVPAR(name, min, cur, max, desc) int last_##name, name = addservparint(#name, min, cur, max, NULL, &name, &last_##name, NULL, false, true, NULL) #define SERVPARLIST(name, min, cur, max, list, desc) int last_##name, name = addservparint(#name, min, cur, max, list, &name, &last_##name, NULL, false, true, NULL) #define SERVPARF(name, min, cur, max, fun, desc) extern void fun(); int last_##name, name = addservparint(#name, min, cur, max, NULL,&name, &last_##name, fun, false, true, NULL) #define SERVPARLISTF(name, min, cur, max, list, fun, desc) extern void fun(); int last_##name, name = addservparint(#name, min, cur, max, list, &name, &last_##name, fun, false, true, NULL) #define SERVSTR(name, cur, min, max, filt, desc) char last_##name[max+1], name[max+1]; bool __sdummy_##name = addservparstr(#name, min, max, filt, cur, name, last_##name, NULL, false, true, NULL) #define SERVSTRF(name, cur, min, max, filt, fun, desc) char last_##name[max+1], name[max+1]; extern void fun(); bool __sdummy_##name = addservparstr(#name, min, max, filt, cur, name, last_##name, fun, false, true, NULL) #define SERVSTAT(name, cur, desc) int last_##name, name = addservparint(#name, 0, cur, 0, NULL, &name, &last_##name, NULL, false, false, desc) #define SERVSTATLOG(name, cur, level, desc) int last_##name, name = addservparint(#name, level, cur, 0, NULL, &name, &last_##name, NULL, true, false, desc) #endif #define CONFIG_MAXPAR 11 const char *maprotkeywords[CONFIG_MAXPAR] = { "weight", "repeat", "time", "mintime", "maxtime", "minplayers", "maxplayers", "maxteamsize", "teamthreshold", "manual", "restrict" }; struct configsetvalues { union { struct { char weight, repeat, time, mintime, maxtime, minplayers, maxplayers, maxteamsize, teamthreshold, manual, restrict; }; char par[CONFIG_MAXPAR]; }; void empty() { weight = repeat = 0; time = mintime = maxtime = minplayers = maxplayers = maxteamsize = teamthreshold = manual = restrict = -1; } void add(configsetvalues u); bool parse(char *keyval); }; // map management #define SERVERMAXMAPFACTOR 10 // 10 is huge... if you're running low on RAM, consider 9 as maximum (don't go higher than 10, or players will name their ugly dog after you) #define SERVERMAP_PATH_BUILTIN "packages" PATHDIVS "maps" PATHDIVS "official" PATHDIVS #define SERVERMAP_PATH_LOCAL "packages" PATHDIVS "maps" PATHDIVS #define SERVERMAP_PATH "packages" PATHDIVS "maps" PATHDIVS "servermaps" PATHDIVS #define SERVERMAP_PATH_INCOMING "packages" PATHDIVS "maps" PATHDIVS "servermaps" PATHDIVS "incoming" PATHDIVS const char *servermappath_off = SERVERMAP_PATH_BUILTIN; const char *servermappath_local = SERVERMAP_PATH_LOCAL; const char *servermappath_serv = SERVERMAP_PATH; const char *servermappath_incom = SERVERMAP_PATH_INCOMING; #define GZBUFSIZE ((MAXCFGFILESIZE * 11) / 10) #define FLOORPLANBUFSIZE (sizeof(struct servsqr) << ((SERVERMAXMAPFACTOR) * 2)) // that's 4MB for size 10 maps (or 1 MB for size 9 maps) #define GAMEHISTLEN 32 // account the last half hour that the map was played #define PENALTYUNIT 100 stream *readmaplog = NULL; // the readmaps thread always logs directly to file struct servermap // in-memory version of a map file on a server { const char *fname, *fpath; // map name and path (fname has to be first member of this struct! hardcoded!) uchar *cgzraw, *cfgrawgz; // direct copies of the cgz and cfg files (cfg is already gzipped) uchar cgzhash[TIGERHASHSIZE], cfghash[TIGERHASHSIZE]; int cgzlen, cfglen, cfggzlen; // file lengths and cfg-gz length int version, headersize, sfactor, numents, maprevision, waterlevel; // from map header uchar *layoutgz; // gzipped precompiled floorplan int layoutlen, layoutgzlen; mapdim_s mapdims; entitystats_s entstats; mapareastats_s areastats; int x1, x2, y1, y2, zmin, zmax; // bounding box for player-reachable areas uchar *enttypes; // table of entity types short *entpos_x, *entpos_y; configsetvalues *hx_modeinfo; // maprot parameters from map author uchar *hx_mapartist; // pubkey of map artist int modes_allowed; // modes_possible reduced to MP modes (if dedicated server) configsetvalues modes[GMODE_NUM]; // maprot parameters for this map int modes_auto; // modes to be suggested automatically int modes_cr[CR_NUM]; // modes available for manual voting, depending on client role int modes_pn[MAXPLAYERS]; // modes suited for a certain number of players char lastmodes[GAMEHISTLEN]; // last played game modes on this map (one entry per minute) int lastplayed[GAMEHISTLEN]; // time the map was played last (one entry per minute) int penalty[GMODE_NUM]; // prevent overplaying a mode on this map int mappenalty; // prevent overplaying of map int bestmode, weight; // mode with least penalty, weight of that mode on this map const char *err; bool isok; // definitive flag! #ifdef _DEBUG char maptitle[129]; #endif servermap(const char *mname, const char *mpath) { memset(&fname, 0, sizeof(struct servermap)); fname = newstring(mname); fpath = mpath; } ~servermap() { delstring(fname); DELETEA(cgzraw); DELETEA(cfgrawgz); DELETEA(enttypes); DELETEA(entpos_x); DELETEA(entpos_y); DELETEA(layoutgz); DELETEA(hx_modeinfo); DELETEA(hx_mapartist); } bool isro() { return fpath == servermappath_off || fpath == servermappath_serv; } bool isofficial() { return fpath == servermappath_off; } bool isdistributable() { return fpath == servermappath_serv || fpath == servermappath_incom; } bool isautoplay() { return isro(); } int getmemusage() { return sizeof(struct servermap) + cgzlen + cfggzlen + layoutgzlen + numents * (sizeof(uchar) + sizeof(short) * 3); } char *getlayout() // uncompress prefabricated floorplan { if(isok && layoutgz && layoutgzlen > 0 && layoutgzlen < layoutlen) { char *lo = new char[layoutlen]; uLongf rawsize = layoutlen; if(lo && uncompress((Bytef*)lo, &rawsize, layoutgz, layoutgzlen) == Z_OK && rawsize - layoutlen == 0) return lo; delete[] lo; } return NULL; } const char *getpathdesc() { if(fpath == servermappath_off) return "official"; if(fpath == servermappath_local) return "local"; if(fpath == servermappath_serv) return "custom"; if(fpath == servermappath_incom) return "temporary"; ASSERT(0); return "unknown"; } void load(void) // load map into memory and extract everything important about it (assumes struct to be zeroed: can only be called once) { static uchar *staticbuffer = NULL; if(!staticbuffer) staticbuffer = new uchar[FLOORPLANBUFSIZE]; // this buffer gets reused for every map load several times (also: because of this, load() is not thread safe) stream *f = NULL; int restofhead; string filename, tmp; if(!validmapname(fname)) err = "illegal filename"; if(err) goto loadfailed; // load map files, prepare sendmap buffer { formatstring(filename)("%s%s.cfg", fpath, fname); path(filename); uchar *cfgraw = (uchar *)loadfile(filename, &cfglen); if(cfgraw) { tigerhash(cfghash, cfgraw, cfglen); loopk(cfglen) if(cfgraw[k] > 0x7f || (cfgraw[k] < 0x20 && !isspace(cfgraw[k]))) err = "illegal chars in cfg file"; } formatstring(filename)("%s%s.cgz", fpath, fname); path(filename); cgzraw = (uchar *)loadfile(filename, &cgzlen); if(cgzraw) tigerhash(cgzhash, cgzraw, cgzlen); if(!cgzraw) err = "loading cgz failed"; else if(cfglen > MAXCFGFILESIZE) err = "cfg file too big"; else if(cgzlen >= MAXMAPSENDSIZE) err = "cgz file too big"; else if(cfgraw) { uLongf gzbufsize = GZBUFSIZE; ASSERT(GZBUFSIZE < FLOORPLANBUFSIZE); if(compress2(staticbuffer, &gzbufsize, cfgraw, cfglen, 9) != Z_OK) gzbufsize = 0; cfggzlen = (int) gzbufsize; if(cgzlen + cfggzlen < MAXMAPSENDSIZE) { // map is small enough to be sent cfgrawgz = new uchar[cfggzlen]; memcpy(cfgrawgz, staticbuffer, cfggzlen); } else err = "cgz + cfg.gz too big to send"; } DELETEA(cfgraw); } if(err) goto loadfailed; // extract entity data and header info; compile map statistics; create floorplan { const int sizeof_header = sizeof(header), sizeof_baseheader = sizeof(header) - sizeof(int) * 16; f = opengzfile(filename, "rb"); header *h = (header *)staticbuffer; if(!f) err = "can't open map file"; else if(f->read(h, sizeof_baseheader) != sizeof_baseheader || (strncmp(h->head, "CUBE", 4) && strncmp(h->head, "ACMP",4))) err = "bad map file"; if(err) goto loadfailed; lilswap(&h->version, 4); // version, headersize, sfactor, numents version = h->version; headersize = fixmapheadersize(h->version, h->headersize); sfactor = h->sfactor; numents = h->numents; #ifdef _DEBUG memcpy(maptitle, h->maptitle, 128); #endif restofhead = min(headersize, sizeof_header) - sizeof_baseheader; ASSERT(SERVERMAXMAPFACTOR <= LARGEST_FACTOR); if(version > MAPVERSION || numents > MAXENTITIES || sfactor < SMALLEST_FACTOR || sfactor > SERVERMAXMAPFACTOR || f->read(&h->waterlevel, restofhead) != restofhead) err = "incompatible map file"; else { lilswap(&h->maprevision, 4); // maprevision, ambient, flags, timestamp maprevision = h->maprevision; lilswap(&h->waterlevel, 1); waterlevel = version >= 4 ? h->waterlevel : -100000; restofhead = clamp(headersize - sizeof_header, 0, MAXHEADEREXTRA); if(f->read(staticbuffer, restofhead) != restofhead) err = "map file truncated"; } } if(err) goto loadfailed; // parse header extras { ucharbuf p(staticbuffer, restofhead); while(1) { int len = getuint(p), flags = getuint(p), type = flags & HX_TYPEMASK; if(p.overread() || len > p.remaining()) break; ucharbuf q(p.subbuf(len).buf, len); switch(type) { case HX_EDITUNDO: break; case HX_CONFIG: break; case HX_MODEINFO: { hx_modeinfo = new configsetvalues[GMODE_NUM]; char done[GMODE_NUM] = { 0 }; loopi(GMODE_NUM) hx_modeinfo[i].empty(); string line; for(int mode = getuint(q); !q.overread() && mode > 0; mode = getuint(q)) { getstring(line, q, MAXSTRLEN); loopi(GMODE_NUM) if((mode & (1 << i)) && !done[i]++) filterconfigset(line, hx_modeinfo + i); stream *mr = modeinfologfilename ? openfile(modeinfologfilename, "a") : NULL; if(mr) { mr->printf("%s %s %s\n", fname, gmode_enum(mode, tmp), line); delete mr; } // dump modeinfo lines to file, may be used as maprot } break; } case HX_ARTIST: if(len == 32) q.get((hx_mapartist = new uchar[32]), 32); else err = "invalid HX_ARTIST record"; break; case HX_MAPINFO: default: break; } } } if(err) goto loadfailed; // read and convert map entities { bool oldentityformat = version < 10; // version < 10 have only 4 attributes and no scaling ASSERT(MAXENTITIES * sizeof(persistent_entity) < FLOORPLANBUFSIZE); persistent_entity *es = (persistent_entity *) staticbuffer; loopi(numents) { persistent_entity &e = es[i]; f->read(&e, oldentityformat ? 12 : sizeof(persistent_entity)); lilswap((short *)&e, 4); if(oldentityformat) e.attr5 = e.attr6 = e.attr7 = 0; else lilswap(&e.attr5, 1); #if 0 if(e.type == LIGHT && e.attr1 >= 0) { if(!e.attr2) e.attr2 = 255; if(e.attr1 > 32) e.attr1 = 32; } #endif transformoldentitytypes(version, e.type); if(oldentityformat && e.type < MAXENTTYPES) { if(e.type == CTF_FLAG || e.type == MAPMODEL) e.attr1 = e.attr1 + 7 - (e.attr1 + 7) % 15; // round the angle to the nearest 15-degree-step, like old versions did during rendering if(e.type == LIGHT && e.attr1 < 0) e.attr1 = 0; // negative lights had no meaning before version 10 int ov, ss; #define SCALEATTR(x) \ if((ss = abs(entwraparound[e.type][x - 1] / entscale[e.type][x - 1]))) e.attr##x = (int(e.attr##x) % ss + ss) % ss; \ e.attr##x = ov = e.attr##x * entscale[e.type][x - 1]; \ if(ov != e.attr##x) err = "overflow during conversion of entity attribute"; SCALEATTR(1); SCALEATTR(2); SCALEATTR(3); SCALEATTR(4); #undef SCALEATTR } } } if(err) goto loadfailed; // convert and count entities for server use { persistent_entity *es = (persistent_entity *) staticbuffer; calcentitystats(entstats, es, numents); enttypes = new uchar[numents]; // FIXME: cut this down to useful entities entpos_x = new short[numents]; entpos_y = new short[numents]; loopi(numents) { persistent_entity &e = es[i]; enttypes[i] = e.type >= MAXENTTYPES ? NOTUSED : e.type; entpos_x[i] = e.x; entpos_y[i] = e.y; } // respect map author wishes about disallowed modes if(hx_modeinfo) loopi(GMODE_NUM) if(hx_modeinfo[i].restrict == 81) entstats.modes_possible &= ~(1 << i); // disallowed by elvis^2 } if(err) goto loadfailed; // read full map geometry (without textures) { layoutlen = 1 << (sfactor * 2); servsqr *ss = (servsqr *)staticbuffer, *tt = NULL, *ee = ss + layoutlen; while(ss < ee && !err) { int type = f->getchar(), n; if(!tt && type > MAXTYPE) err = "map file broken"; else switch(type) { case -1: err = "while reading map: unexpected eof"; break; case 255: n = f->getchar(); loopi(n) memcpy(ss++, tt, sizeof(servsqr)); ss--; break; case 254: // only in MAPVERSION<=2 memcpy(ss, tt, sizeof(servsqr)); f->getchar(); f->getchar(); break; case SOLID: ss->type = SOLID; f->getchar(); ss->vdelta = f->getchar(); if(version <= 2) { f->getchar(); f->getchar(); } break; case 253: // SOLID with all textures during editing (undo) err = "unoptimised map file not allowed on server"; break; default: if(type < 0 || type >= MAXTYPE) err = "illegal type"; else { ASSERT((int)MAXTYPE < (int)TAGTRIGGERMASK); ss->type = type; ss->floor = f->getchar(); ss->ceil = f->getchar(); if(ss->floor >= ss->ceil) ss->floor = ss->ceil - 1; // for pre 12_13 f->getchar(); f->getchar(); f->getchar(); if(version <= 2) { f->getchar(); f->getchar(); } ss->vdelta = f->getchar(); if(version >= 2) f->getchar(); if(version >= 5) ss->type |= TAGANYCLIP & f->getchar(); } break; } tt = ss; ss++; } } if(err) goto loadfailed; // collect geometry stats (exactly the same as calculated by the client) { if(calcmapdims(mapdims, (servsqr *)staticbuffer, 1 << sfactor) < 0) err = "world geometry error"; } if(err) goto loadfailed; // merge vdelta into floor & ceil { servsqr *ss = (servsqr *)staticbuffer; int linelen = 1 << sfactor, linegap = linelen - mapdims.xspan; ss += linelen * mapdims.y1 + mapdims.x1; for(int j = mapdims.yspan; j > 0; j--, ss += linegap) loopirev(mapdims.xspan) { int type = ss->type & TAGTRIGGERMASK; if(type == FHF || type == CHF) { int diff = (ss->vdelta + ss[1].vdelta + ss[linelen].vdelta + ss[linelen + 1].vdelta + 15) / 16; // round up to full cubes if(type == FHF) ss->floor = max(-128, ss->floor - diff); else ss->ceil = min(127, ss->ceil + diff); } ss++; } } if(err) goto loadfailed; // run mipmapper (to get corners right) // TODO :) // calculate area statistics from type & vdelta values (destroys vdelta!) { if(calcmapareastats(areastats, (servsqr *)staticbuffer, 1 << sfactor, mapdims) < 0) err = "world layout malformed"; // should be a quite fringe error } if(err) goto loadfailed; // work "player accessibility" into the floorplan, calculate map bounding box for player-accessible areas only { servsqr *ss = (servsqr *)staticbuffer; int linelen = 1 << sfactor, linegap = linelen - mapdims.xspan; ss += linelen * mapdims.y1 + mapdims.x1; x1 = y1 = linelen; zmin = 127; zmax = -128; for(int j = 0; j < mapdims.yspan; j++, ss += linegap) loopi(mapdims.xspan) { if((ss->type & TAGANYCLIP) > 0 || ss->ceil - ss->floor < 5) ss->type = SOLID; // treat any cube that is not accessible to a player as SOLID if(!SOLID(ss)) { if(i < x1) x1 = i; if(i > x2) x2 = i; if(j < y1) y1 = j; if(j > y2) y2 = j; if(ss->floor < zmin) zmin = ss->floor; if(ss->ceil > zmax) zmax = ss->ceil; } ss++; } x1 += mapdims.x1; x2 += mapdims.x1; y1 += mapdims.y1; y2 += mapdims.y1; if(x2 - x1 < 10 || y2 - y1 < 10 || zmax - zmin < 10) err = "world layout unplayable"; x1 -= 2; x2 += 2; y1 -= 2; y2 += 2; // make sure, the bounding box is big enough } if(err) goto loadfailed; // create compact floorplan { char *layout = (char *)staticbuffer; servsqr *ss = (servsqr *)staticbuffer; loopirev(layoutlen) { switch(ss->type & TAGTRIGGERMASK) { case SOLID: *layout = 127; break; case CORNER: *layout = zmin; break; // basically: ignore corner areas (otherwise, we'd need a mipmapper to sort it out properly), maybe, this should be fixed at some point... default: *layout = ss->floor; break; } layout++; ss++; } ASSERT(layoutlen * 3 <= (int)FLOORPLANBUFSIZE); uLongf gzbufsize = layoutlen * 2; // valid for sizeof(struct servsqr) >= 3 if(compress2(staticbuffer + layoutlen, &gzbufsize, staticbuffer, layoutlen, 9) != Z_OK) gzbufsize = 0; layoutgzlen = (int) gzbufsize; if(layoutgzlen > 0 && layoutgzlen < layoutlen) { // gzipping went well -> keep it layoutgz = new uchar[layoutgzlen]; memcpy(layoutgz, staticbuffer + layoutlen, layoutgzlen); } else err = "gzipping the floorplan failed"; } loadfailed: DELETEP(f); if(err) { // fail if(readmaplog) readmaplog->printf("reading map '%s%s' failed: %s.\n", fpath, fname, err); } else { // success if(readmaplog) readmaplog->printf("read map '%s%s': cgz %d bytes, cfg %d bytes (%d gz), version %d, size %d, rev %d, " "ents %d, x %d:%d, y %d:%d, z %d:%d, layout %d bytes, spawns %d:%d:%d, flags %d:%d\n", fpath, fname, cgzlen, cfglen, cfggzlen, version, sfactor, maprevision, numents, x1, x2, y1, y2, zmin, zmax, layoutgzlen, entstats.spawns[0], entstats.spawns[1], entstats.spawns[2], entstats.flags[0], entstats.flags[1]); isok = true; } } void oneminuteplayed(int tm, char mode) { memmove(lastmodes + 1, lastmodes, GAMEHISTLEN - 1); memmove(lastplayed + 1, lastplayed, GAMEHISTLEN - 1); lastmodes[0] = mode; lastplayed[0] = tm; } }; int servermapsortname(servermap **a, servermap **b) { return strcmp((*a)->fname, (*b)->fname); } // sort by name (ascending) int servermapsortweight(servermap **a, servermap **b) { return (*b)->weight - (*a)->weight; } // sort by weight (descending) // data structures to sync data flow between main thread and readmapsthread volatile servermap *servermapdropbox = NULL; // changed servermap entry back to the main thread volatile bool startnewservermapsepoch = false; // signal readmapsthread to start an new full search sl_semaphore *readmapsthread_sem = NULL; // sync readmapsthread with main thread // readmapsthread // // * checks all servermaps directories for map files, prioritises and reads them all into memory // * compiles floorplans for every map and keeps them in memory (gzipped) // * fetches extended attributes from the map header // * basically extracts everything from the map file, that the server needs to run a game on it // (may take a while, but we're not in a hurry) // // the thread processes one map at a time and waits for the main thread to store and enlist the findings // struct mapfilename { const char *fname, *fpath; int cgzlen, cfglen, epoch; }; // for tracking map/cfg file (-size) changes vector mapfilenames; // this list only grows void updateservermap(int index, bool deletethis) { mapfilename &m = mapfilenames[index]; servermap *sm = new servermap(m.fname, m.fpath); if(!deletethis) sm->load(); if(sm->isok) { m.cgzlen = sm->cgzlen; m.cfglen = sm->cfglen; } else m.cgzlen = m.cfglen = 0; // pipe sm to main thread.... // we're handing a pointer to a new servermap to the main thread (who manages the array for all servermaps) // if the new servermap is not loaded properly, that's the signal for the main thread to delete the entry from the array while(servermapdropbox) readmapsthread_sem->wait(); // wait for the main thread to process the last sent servermap servermapdropbox = sm; } int getmapfilenameindex(const char *fname, const char *fpath) { loopv(mapfilenames) if(fpath == mapfilenames[i].fpath && !strcmp(fname, mapfilenames[i].fname)) return i; return -1; } void trymapfiles(const char *fpath, const char *fname, int epoch) //check, if map files were added or altered (detecting alteration requires filesizes to be changed as well, as usual) { bool updatethis = false; int mapfileindex = getmapfilenameindex(fname, fpath); if(mapfileindex < 0) { // new map file mapfileindex = mapfilenames.length(); mapfilename &m = mapfilenames.add(); m.fname = newstring(fname); m.fpath = fpath; m.cgzlen = m.cfglen = 0; m.epoch = epoch; updatethis = true; } else { defformatstring(fcgz)("%s%s.cgz", fpath, fname); defformatstring(fcfg)("%s%s.cfg", fpath, fname); if(mapfilenames[mapfileindex].cgzlen != getfilesize(path(fcgz)) || mapfilenames[mapfileindex].cfglen != getfilesize(path(fcfg))) { updatethis = true; // changed filesize detected } } if(updatethis) updateservermap(mapfileindex, false); } void tagmapfile(const char *fpath, const char *fname, int epoch) // tag list entry, if file (+path) is already in it { int mapfileindex = getmapfilenameindex(fname, fpath); if(mapfileindex >= 0) mapfilenames[mapfileindex].epoch = epoch; } int readmapsthread(void *logfilename) { static int readmaps_epoch = 0; while(1) { while(!startnewservermapsepoch) readmapsthread_sem->wait(); // wait without using the cpu if(logfilename) readmaplog = openfile((const char *)logfilename, "a"); vector maps_off, maps_serv, maps_incom; // get all map file names listfiles(servermappath_off, "cgz", maps_off, stringsort); listfiles(servermappath_serv, "cgz", maps_serv, stringsort); listfiles(servermappath_incom, "cgz", maps_incom, stringsort); if(readmaplog) readmaplog->printf("\n#### %s #### scanning all map directories... found %d official maps, %d servermaps and %d maps in 'incoming'\n", timestring(false), maps_off.length(), maps_serv.length(), maps_incom.length()); // enforce priority: official > servermaps > incoming // (every map name is only allowed once among the paths) loopv(maps_off) { loopvjrev(maps_serv) if(!strcmp(maps_off[i], maps_serv[j])) delstring(maps_serv.remove(j)); loopvjrev(maps_incom) if(!strcmp(maps_off[i], maps_incom[j])) delstring(maps_incom.remove(j)); } loopv(maps_serv) { loopvjrev(maps_incom) if(!strcmp(maps_serv[i], maps_incom[j])) delstring(maps_incom.remove(j)); } int lastepoch = readmaps_epoch; readmaps_epoch++; // tag all files in the list that we have found again in this round by giving it the new epoch number loopv(maps_off) tagmapfile(servermappath_off, maps_off[i], readmaps_epoch); loopv(maps_serv) tagmapfile(servermappath_serv, maps_serv[i], readmaps_epoch); loopv(maps_incom) tagmapfile(servermappath_incom, maps_incom[i], readmaps_epoch); // signal deletion of all files in the list that are no longer found to the main thread loopvrev(mapfilenames) if(mapfilenames[i].epoch == lastepoch) updateservermap(i, true); // process all currently available files: load new or changed files and pass them to the main thread loopv(maps_off) trymapfiles(servermappath_off, maps_off[i], readmaps_epoch); loopv(maps_serv) trymapfiles(servermappath_serv, maps_serv[i], readmaps_epoch); loopv(maps_incom) trymapfiles(servermappath_incom, maps_incom[i], readmaps_epoch); DELETEP(readmaplog); // always close the logfile when done, so we can create a new one, if someone removed or renamed the old one startnewservermapsepoch = false; } return 0; } #ifndef STANDALONE const char *checklocalmap(const char *mapname) // check, if a map file exists in packages/maps/official or packages/maps - return proper servermap-path, if found { const char **fn = setnames(behindpath(mapname)), *locs[2] = { servermappath_off, servermappath_local }; int l = 0; if(!isdedicated && (getfilesize(fn[l]) > 10 || getfilesize(fn[++l]) > 10)) return locs[l]; return NULL; } #endif bool serverwritemap(const char *mapname, int mapsize, int cfgsize, int cfgsizegz, uchar *data) { FILE *fp; bool written = false; if(!mapname[0] || mapsize <= 0 || cfgsizegz < 0 || mapsize + cfgsizegz > MAXMAPSENDSIZE || cfgsize > MAXCFGFILESIZE) return false; // malformed: probably modded client defformatstring(name)(SERVERMAP_PATH_INCOMING "%s.cgz", mapname); path(name); fp = fopen(name, "wb"); if(fp) { fwrite(data, 1, mapsize, fp); fclose(fp); formatstring(name)(SERVERMAP_PATH_INCOMING "%s.cfg", mapname); path(name); fp = fopen(name, "wb"); if(fp) { uLongf rawsize = cfgsize; uchar *gzbuf = new uchar[cfgsize]; if(uncompress(gzbuf, &rawsize, data + mapsize, cfgsizegz) == Z_OK && rawsize - cfgsize == 0) fwrite(gzbuf, 1, cfgsize, fp); fclose(fp); delete[] gzbuf; written = true; } } return written; } // server config files vector serverconfigs; // list of server config files, automatically polled in the background volatile bool recheckallserverconfigs = false; // signal readserverconfigsthread to poll all files sl_semaphore *readserverconfigsthread_sem = NULL; // sync readmapsthread with main thread void serverconfigfile::init(const char *name, bool tracking) { copystring(filename, name); path(filename); clear(); if(tracking) serverconfigs.add(this); } bool serverconfigfile::load() { DELETEA(buf); buf = loadfile(filename, &filelen); if((readfailed = !buf)) { xlog(ACLOG_INFO,"could not read config file '%s'", filename); return false; } entropy_add_block((const uchar *)buf, filelen); char *p; if('\r' != '\n') // this is not a joke! { char c = strchr(buf, '\n') ? ' ' : '\n'; // in files without /n substitute /r with /n, otherwise remove /r for(p = buf; (p = strchr(p, '\r')); p++) *p = c; } for(p = buf; (p = strstr(p, "//")); ) // remove comments { while(*p != '\n' && *p != '\0') p++[0] = ' '; } for(p = buf; (p = strchr(p, '\t')); p++) *p = ' '; for(p = buf; (p = strchr(p, '\n')); p++) *p = '\0'; // one string per line return true; } bool serverconfigfile::isbusy() { return busy.getvalue() < 1; } void serverconfigfile::trypreload() // in readserverconfigsthread: load changed files into RAM { if(!buf && !isbusy()) { int newfilelen; bool contentchanged = false; char *newbuf = loadfile(filename, &newfilelen); readfailed = !newbuf; if(newbuf) { uint32_t newhash; uchar *p = (uchar*)newbuf; fnv1a_init(newhash); loopirev(newfilelen) fnv1a_add(newhash, *p++); if(newhash != filehash) { contentchanged = true; filehash = newhash; } delete newbuf; } if(contentchanged) load(); } } void serverconfigfile::process() // in main thread: refill data structures with preloaded file content { updated = false; if((readfailed || buf) && !isbusy() && !busy.trywait()) { clear(); if(buf && strlen(buf)) read(); DELETEA(buf); busy.post(); updated = true; } } string vitafilename, vitafilename_backup, vitafilename_update, vitafilename_update_backup_base, vitafilename_update_backup; char *vitaupdatebuf = NULL; int vitaupdatebuflen = 0; vector *vitastosave = NULL; int readserverconfigsthread(void *data) { while(1) { while(!recheckallserverconfigs) readserverconfigsthread_sem->wait(); // wait without using the cpu loopv(serverconfigs) serverconfigs[i]->trypreload(); // special treatment for vita updates: read once and move out of the way if(!vitaupdatebuf && getfilesize(vitafilename_update) > 0) { formatstring(vitafilename_update_backup)("%s%d.cfg", vitafilename_update_backup_base, (int)(time(NULL) / 60)); backup(vitafilename_update, vitafilename_update_backup); serverconfigfile vsf; vsf.init(vitafilename_update_backup, false); vsf.load(); vitaupdatebuf = vsf.buf; vitaupdatebuflen = vsf.filelen; vsf.buf = NULL; } if(vitastosave) { // mainloop prepared a selection of vitas to save backup(vitafilename, vitafilename_backup); stream *f = openfile(vitafilename, "w"); extern void writevitas(stream *f, vector &vs); if(f) writevitas(f, *vitastosave); DELETEP(f); DELETEP(vitastosave); } entropy_save(); recheckallserverconfigs = false; } return 0; } // maprot.cfg #define MODEHISTLEN 64 // remember the game modes of the last hour (roughly) void configsetvalues::add(configsetvalues u) { weight = clamp(weight + u.weight, -100, 100); repeat = clamp(repeat + u.repeat, -100, 100); for(int i = 2; i < CONFIG_MAXPAR; i++) if(u.par[i] >= 0) par[i] = u.par[i]; } bool configsetvalues::parse(char *keyval) // parse one key:value pair { char *kb, *k = strtok_r(keyval, ":", &kb); if(k) { loopi(CONFIG_MAXPAR) if(!strcmp(k, maprotkeywords[i])) { if((k = strtok_r(NULL, ":", &kb))) { par[i] = clamp(atoi(k), -100, 100); return true; } } } return false; } void filterconfigset(char *list, void *cs) { configsetvalues e, u, *c = cs ? (configsetvalues *) cs : &u; e.empty(), c->empty(); char *b, *kv = strtok_r(list, " ", &b); while(kv) { c->parse(kv); kv = strtok_r(NULL, " ", &b); } list[0] = '\0'; loopi(CONFIG_MAXPAR) if(c->par[i] != e.par[i]) concatformatstring(list, "%s%s:%d", list[0] ? " " : "", maprotkeywords[i], c->par[i]); } SERVSTRF(use_hx_modeinfo, "weight|time|mintime|maxtime|minplayers|maxplayers|maxteamsize|teamthreshold", 0, 159, FTXT__MAPROT, filter_use_hx_modeinfo, "gList of maprot keywords to copy from map headers"); int use_hx_modeinfo_mask = 0; void filter_use_hx_modeinfo() { use_hx_modeinfo_mask = 0; char *tmp = newstring(use_hx_modeinfo), *b, *kv = strtok_r(tmp, "|", &b); while(kv) { loopi(CONFIG_MAXPAR) if(!strcmp(kv, maprotkeywords[i])) use_hx_modeinfo_mask |= 1 << i; kv = strtok_r(NULL, "|", &b); } delstring(tmp); } struct configset : configsetvalues { char mapname[MAXMAPNAMELEN + 1]; int mmask; char asterisk; }; struct servermaprot : serverconfigfile { vector configsets, preloaded; configsetvalues base[GMODE_NUM], prebase[GMODE_NUM]; int lastplayedmodepointer; char lastplayedmodes[MODEHISTLEN]; int modepenalty[GMODE_NUM]; servermap *current; servermaprot() { lastplayedmodepointer = 0; loopi(MODEHISTLEN) lastplayedmodes[i] = -1; current = NULL; } void clear() { configsets.shrink(0); loopi(GMODE_NUM) base[i].empty(); } bool load() { preloaded.shrink(0); loopi(GMODE_NUM) prebase[i].empty(); if(serverconfigfile::load()) { configset c, cempty; memset(&cempty, 0, sizeof(cempty)); cempty.empty(); int line = 0; char *b, *l, *p = buf; while(p < buf + filelen) { l = p; p += strlen(p) + 1; line++; l = strtok_r(l, " ", &b); if(l) { c = cempty; char *e = strchr(l, '*'); c.asterisk = e ? clamp(int(e - l), 0, MAXMAPNAMELEN) : -1; filtertext(c.mapname, behindpath(l), FTXT__MAPNAME, MAXMAPNAMELEN); bool haskeywords = false; l = strtok_r(NULL, " ", &b); if(l) c.mmask = gmode_parse(l); while((l = strtok_r(NULL, " ", &b))) { if(c.parse(l)) haskeywords = true; } if(haskeywords) { if(c.asterisk == 0) { loopj(GMODE_NUM) if((1 << j) & c.mmask) prebase[j].add(c); } else preloaded.add(c); } } } // prepare compiled maprot message for clients return true; } return false; } void read() { loopi(GMODE_NUM) base[i] = prebase[i]; loopv(preloaded) configsets.add(preloaded[i]); mlog(ACLOG_INFO,"read %d map rotation entries from '%s'", configsets.length(), filename); } void initmap(servermap *m, stream *f) // set maprot parameters of a servermap { configsetvalues e; memcpy(m->modes, base, sizeof(m->modes)); if(m->hx_modeinfo && use_hx_modeinfo_mask) loopj(GMODE_NUM) { e.empty(); loopk(CONFIG_MAXPAR) if(use_hx_modeinfo_mask & (1 << k)) e.par[k] = m->hx_modeinfo[j].par[k]; m->modes[j].add(e); } loopv(configsets) { configset &c = configsets[i]; if(c.asterisk < 0 ? !strcmp(c.mapname, m->fname) : !strncmp(c.mapname, m->fname, c.asterisk)) { loopj(GMODE_NUM) if((1 << j) & c.mmask) m->modes[j].add(c); } } m->modes_allowed = m->entstats.modes_possible & (isdedicated ? GMMASK__MPNOCOOP : GMMASK__ALL); int rm[CR_NUM] = { 0 }, man = 0; loopj(GMODE_NUM) { if(m->modes[j].manual > 0) man |= 1 << j; for(int i = max(0, (int)m->modes[j].restrict); i < CR_NUM; i++) rm[i] |= 1 << j; } m->modes_auto = m->isautoplay() ? m->modes_allowed & ~man : 0; loopi(CR_NUM) m->modes_cr[i] = m->modes_allowed & rm[i]; loopi(MAXPLAYERS) m->modes_pn[i] = 0; loopj(GMODE_NUM) { if((1 << j) & m->modes_allowed) { int lb = m->modes[j].minplayers < 1 ? 0 : m->modes[j].minplayers - 1, ub = m->modes[j].maxplayers < 1 || m->modes[j].maxplayers > MAXPLAYERS ? MAXPLAYERS : m->modes[j].maxplayers; while(lb < ub) m->modes_pn[lb++] |= 1 << j; } } if(f) { string s; f->printf("maprot parameters for: %s%s\n", m->fpath, m->fname); e.empty(); loopj(GMODE_NUM) if((1 << j) & GMMASK__MPNOCOOP) { f->printf(" mode %s:", gmode_enum(1 << j, s)); loopk(CONFIG_MAXPAR) if(e.par[k] != m->modes[j].par[k]) f->printf(" %s:%d", maprotkeywords[k], m->modes[j].par[k]); f->printf("\n"); } f->printf(" modes_allowed %s\n", gmode_enum(m->modes_allowed, s)); f->printf(" modes_auto %s\n", gmode_enum(m->modes_auto, s)); f->printf(" modes man %s\n", gmode_enum(man, s)); loopi(CR_NUM) f->printf(" modes_cr[%d] %s\n", i, gmode_enum(m->modes_cr[i], s)); loopi(MAXPLAYERS) f->printf(" modes_pn[%02d] %s\n", i + 1, gmode_enum(m->modes_pn[i], s)); f->printf("\n"); } } void oneminutepassed(int mode) { lastplayedmodes[lastplayedmodepointer++] = mode; lastplayedmodepointer %= MODEHISTLEN; } void calcmodepenalties(int weight) // sum up, how many minutes each game mode was played during the last hour (including server-empty times) { weight = (weight * PENALTYUNIT) / 100; loopi(GMODE_NUM) modepenalty[i] = 0; loopi(MODEHISTLEN) { int n = lastplayedmodes[i]; if(n >= 0 && (GMMASK__MPNOCOOP & (1 << n))) modepenalty[n] += weight; } } void calcmappenalties(); // sum up, how long ago each map+mode was played servermap *recalcgamesuggestions(int numpl); // regenerate list of playable games (map+mode) servermap *imfeelinglucky(int numpl = -1) { return recalcgamesuggestions(numpl); } servermap *getcurrent() { if(!current) current = imfeelinglucky(); return current; } servermap *next(bool notify = true, bool nochange = false) // load next maprotation set { #ifndef STANDALONE if(!isdedicated) { defformatstring(nextmapalias)("nextmap_%s", getclientmap()); const char *map = getalias(nextmapalias); // look up map in the cycle startgame(map && notify ? map : getclientmap(), getclientmode(), -1, notify); return NULL; } #endif servermap *s = imfeelinglucky(numclients()); if(!nochange) { current = s; startgame(s->fname, s->bestmode, s->modes[s->bestmode].time, notify); } return s; } void restart(bool notify = true) // restart current map { #ifndef STANDALONE if(!isdedicated) { startgame(getclientmap(), getclientmode(), -1, notify); return; } #endif servermap *s = getcurrent(); startgame(s->fname, s->bestmode, s->modes[s->bestmode].time, notify); } }; // generic commented IP list struct serveripcclist : serverconfigfile { vector ipranges; bool parsecomment(iprangecc &ipr, const char *l) { memset(ipr.cc, 0, 4); l += strspn(l, " "); loopi(3) if(isprint(l[i])) ipr.cc[i] = l[i]; else break; return ipr.cc[0] && ipr.cc[1]; } const char *printcomment(iprangecc &ipr) { return ipr.cc[3] == '\0' ? ipr.cc : "ERR"; } int cmpcomment(iprangecc &ipr1, iprangecc &ipr2) { return ipr1.ci - ipr2.ci; } void clear() { ipranges.shrink(0); } void read() { iprangecc ir; int line = 0, errors = 0, concatd = 0; char *l, *r, *p = buf; mlog(ACLOG_VERBOSE,"reading ip list '%s'", filename); while(p < buf + filelen) { l = p + strspn(p, " "); p += strlen(p) + 1; line++; if(!l[0]) continue; if((r = (char *) atoipr(l, &ir)) && parsecomment(ir, r)) { if(ipranges.length() && ipranges.last().ur == ir.lr - 1 && !cmpcomment(ipranges.last(), ir)) { ipranges.last().ur = ir.ur; // direct concatenation (for pre-sorted lists) concatd++; } else ipranges.add(ir); } else mlog(ACLOG_INFO," error in line %d, file %s: failed to parse '%s'", line, filename, r ? r : l), errors++; } ipranges.sort(cmpiprange); int orglength = ipranges.length(); string b1, b2; loopv(ipranges) { // make sure, ranges don't overlap - otherwise bsearch gets unpredictable if(!i) continue; if(ipranges[i].ur <= ipranges[i - 1].ur) { if(cmpcomment(ipranges[i], ipranges[i - 1])) mlog(ACLOG_INFO," error: IP list entry %s|%s deleted because of overlapping %s|%s", iprtoa(ipranges[i], b1), printcomment(ipranges[i]), iprtoa(ipranges[i - 1], b2), printcomment(ipranges[i - 1])); else { if(ipranges[i].lr == ipranges[i - 1].lr && ipranges[i].ur == ipranges[i - 1].ur) mlog(ACLOG_VERBOSE," IP list entry %s got dropped (double entry)", iprtoa(ipranges[i], b1)); else mlog(ACLOG_VERBOSE," IP list entry %s got dropped (already covered by %s)", iprtoa(ipranges[i], b1), iprtoa(ipranges[i - 1], b2)); } ipranges.remove(i--); continue; } if(ipranges[i].lr <= ipranges[i - 1].ur) { if(!cmpcomment(ipranges[i], ipranges[i - 1])) // same comment { mlog(ACLOG_VERBOSE," IP list entries %s and %s are joined due to overlap (both %s)", iprtoa(ipranges[i - 1], b1), iprtoa(ipranges[i], b2), printcomment(ipranges[i])); ipranges[i - 1].ur = ipranges[i].ur; ipranges.remove(i--); continue; } else { mlog(ACLOG_INFO," error: IP list entries %s|%s and %s|%s are overlapping - dropping %s|%s", iprtoa(ipranges[i - 1], b1), printcomment(ipranges[i - 1]), iprtoa(ipranges[i], b2), printcomment(ipranges[i]), b2, printcomment(ipranges[i])); errors++; ipranges.remove(i--); continue; } } if(ipranges[i].lr - 1 == ipranges[i - 1].ur && !cmpcomment(ipranges[i], ipranges[i - 1])) { mlog(ACLOG_VERBOSE," concatenating IP list entries %s and %s (both %s)", iprtoa(ipranges[i - 1], b1), iprtoa(ipranges[i], b2), printcomment(ipranges[i])); ipranges[i - 1].ur = ipranges[i].ur; ipranges.remove(i--); continue; } } if(logcheck(ACLOG_VERBOSE)) loopv(ipranges) mlog(ACLOG_VERBOSE," %s %s", iprtoa(ipranges[i], b1), printcomment(ipranges[i])); mlog(ACLOG_INFO,"read %d (%d) IP list entries from '%s', %d errors, %d concatenated", ipranges.length(), orglength, filename, errors, concatd); } const char *check(enet_uint32 ip, bool lock = false) // ip: host byte order { if(lock) busy.wait(); iprangecc t, *res; t.lr = ip; t.ur = 0; res = ipranges.search(&t, cmpipmatch); const char *cc = res ? printcomment(*res) : NULL; if(lock) busy.post(); return cc; } }; // serverblacklist.cfg struct serveripblacklist : serverconfigfile { vector ipranges; void clear() { ipranges.shrink(0); } void read() { iprange ir; int line = 0, errors = 0; char *l, *r, *p = buf; mlog(ACLOG_VERBOSE,"reading ip blacklist '%s'", filename); while(p < buf + filelen) { l = p; p += strlen(p) + 1; line++; if((r = (char *) atoipr(l, &ir))) { ipranges.add(ir); l = r; } if(l[strspn(l, " ")]) { for(int i = (int)strlen(l) - 1; i > 0 && l[i] == ' '; i--) l[i] = '\0'; mlog(ACLOG_INFO," error in line %d, file %s: ignored '%s'", line, filename, l); errors++; } } ipranges.sort(cmpiprange); int orglength = ipranges.length(); string b1, b2; loopv(ipranges) { if(!i) continue; if(ipranges[i].ur <= ipranges[i - 1].ur) { if(ipranges[i].lr == ipranges[i - 1].lr && ipranges[i].ur == ipranges[i - 1].ur) mlog(ACLOG_VERBOSE," blacklist entry %s got dropped (double entry)", iprtoa(ipranges[i], b1)); else mlog(ACLOG_VERBOSE," blacklist entry %s got dropped (already covered by %s)", iprtoa(ipranges[i], b1), iprtoa(ipranges[i - 1], b2)); ipranges.remove(i--); continue; } if(ipranges[i].lr <= ipranges[i - 1].ur) { mlog(ACLOG_VERBOSE," blacklist entries %s and %s are joined due to overlap", iprtoa(ipranges[i - 1], b1), iprtoa(ipranges[i], b2)); ipranges[i - 1].ur = ipranges[i].ur; ipranges.remove(i--); continue; } } if(logcheck(ACLOG_VERBOSE)) loopv(ipranges) mlog(ACLOG_VERBOSE," %s", iprtoa(ipranges[i], b1)); mlog(ACLOG_INFO,"read %d (%d) blacklist entries from '%s', %d errors", ipranges.length(), orglength, filename, errors); } bool check(enet_uint32 ip) // ip: network byte order { iprange t; t.lr = ENET_NET_TO_HOST_32(ip); // blacklist uses host byte order t.ur = 0; return ipranges.search(&t, cmpipmatch) != NULL; } }; // nicknameblacklist.cfg #define MAXNICKFRAGMENTS 5 enum { NWL_UNLISTED = 0, NWL_PASS, NWL_PWDFAIL, NWL_IPFAIL }; struct servernickblacklist : serverconfigfile { struct iprchain { struct iprange ipr; const char *pwd; int next; }; struct blackline { int frag[MAXNICKFRAGMENTS]; bool ignorecase; int line; void clear() { loopi(MAXNICKFRAGMENTS) frag[i] = -1; } }; hashtable whitelist; vector whitelistranges; vector blacklines; vector blfraglist; void clear() { whitelistranges.setsize(0); enumeratek(whitelist, const char *, key, delete key); whitelist.clear(false); blfraglist.deletecontents(); blacklines.setsize(0); } void read() { const char *sep = " "; int line = 1, errors = 0; iprchain iprc; blackline bl; char *b, *l, *s, *r, *p = buf; mlog(ACLOG_VERBOSE,"reading nickname blacklist '%s'", filename); while(p < buf + filelen) { l = p; p += strlen(p) + 1; l = strtok_r(l, sep, &b); if(l) { s = strtok_r(NULL, sep, &b); int ic = 0; if(s && (!strcmp(l, "accept") || !strcmp(l, "a"))) { // accept nickname IP-range int *i = whitelist.access(s); if(!i) i = &whitelist.access(newstring(s), -1); s += strlen(s) + 1; while(s < p) { r = (char *) atoipr(s, &iprc.ipr); s += strspn(s, sep); iprc.pwd = r && *s ? NULL : newstring(s, strcspn(s, sep)); if(r || *s) { iprc.next = *i; *i = whitelistranges.length(); whitelistranges.add(iprc); s = r ? r : s + strlen(iprc.pwd); } else break; } s = NULL; } else if(s && (!strcmp(l, "block") || !strcmp(l, "b") || ic++ || !strcmp(l, "blocki") || !strcmp(l, "bi"))) { // block nickname fragments (ic == ignore case) bl.clear(); loopi(MAXNICKFRAGMENTS) { if(ic) strtoupper(s); loopvj(blfraglist) { if(!strcmp(s, blfraglist[j])) { bl.frag[i] = j; break; } } if(bl.frag[i] < 0) { bl.frag[i] = blfraglist.length(); blfraglist.add(newstring(s)); } s = strtok_r(NULL, sep, &b); if(!s) break; } bl.ignorecase = ic > 0; bl.line = line; blacklines.add(bl); } else { mlog(ACLOG_INFO," error in line %d, file %s: unknown keyword '%s'", line, filename, l); errors++; } if(s && s[strspn(s, " ")]) { mlog(ACLOG_INFO," error in line %d, file %s: ignored '%s'", line, filename, s); errors++; } } line++; } if(logcheck(ACLOG_VERBOSE)) { mlog(ACLOG_VERBOSE," nickname whitelist (%d entries):", whitelist.numelems); string text, b; enumeratekt(whitelist, const char *, key, int, idx, { text[0] = '\0'; for(int i = idx; i >= 0; i = whitelistranges[i].next) { iprchain &ic = whitelistranges[i]; if(ic.pwd) concatformatstring(text, " pwd:\"%s\"", hiddenpwd(ic.pwd)); else concatformatstring(text, " %s", iprtoa(ic.ipr, b)); } mlog(ACLOG_VERBOSE, " accept %s%s", key, text); }); mlog(ACLOG_VERBOSE," nickname blacklist (%d entries):", blacklines.length()); loopv(blacklines) { text[0] = '\0'; loopj(MAXNICKFRAGMENTS) { int k = blacklines[i].frag[j]; if(k >= 0) { concatstring(text, " "); concatstring(text, blfraglist[k]); } } mlog(ACLOG_VERBOSE, " %2d block%s%s", blacklines[i].line, blacklines[i].ignorecase ? "i" : "", text); } } mlog(ACLOG_INFO,"read %d + %d entries from nickname blacklist file '%s', %d errors", whitelist.numelems, blacklines.length(), filename, errors); } int checkwhitelist(const client &c) { if(c.type != ST_TCPIP) return NWL_PASS; iprange ipr; ipr.lr = ENET_NET_TO_HOST_32(c.peer->address.host); // blacklist uses host byte order int *idx = whitelist.access(c.name); if(!idx) return NWL_UNLISTED; // no matching entry int i = *idx; bool needipr = false, iprok = false, needpwd = false, pwdok = false; while(i >= 0) { iprchain &ic = whitelistranges[i]; if(ic.pwd) { // check pwd needpwd = true; if(pwdok || !strcmp(genpwdhash(c.name, ic.pwd, c.salt), c.pwd)) pwdok = true; } else { // check IP needipr = true; if(!cmpipmatch(&ipr, &ic.ipr)) iprok = true; // range match found } i = whitelistranges[i].next; } if(needpwd && !pwdok) return NWL_PWDFAIL; // wrong PWD if(needipr && !iprok) return NWL_IPFAIL; // wrong IP return NWL_PASS; } int checkblacklist(const char *name) { if(blacklines.empty()) return -2; // no nickname blacklist loaded string nameuc; copystring(nameuc, name); strtoupper(nameuc); loopv(blacklines) { loopj(MAXNICKFRAGMENTS) { int k = blacklines[i].frag[j]; if(k < 0) return blacklines[i].line; // no more fragments to check if(strstr(blacklines[i].ignorecase ? nameuc : name, blfraglist[k])) { if(j == MAXNICKFRAGMENTS - 1) return blacklines[i].line; // all fragments match } else break; // this line no match } } return -1; // no match } }; #define FORBIDDENSIZE 15 bool issimilar (char s, char d) { s = tolower(s); d = tolower(d); if ( s == d ) return true; switch (d) { case 'a': if ( s == '@' || s == '4' ) return true; break; case 'c': if ( s == 'k' ) return true; break; case 'e': if ( s == '3' ) return true; break; case 'i': if ( s == '!' || s == '1' ) return true; break; case 'o': if ( s == '0' ) return true; break; case 's': if ( s == '$' || s == '5' ) return true; break; case 't': if ( s == '7' ) return true; break; case 'u': if ( s == '#' ) return true; break; } return false; } bool findpattern (char *s, char *d) // returns true if there is more than 80% of similarity { int len, hit = 0; if (!d || (len = strlen(d)) < 1) return false; char *dp = d, *s_end = s + strlen(s); while (s != s_end) { if ( *s == ' ' ) // spaces separate words { if ( !issimilar(*(s+1),*dp) ) { dp = d; hit = 0; } // d e t e c t i t } else if ( issimilar(*s,*dp) ) { dp++; hit++; } // hit! else if ( hit > 0 ) // this is not a pair, but there is a previous pattern { if (*s == '.' || *s == *(s-1) || issimilar(*(s+1),*dp) ); // separator or typo (do nothing) else if ( issimilar(*(s+1),*(dp+1)) || *s == '*' ) dp++; // wild card or typo else hit--; // maybe this is nothing } else dp = d; // nothing here s++; // walk on the string if ( hit && 5 * hit > 4 * len ) return true; // found it! } return false; } struct serverforbiddenlist : serverconfigfile { int num; char entries[100][2][FORBIDDENSIZE+1]; // 100 entries and 2 words per entry is more than enough void clear() { num = 0; memset(entries,'\0',2*100*(FORBIDDENSIZE+1)); } void addentry(char *s) { int len = strlen(s); if ( len > 128 || len < 3 ) return; int n = 0; string s1, s2; char *c1 = s1, *c2 = s2; if (num < 100 && (n = sscanf(s,"%s %s",s1,s2)) > 0 ) // no warnings { copystring(entries[num][0],c1,FORBIDDENSIZE); if ( n > 1 ) copystring(entries[num][1],c2,FORBIDDENSIZE); else entries[num][1][0]='\0'; num++; } } void read() { char *l, *p = buf; mlog(ACLOG_VERBOSE,"reading forbidden list '%s'", filename); while(p < buf + filelen) { l = p; p += strlen(p) + 1; addentry(l); } } bool canspeech(char *s) { for (int i=0; i adminpwds; int staticpasses; serverpasswords() : staticpasses(0) {} void init(const char *name, const char *cmdlinepass) { if(cmdlinepass[0]) { pwddetail c; copystring(c.pwd, cmdlinepass); c.line = 0; // commandline is 'line 0' c.denyadmin = false; adminpwds.add(c); } staticpasses = adminpwds.length(); serverconfigfile::init(name); } void clear() { adminpwds.shrink(staticpasses); } void read() { pwddetail c; const char *sep = " "; int i, line = 1, par[ADMINPWD_MAXPAR]; char *b, *l, *p = buf; mlog(ACLOG_VERBOSE,"reading admin passwords '%s'", filename); while(p < buf + filelen) { l = p; p += strlen(p) + 1; l = strtok_r(l, sep, &b); if(l) { copystring(c.pwd, l); par[0] = 0; // default values for(i = 0; i < ADMINPWD_MAXPAR; i++) { if((l = strtok_r(NULL, sep, &b)) != NULL) par[i] = atoi(l); else break; } //if(i > 0) { c.line = line; c.denyadmin = par[0] > 0; adminpwds.add(c); mlog(ACLOG_VERBOSE,"line%4d: %s %d", c.line, hiddenpwd(c.pwd), c.denyadmin ? 1 : 0); } } line++; } mlog(ACLOG_INFO,"read %d admin passwords from '%s'", adminpwds.length() - staticpasses, filename); } bool check(const char *name, const char *pwd, int salt, pwddetail *detail = NULL, enet_uint32 address = 0) { bool found = false; if (address && passguy == address) { if (passtime + 3000 > servmillis || ( passtries > 5 && passtime + 10000 > servmillis )) { passtries++; passtime = servmillis; return false; } else { if ( passtime + 60000 < servmillis ) passtries = 0; } passtries++; } else { passtries = 0; } passguy = address; passtime = servmillis; loopv(adminpwds) { if(!strcmp(genpwdhash(name, adminpwds[i].pwd, salt), pwd)) { if(detail) *detail = adminpwds[i]; found = true; break; } } return found; } }; // serverinfo_en.txt, motd_en.txt #define MAXINFOLINELEN 100 // including color codes struct serverinfofile : serverconfigfile // plaintext info file, used for serverinfo and motd { vector msg; int maxlen; void init(const char *fnbase, int ml) { maxlen = ml; defformatstring(fname)("%s_en.txt", fnbase); serverconfigfile::init(fname); } void clear() { msg.shrink(0); } void read() { string s; char *l, *p = buf; mlog(ACLOG_VERBOSE,"reading info text file '%s'", filename); while(p < buf + filelen) { l = p; p += strlen(p) + 1; ASSERT(MAXINFOLINELEN < MAXSTRLEN); filtertext(s, l, FTXT__SERVERINFOLINE, MAXINFOLINELEN); if(*s) cvecprintf(msg, "%s\n", s); } if(msg.length() > maxlen) { mlog(ACLOG_VERBOSE,"info text file '%s' truncated: %d -> %d", filename, msg.length(), maxlen); msg.shrink(maxlen); } } const char *getmsg() { if(!msg.length() || msg.last()) msg.add('\0'); // make sure, msg is terminated return msg.getbuf(); } char *getmsgline(string s, int *pos) { s[0] = '\0'; if(msg.inrange(*pos)) { const char *p = getmsg() + *pos; int len = strcspn(p, "\n") + 1; copystring(s, p, len); *pos += len; } return s; } }; // realtime server parameters enum { SID_INT, SID_STR }; struct servpar { int type; // one of SPAR_* above const char *name; union { int minval; // SID_INT int minlen; // SID_STR }; union { int maxval; // SID_INT int maxlen; // SID_STR }; union { int *i; // SID_INT char *s; // SID_STR }; union { int *shadow_i; // SID_INT char *shadow_s; // SID_STR }; void (*fun)(); union { const char **list; // SID_INT const char *defaultstr; // SID_STR }; union { int defaultint; // SID_INT int filter; // SID_STR }; #ifdef _DEBUG const char *desc; int chapter; #endif bool logchanges; // log value changes (careful!) bool fromfile; // true: value can be set by config file, false: value is set by server bool fromfile_org; // "fromfile" as initially set (a backup to undo manual changes) servpar() {} // SID_INT servpar(int type, const char *name, int minval, int maxval, const char *list[], int *i, int *shadow_i, int defval, void (*fun)(), bool logchanges, bool fromfile, const char *_desc) : type(type), name(name), minval(minval), maxval(maxval), i(i), shadow_i(shadow_i), fun(fun), list(list), defaultint(defval), logchanges(logchanges), fromfile(fromfile), fromfile_org(fromfile) { DEBUGCODE(desc = _desc); } // SID_STR servpar(int type, const char *name, int minlen, int maxlen, int filter, char *s, char *shadow_s, const char *defaultstr, void (*fun)(), bool logchanges, bool fromfile, const char *_desc) : type(type), name(name), minlen(minlen), maxlen(maxlen), s(s), shadow_s(shadow_s), fun(fun), defaultstr(defaultstr), filter(filter), logchanges(logchanges), fromfile(fromfile), fromfile_org(fromfile) { DEBUGCODE(desc = _desc); } void update() { if(fromfile_org) switch(type) { case SID_INT: { if(*i != *shadow_i) { int tmp = *i; *i = *shadow_i; *shadow_i = tmp; // for the duration of fun(), last_parname _is_ actually the old value (at any other time, it's the next value) if(fun) ((void (__cdecl *)())fun)(); *shadow_i = *i; } break; } case SID_STR: { if(strcmp(s, shadow_s)) { if(fun) { char *tmp = new char[maxlen + 1]; s[maxlen] = shadow_s[maxlen] = '\0'; strncpy(tmp, s, maxlen + 1); strncpy(s, shadow_s, maxlen + 1); strncpy(shadow_s, tmp, maxlen + 1); // for the duration of fun(), last_parname _is_ actually the old value (at any other time, it's the next value) ((void (__cdecl *)())fun)(); strncpy(shadow_s, s, maxlen + 1); delete[] tmp; } else strncpy(s, shadow_s, maxlen + 1); } break; } } } bool nextvalue(char *nv) { switch(type) { case SID_INT: { int ni = ATOI(nv), idx; if(list && (idx = getlistindex(nv, list, false, -1)) >= 0) ni = idx + minval; // multiple choice if(ni >= minval && ni <= maxval) { *shadow_i = ni; return true; } break; } case SID_STR: { filtertext(nv, nv, filter); int ni = (int)strlen(nv); if(ni >= minlen && ni <= maxlen) { strncpy(shadow_s, nv, maxlen + 1); return true; } break; } } return false; } }; hashtable *servpars = NULL; int addservparint(const char *name, int minval, int cur, int maxval, const char *list[], int *storage, int *shadowstorage, void (*fun)(), bool logchanges, bool fromfile, const char *desc) { if(!servpars) servpars = new hashtable; if(list && minval == maxval) while(list[maxval - minval + 1][0]) maxval++; // get list length ASSERT((fromfile && cur >= minval && cur <= maxval) || (!fromfile && minval < ACLOG_NUM)); servpar v(SID_INT, name, minval, maxval, list, storage, shadowstorage, cur, fun, logchanges, fromfile, desc); servpars->access(name, v); *shadowstorage = cur; return cur; } bool addservparstr(const char *name, int minlen, int maxlen, int filt, const char *cur, char *storage, char *shadowstorage, void (*fun)(), bool logchanges, bool fromfile, const char *desc) { if(!servpars) servpars = new hashtable; ASSERT(((fromfile && maxlen > minlen && minlen >= 0 && int(strlen(cur)) >= minlen) || (!fromfile && minlen < ACLOG_NUM)) && int(strlen(cur)) <= maxlen); servpar v(SID_STR, name, minlen, maxlen, filt, storage, shadowstorage, cur, fun, logchanges, fromfile, desc); servpars->access(name, v); strncpy(storage, cur, maxlen + 1); strncpy(shadowstorage, cur, maxlen + 1); storage[maxlen] = shadowstorage[maxlen] = '\0'; return true; } struct serverparameter : serverconfigfile { bool load() { if(serverconfigfile::load()) { int line = 0; char *b, *l, *p = buf, *kb, *k, *v; servpar *id; while(p < buf + filelen) { l = p; p += strlen(p) + 1; line++; l = strtok_r(l, " ", &b); if(l && (k = strtok_r(l, ":", &kb)) && (v = strtok_r(NULL, ":", &kb)) && (id = servpars->access(k)) && id->fromfile) { if(id->type == SID_STR && (v - k) - strlen(k) > 1) filterrichtext(v, v); id->nextvalue(v); } } return true; } return false; } void read() { // in mainloop: update values from shadow values enumerate(*servpars, servpar, id, id.update()); } void dump(stream *f) { enumerate(*servpars, servpar, id, switch(id.type) { case SID_INT: f->printf("%s:%d // default %d\n", id.name, *id.i, id.defaultint); break; case SID_STR: f->printf("%s:%s // default %s\n", id.name, id.s, id.defaultstr); break; } ); } void log() { // in mainloop: check for changed values and log if necessary enumerate(*servpars, servpar, id, { if(id.logchanges) switch(id.type) { case SID_INT: { if(*id.i != *id.shadow_i) { if(id.fun) ((void (__cdecl *)())id.fun)(); mlog(id.minval, "server statistics parameter %s changed from %d to %d", id.name, *id.shadow_i, *id.i); *id.shadow_i = *id.i; } break; } case SID_STR: { if(strcmp(id.s, id.shadow_s)) { id.s[id.maxlen] = id.shadow_s[id.maxlen] = '\0'; if(id.fun) ((void (__cdecl *)())id.fun)(); string tmp1; string tmp2; filtertext(tmp1, id.shadow_s, id.filter); filtertext(tmp2, id.s, id.filter); mlog(id.minval, "server statistics parameter %s changed from \"%s\" to \"%s\"", id.name, tmp1, tmp2); strncpy(id.shadow_s, id.s, id.maxlen + 1); id.shadow_s[id.maxlen] = '\0'; } break; } } }); } }; // optionally overwrite builtin default values with commandline parameters - before updating from file starts void initserverparameter(const char *name, int value) { ASSERT(servpars); servpar *id = servpars->access(name); ASSERT(id && id->type == SID_INT); if(value >= id->minval && value <= id->maxval) { *id->shadow_i = value; id->update(); } } void initserverparameter(const char *name, const char *value) { ASSERT(servpars); servpar *id = servpars->access(name); ASSERT(id && id->type == SID_STR); string tmp; filtertext(tmp, value, id->filter); int len = (int)strlen(tmp); if(len >= id->minlen && len <= id->maxlen) { strncpy(id->shadow_s, value, id->maxlen + 1); id->update(); } } void servparallfun() // trigger all functions once, to make sure, unchanged defaults are translated into filtered values { enumerate(*servpars, servpar, id, { if(id.fun) ((void (__cdecl *)())id.fun)(); }); } // write a nice template for serverparameters.cfg #ifdef _DEBUG int siddocsort(servpar **a, servpar **b) { return (*a)->chapter == (*b)->chapter ? strcmp((*a)->name, (*b)->name) : (*a)->chapter - (*b)->chapter; } const char *siddocchapters[] = { "dDebug switches", "mMisc settings", "vVote settings", "gMaprot settings", "sServer setup settings", "CCommandline switch overrides", "DDemo recording settings", "lServer load statistics", "" }; const char *siddocchaptersorting = "sgDvmdC", *siddocchaptercommentedout = "CD"; void serverparameters_dumpdocu(char *fname) { vector sp; enumerate(*servpars, servpar, id, if(id.desc && id.desc[0]) sp.add(&id)); loopv(sp) { servpar &id = *sp[i]; const char *e = strchr(siddocchaptersorting, id.desc[0]); id.chapter = e ? (int) (e - siddocchaptersorting) : id.desc[0]; if(!id.fromfile) id.chapter |= 1 << 20; if(!id.logchanges) id.chapter |= 1 << 19; } sp.sort(siddocsort); stream *f = openfile(path(fname), "w"); int lastchap = 0; if(f) { f->printf("// serverparameters.cfg\n//\n// parameters from this file are read once per minute by the server\n// and are effective immediately\n\n" "// each setting consists of a keyword and a value, written like this: keyword:value\n// there are no spaces allowed between keyword and value\n" "// using two colons (keyword::value) activates rich-text filtering for the value\n\n\n"); loopv(sp) { servpar &id = *sp[i]; if(lastchap != id.desc[0]) { int ch = -1; for(int k = 0; siddocchapters[k][0]; k++) if(siddocchapters[k][0] == id.desc[0]) ch = k; lastchap = id.desc[0]; if(ch < 0) f->printf("\n// **** untitled chapter %c ****\n\n", lastchap); else f->printf("\n// **** %s ****\n\n", siddocchapters[ch] + 1); } const char *commentout = strchr(siddocchaptercommentedout, id.desc[0]) != NULL ? "//" : ""; if(id.fromfile) switch(id.type) { case SID_INT: { f->printf("// %s %s\n// integer [%d..%d], default %d", id.name, id.desc + 1, id.minval, id.maxval, id.defaultint); if(id.list) { for(int k = 0; id.list[k] && id.list[k][0]; k++) f->printf(", %d:%s", id.minval + k, id.list[k]); } f->printf("\n%s%s:%d\n\n", commentout, id.name, id.defaultint); break; } case SID_STR: { f->printf("// %s %s\n// string [%d..%d chars], default \"%s\"\n%s%s:%s\n\n", id.name, id.desc + 1, id.minlen, id.maxlen, id.defaultstr, commentout, id.name, id.defaultstr); break; } } else switch(id.type) // stat parameter { case SID_INT: case SID_STR: { f->printf("// %s %s", id.name, id.desc + 1); if(id.logchanges) f->printf(" (changes logged at level %d)", id.type == SID_INT ? id.minval : id.minlen); f->printf("\n\n"); break; } } } f->printf("\n\n"); delete f; } } #endif // player ID bookkeeping const char *vskeywords[VS_NUM + 1] = { "first", "last", "mute", "novote", "ban", "admin", "owner", "mapupload", "minconn", "minact", "flags", "frags", "deaths", "tks", "damage", "ff", "" }; const char *vsnames[VS_NUM + 1] = { "first login", "last login", "muted until", "novote until", "banned until", "admin until", "owner until", "mapupload until", "minutes connected", "minutes active", "flags", "frags", "deaths", "teamkills", "damage dealt", "team damage dealt", "" }; hashtable vitas; void vita_s::addname(const char *name) { int rem = 0; for(; rem < VITANAMEHISTLEN - 1; rem++) if(!strcasecmp(name, namehist[rem])) break; if(rem) memmove(namehist[1], namehist[0], rem * (MAXNAMELEN + 1)); copystring(namehist[0], name, MAXNAMELEN + 1); } void vita_s::addip(enet_uint32 ip) { int rem = 0; for(; rem < VITAIPHISTLEN - 1; rem++) if(iphist[rem] == ip) break; if(rem) memmove(iphist + 1, iphist, rem * sizeof(ip)); iphist[0] = ip; } void vita_s::addcc(const char *cc) { int rem = 0; for(; rem < VITACCHISTLEN - 1; rem++) if(cchist[rem * 2] == cc[0] && cchist[rem * 2 + 1] == cc[1]) break; if(rem) memmove(cchist + 2, iphist, rem * 2); loopi(2) cchist[i] = cc[i]; } int parsevitas(char *buf, int filelen) { vita_s v, *vp = &v; uchar32 pubkey; string tmp; enet_uint32 ip; int res = 0; char *l, *r, *p = buf, *pe = p + filelen, *b, *o; while(p < pe) { l = p; p += strlen(p) + 1; l = strtok_r(l, " \t", &b); while(l && (r = strtok_r(NULL, " \t", &b))) { filterrichtext(r, r); if(!strcmp(l, "PUBKEY")) { if(hex2bin(pubkey.u, r, 32) == 32) { if(!(vp = vitas.access(pubkey))) { memset(&v, 0, sizeof(v)); vp = &vitas.access(pubkey, v); } res++; } else vp = &v; // bad pubkey: point to dummy break; } else if(!strcmp(l, "NAME")) { filtertext(tmp, r, FTXT__PLAYERNAME, MAXNAMELEN); if(*tmp) vp->addname(tmp); } else if(!strcmp(l, "PUBCOM") || !strcmp(l, "PRIVCOM")) { filtertext(tmp, r, FTXT__VITACOMMENT, VITACOMMENTLEN); copystring(l[1] == 'U' ? vp->publiccomment : vp->privatecomment, tmp, VITACOMMENTLEN + 1); break; } else if(!strcmp(l, "IP")) { if(atoip(r, &ip) && ip) vp->addip(ip); } else if(!strcmp(l, "CC")) { filtercountrycode(r, r); if(*r != '-') vp->addcc(r); } else if(!strcmp(l, "CLAN")) { filtertext(tmp, r, FTXT__VITACLAN, VITACLANLEN); copystring(vp->clan, tmp, VITACLANLEN + 1); break; } else if(!strcmp(l, "STAT")) { if((o = strchr(r, ':'))) // key:value { *o++ = '\0'; // r is key, o is value loopk(VS_NUM) if(!strcmp(vskeywords[k], r)) { vp->vs[k] = atoi(o); break; } } } } } return res; } int readvitas(const char *fname) { serverconfigfile vsf; vsf.init(fname, false); vsf.load(); return vsf.buf ? parsevitas(vsf.buf, vsf.filelen) : -1; } void writevitas(stream *f, vector &vs) { string tmp; vector ctmp; f->printf("// AC server vita database\n// autosaved %s\n// (don't edit this file, while the server is running)\n\n", timestring(true, "%c", tmp)); loopvj(vs) { const vita_s &v = *vs[j].v; f->printf("PUBKEY %s", bin2hex(tmp, vs[j].k->u, 32)); if(v.namehist[0][0]) { f->printf("\n\tNAME"); loopirev(VITANAMEHISTLEN) if(v.namehist[i][0]) f->printf(" %s", escapestring(v.namehist[i], true, true, &ctmp)); } if(v.iphist[0]) { f->printf("\n\tIP"); loopirev(VITAIPHISTLEN) if(v.iphist[i]) f->printf(" %s", iptoa(v.iphist[i], tmp)); } if(v.privatecomment[0]) f->printf("\n\tPRIVCOM %s", escapestring(v.privatecomment, true, true, &ctmp)); if(v.publiccomment[0]) f->printf("\n\tPUBCOM %s", escapestring(v.publiccomment, true, true, &ctmp)); if(v.clan[0]) f->printf("\n\tCLAN %s", escapestring(v.clan, true, true, &ctmp)); if(v.cchist[0]) { f->printf("\n\tCC"); loopirev(VITACCHISTLEN) if(v.cchist[i * 2]) f->printf(" %c%c", v.cchist[i * 2], v.cchist[i * 2 + 1]); } f->printf("\n\tSTAT"); loopi(VS_NUM) if(v.vs[i]) f->printf(" %s:%d", vskeywords[i], v.vs[i]); f->printf("\n\n"); } }