// serverfiles.h // 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 "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_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) 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; 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); } bool isro() { return fpath == servermappath_off || fpath == servermappath_serv; } bool isofficial() { return fpath == servermappath_off; } int getmemusage() { return sizeof(struct servermap) + cgzlen + cfggzlen + layoutgzlen + numents * (sizeof(uchar) + sizeof(short) * 3); } 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) const char *err = NULL; stream *f = NULL; int restofhead; // load map files, prepare sendmap buffer defformatstring(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_MAPINFO: case HX_MODEINFO: // for new maprot... case HX_ARTIST: 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; } } 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) // 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; } } }; // 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 *logfileprefix) { static int readmaps_epoch = 0; while(1) { while(!startnewservermapsepoch) readmapsthread_sem->wait(); // wait without using the cpu if(logfileprefix) { defformatstring(logfilename)("%sreadmaps_log.txt", (const char *)logfileprefix); path(logfilename); readmaplog = openfile(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; } struct servermapbuffer // sending of maps between clients { string mapname; int cgzsize, cfgsize, cfgsizegz, revision, datasize; uchar *data, *gzbuf; servermapbuffer() : data(NULL) { gzbuf = new uchar[GZBUFSIZE]; } ~servermapbuffer() { delete[] gzbuf; } void clear() { DELETEA(data); revision = 0; } int available() { if(data && !strcmp(mapname, behindpath(smapname)) && cgzsize == smapstats.cgzsize) { if( !revision || (revision == smapstats.hdr.maprevision)) return cgzsize; } return 0; } void setrevision() { if(available() && !revision) revision = smapstats.hdr.maprevision; } void load(void) // load currently played map into the buffer (if distributable), clear buffer otherwise { string cgzname, cfgname; const char *name = behindpath(smapname); // no paths allowed here clear(); formatstring(cgzname)(SERVERMAP_PATH "%s.cgz", name); path(cgzname); if(fileexists(cgzname, "r")) { formatstring(cfgname)(SERVERMAP_PATH "%s.cfg", name); } else { formatstring(cgzname)(SERVERMAP_PATH_INCOMING "%s.cgz", name); path(cgzname); formatstring(cfgname)(SERVERMAP_PATH_INCOMING "%s.cfg", name); } path(cfgname); uchar *cgzdata = (uchar *)loadfile(cgzname, &cgzsize); uchar *cfgdata = (uchar *)loadfile(cfgname, &cfgsize); if(cgzdata && (!cfgdata || cfgsize < MAXCFGFILESIZE)) { uLongf gzbufsize = GZBUFSIZE; if(!cfgdata || compress2(gzbuf, &gzbufsize, cfgdata, cfgsize, 9) != Z_OK) { cfgsize = 0; gzbufsize = 0; } cfgsizegz = (int) gzbufsize; if(cgzsize + cfgsizegz < MAXMAPSENDSIZE) { // map is ok, fill buffer copystring(mapname, name); datasize = cgzsize + cfgsizegz; data = new uchar[datasize]; memcpy(data, cgzdata, cgzsize); memcpy(data + cgzsize, gzbuf, cfgsizegz); logline(ACLOG_INFO,"loaded map %s, %d + %d(%d) bytes.", cgzname, cgzsize, cfgsize, cfgsizegz); } } DELETEA(cgzdata); DELETEA(cfgdata); } bool sendmap(const char *nmapname, int nmapsize, int ncfgsize, int ncfgsizegz, uchar *ndata) { FILE *fp; bool written = false; if(!nmapname[0] || nmapsize <= 0 || ncfgsizegz < 0 || nmapsize + ncfgsizegz > MAXMAPSENDSIZE || ncfgsize > MAXCFGFILESIZE) return false; // malformed: probably modded client int cfgsize = ncfgsize; if(smode == GMODE_COOPEDIT && !strcmp(nmapname, behindpath(smapname))) { // update mapbuffer only in coopedit mode (and on same map) copystring(mapname, nmapname); datasize = nmapsize + ncfgsizegz; revision = 0; DELETEA(data); data = new uchar[datasize]; memcpy(data, ndata, datasize); } defformatstring(name)(SERVERMAP_PATH_INCOMING "%s.cgz", nmapname); path(name); fp = fopen(name, "wb"); if(fp) { fwrite(ndata, 1, nmapsize, fp); fclose(fp); formatstring(name)(SERVERMAP_PATH_INCOMING "%s.cfg", nmapname); path(name); fp = fopen(name, "wb"); if(fp) { uLongf rawsize = ncfgsize; if(uncompress(gzbuf, &rawsize, ndata + nmapsize, ncfgsizegz) == Z_OK && rawsize - ncfgsize == 0) fwrite(gzbuf, 1, cfgsize, fp); fclose(fp); written = true; } } return written; } void sendmap(client *cl, int chan) { if(!available()) return; packetbuf p(MAXTRANS + datasize, ENET_PACKET_FLAG_RELIABLE); putint(p, SV_RECVMAP); sendstring(mapname, p); putint(p, cgzsize); putint(p, cfgsize); putint(p, cfgsizegz); putint(p, revision); p.put(data, datasize); sendpacket(cl->clientnum, chan, p.finalize()); } }; // provide maps by the server enum { MAP_NOTFOUND = 0, MAP_TEMP, MAP_CUSTOM, MAP_LOCAL, MAP_OFFICIAL, MAP_VOID }; static const char * const maplocstr[] = { "not found", "temporary", "custom", "local", "official", "void" }; #define readonlymap(x) ((x) >= MAP_CUSTOM) #define distributablemap(x) ((x) == MAP_TEMP || (x) == MAP_CUSTOM) int findmappath(const char *mapname, char *filename) { if(!mapname[0]) return MAP_NOTFOUND; string tempname; if(!filename) filename = tempname; const char *name = behindpath(mapname); formatstring(filename)(SERVERMAP_PATH_BUILTIN "%s.cgz", name); path(filename); int loc = MAP_NOTFOUND; if(getfilesize(filename) > 10) loc = MAP_OFFICIAL; else { #ifndef STANDALONE copystring(filename, setnames(name)); if(!isdedicated && getfilesize(filename) > 10) loc = MAP_LOCAL; else { #endif formatstring(filename)(SERVERMAP_PATH "%s.cgz", name); path(filename); if(isdedicated && getfilesize(filename) > 10) loc = MAP_CUSTOM; else { formatstring(filename)(SERVERMAP_PATH_INCOMING "%s.cgz", name); path(filename); if(isdedicated && getfilesize(filename) > 10) loc = MAP_TEMP; } #ifndef STANDALONE } #endif } return loc; } mapstats *getservermapstats(const char *mapname, bool getlayout, int *maploc) { string filename; int ml; if(!maploc) maploc = &ml; *maploc = findmappath(mapname, filename); if(getlayout) DELETEA(maplayout); return *maploc == MAP_NOTFOUND ? NULL : loadmapstats(filename, getlayout); } // server config files void serverconfigfile::init(const char *name) { copystring(filename, name); path(filename); read(); } bool serverconfigfile::load() { DELETEA(buf); buf = loadfile(filename, &filelen); if(!buf) { logline(ACLOG_INFO,"could not read config file '%s'", filename); return false; } 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; } // maprot.cfg #define CONFIG_MAXPAR 6 struct configset { string mapname; union { struct { int mode, time, vote, minplayer, maxplayer, skiplines; }; int par[CONFIG_MAXPAR]; }; }; int FlagFlag = MINFF * 1000; int Mvolume, Marea, SHhits, Mopen = 0; float Mheight = 0; bool mapisok(mapstats *ms) { if ( Mheight > MAXMHEIGHT ) { logline(ACLOG_INFO, "MAP CHECK FAIL: The overall ceil height is too high (%.1f cubes)", Mheight); return false; } if ( Mopen > MAXMAREA ) { logline(ACLOG_INFO, "MAP CHECK FAIL: There is a big open area in this (hint: use more solid walls)"); return false; } if ( SHhits > MAXHHITS ) { logline(ACLOG_INFO, "MAP CHECK FAIL: Too high height in some parts of the map (%d hits)", SHhits); return false; } if ( ms->hasflags ) // Check if flags are ok { struct { short x, y; } fl[2]; loopi(2) { if(ms->flags[i] == 1) { short *fe = ms->entposs + ms->flagents[i] * 3; fl[i].x = *fe; fe++; fl[i].y = *fe; } else fl[i].x = fl[i].y = 0; // the map has no valid flags } FlagFlag = pow2(fl[0].x - fl[1].x) + pow2(fl[0].y - fl[1].y); } else FlagFlag = MINFF * 1000; // the map has no flags if ( FlagFlag < MINFF ) { logline(ACLOG_INFO, "MAP CHECK FAIL: The flags are too close to each other"); return false; } for (int i = 0; i < ms->hdr.numents; i++) { int v = ms->enttypes[i]; if (v < I_CLIPS || v > I_AKIMBO) continue; short *p = &ms->entposs[i*3]; float density = 0, hdensity = 0; for(int j = 0; j < ms->hdr.numents; j++) { int w = ms->enttypes[j]; if (w < I_CLIPS || w > I_AKIMBO || i == j) continue; short *q = &ms->entposs[j*3]; float r2 = 0; loopk(3){ r2 += (p[k]-q[k])*(p[k]-q[k]); } if ( r2 == 0.0f ) { logline(ACLOG_INFO, "MAP CHECK FAIL: Items too close %s %s (%hd,%hd)", entnames[v], entnames[w],p[0],p[1]); return false; } r2 = 1/r2; if (r2 < 0.0025f) continue; if (w != v) { hdensity += r2; continue; } density += r2; } /* if (hdensity > 0.0f) { logline(ACLOG_INFO, "ITEM CHECK H %s %f", entnames[v], hdensity); } if (density > 0.0f) { logline(ACLOG_INFO, "ITEM CHECK D %s %f", entnames[v], density); }*/ if ( hdensity > 0.5f ) { logline(ACLOG_INFO, "MAP CHECK FAIL: Items too close %s %.2f (%hd,%hd)", entnames[v],hdensity,p[0],p[1]); return false; } switch(v) { #define LOGTHISSWITCH(X) if( density > X ) { logline(ACLOG_INFO, "MAP CHECK FAIL: Items too close %s %.2f (%hd,%hd)", entnames[v],density,p[0],p[1]); return false; } case I_CLIPS: case I_HEALTH: LOGTHISSWITCH(0.24f); break; case I_AMMO: LOGTHISSWITCH(0.04f); break; case I_HELMET: LOGTHISSWITCH(0.02f); break; case I_ARMOUR: case I_GRENADE: case I_AKIMBO: LOGTHISSWITCH(0.005f); break; default: break; #undef LOGTHISSWITCH } } return true; } struct servermaprot : serverconfigfile { vector configsets; int curcfgset; servermaprot() : curcfgset(-1) {} void read() { if(getfilesize(filename) == filelen) return; configsets.shrink(0); if(!load()) return; const char *sep = ": "; configset c; int i, line = 0; char *b, *l, *p = buf; logline(ACLOG_VERBOSE,"reading map rotation '%s'", filename); while(p < buf + filelen) { l = p; p += strlen(p) + 1; line++; l = strtok_r(l, sep, &b); if(l) { copystring(c.mapname, behindpath(l)); for(i = 3; i < CONFIG_MAXPAR; i++) c.par[i] = 0; // default values for(i = 0; i < CONFIG_MAXPAR; i++) { if((l = strtok_r(NULL, sep, &b)) != NULL) c.par[i] = atoi(l); else break; } if(i > 2) { configsets.add(c); logline(ACLOG_VERBOSE," %s, %s, %d minutes, vote:%d, minplayer:%d, maxplayer:%d, skiplines:%d", c.mapname, modestr(c.mode, false), c.time, c.vote, c.minplayer, c.maxplayer, c.skiplines); } else logline(ACLOG_INFO," error in line %d, file %s", line, filename); } } DELETEA(buf); logline(ACLOG_INFO,"read %d map rotation entries from '%s'", configsets.length(), filename); return; } int 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 -1; } #endif if(configsets.empty()) fatal("maprot unavailable"); int n = numclients(); int csl = configsets.length(); int ccs = curcfgset; if(ccs >= 0 && ccs < csl) ccs += configsets[ccs].skiplines; configset *c = NULL; loopi(3 * csl + 1) { ccs++; if(ccs >= csl || ccs < 0) ccs = 0; c = &configsets[ccs]; if((n >= c->minplayer || i >= csl) && (!c->maxplayer || n <= c->maxplayer || i >= 2 * csl)) { mapstats *ms = NULL; if((ms = getservermapstats(c->mapname)) && mapisok(ms)) break; else logline(ACLOG_INFO, "maprot error: map '%s' %s", c->mapname, (ms ? "does not satisfy some basic requirements" : "not found")); } if(i >= 3 * csl) fatal("maprot unusable"); // not a single map in rotation can be found... } if(!nochange) { curcfgset = ccs; startgame(c->mapname, c->mode, c->time, notify); } return ccs; } void restart(bool notify = true) // restart current map { #ifndef STANDALONE if(!isdedicated) { startgame(getclientmap(), getclientmode(), -1, notify); return; } #endif startgame(smapname, smode, -1, notify); } configset *current() { return configsets.inrange(curcfgset) ? &configsets[curcfgset] : NULL; } configset *get(int ccs) { return configsets.inrange(ccs) ? &configsets[ccs] : NULL; } }; // serverblacklist.cfg struct serveripblacklist : serverconfigfile { vector ipranges; void read() { if(getfilesize(filename) == filelen) return; ipranges.shrink(0); if(!load()) return; iprange ir; int line = 0, errors = 0; char *l, *r, *p = buf; logline(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'; logline(ACLOG_INFO," error in line %d, file %s: ignored '%s'", line, filename, l); errors++; } } DELETEA(buf); ipranges.sort(cmpiprange); int orglength = ipranges.length(); 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) logline(ACLOG_VERBOSE," blacklist entry %s got dropped (double entry)", iprtoa(ipranges[i])); else logline(ACLOG_VERBOSE," blacklist entry %s got dropped (already covered by %s)", iprtoa(ipranges[i]), iprtoa(ipranges[i - 1])); ipranges.remove(i--); continue; } if(ipranges[i].lr <= ipranges[i - 1].ur) { logline(ACLOG_VERBOSE," blacklist entries %s and %s are joined due to overlap", iprtoa(ipranges[i - 1]), iprtoa(ipranges[i])); ipranges[i - 1].ur = ipranges[i].ur; ipranges.remove(i--); continue; } } loopv(ipranges) logline(ACLOG_VERBOSE," %s", iprtoa(ipranges[i])); logline(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 destroylists() { whitelistranges.setsize(0); enumeratek(whitelist, const char *, key, delete key); whitelist.clear(false); blfraglist.deletecontents(); blacklines.setsize(0); } void read() { if(getfilesize(filename) == filelen) return; destroylists(); if(!load()) return; const char *sep = " "; int line = 1, errors = 0; iprchain iprc; blackline bl; char *b, *l, *s, *r, *p = buf; logline(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 { logline(ACLOG_INFO," error in line %d, file %s: unknown keyword '%s'", line, filename, l); errors++; } if(s && s[strspn(s, " ")]) { logline(ACLOG_INFO," error in line %d, file %s: ignored '%s'", line, filename, s); errors++; } } line++; } DELETEA(buf); logline(ACLOG_VERBOSE," nickname whitelist (%d entries):", whitelist.numelems); string text; 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)); } logline(ACLOG_VERBOSE, " accept %s%s", key, text); }); logline(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]); } } logline(ACLOG_VERBOSE, " %2d block%s%s", blacklines[i].line, blacklines[i].ignorecase ? "i" : "", text); } logline(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 initlist() { 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 { strncpy(entries[num][0],c1,FORBIDDENSIZE); if ( n > 1 ) strncpy(entries[num][1],c2,FORBIDDENSIZE); else entries[num][1][0]='\0'; num++; } } void read() { if(getfilesize(filename) == filelen) return; initlist(); if(!load()) return; char *l, *p = buf; logline(ACLOG_VERBOSE,"reading forbidden list '%s'", filename); while(p < buf + filelen) { l = p; p += strlen(p) + 1; addentry(l); } DELETEA(buf); } 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 read() { if(getfilesize(filename) == filelen) return; adminpwds.shrink(staticpasses); if(!load()) return; pwddetail c; const char *sep = " "; int i, line = 1, par[ADMINPWD_MAXPAR]; char *b, *l, *p = buf; logline(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); logline(ACLOG_VERBOSE,"line%4d: %s %d", c.line, hiddenpwd(c.pwd), c.denyadmin ? 1 : 0); } } line++; } DELETEA(buf); logline(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 { struct serverinfotext { const char *type; char lang[3]; char *info; int lastcheck; }; vector serverinfotexts; const char *infobase, *motdbase; void init(const char *info, const char *motd) { infobase = info; motdbase = motd; } char *readinfofile(const char *fnbase, const char *lang) { defformatstring(fname)("%s_%s.txt", fnbase, lang); path(fname); int len, n; char *c, *b, *s, *t, *buf = loadfile(fname, &len); if(!buf) return NULL; char *nbuf = new char[len + 2]; for(t = nbuf, s = strtok_r(buf, "\n\r", &b); s; s = strtok_r(NULL, "\n\r", &b)) { c = strstr(s, "//"); if(c) *c = '\0'; // strip comments for(n = (int)strlen(s) - 1; n >= 0 && s[n] == ' '; n--) s[n] = '\0'; // strip trailing blanks filterrichtext(t, s + strspn(s, " "), MAXINFOLINELEN); // skip leading blanks n = (int)strlen(t); if(n) t += n + 1; } *t = '\0'; DELETEA(buf); if(!*nbuf) DELETEA(nbuf); logline(ACLOG_DEBUG,"read file \"%s\"", fname); return nbuf; } const char *getinfocache(const char *fnbase, const char *lang) { serverinfotext sn = { fnbase, { 0, 0, 0 }, NULL, 0} , *s = &sn; filterlang(sn.lang, lang); if(!sn.lang[0]) return NULL; loopv(serverinfotexts) { serverinfotext &si = serverinfotexts[i]; if(si.type == s->type && !strcmp(si.lang, s->lang)) { if(servmillis - si.lastcheck > (si.info ? 15 : 45) * 60 * 1000) { // re-read existing files after 15 minutes; search missing again after 45 minutes DELETEA(si.info); s = &si; } else return si.info; } } s->info = readinfofile(fnbase, lang); s->lastcheck = servmillis; if(s == &sn) serverinfotexts.add(*s); if(fnbase == motdbase && s->info) { char *c = s->info; while(*c) { c += strlen(c); if(c[1]) *c++ = '\n'; } if(strlen(s->info) > MAXSTRLEN) s->info[MAXSTRLEN] = '\0'; // keep MOTD at sane lengths } return s->info; } const char *getinfo(const char *lang) { return getinfocache(infobase, lang); } const char *getmotd(const char *lang) { const char *motd; if(*lang && (motd = getinfocache(motdbase, lang))) return motd; return getinfocache(motdbase, "en"); } };