diff --git a/config/menus_multiplayer.cfg b/config/menus_multiplayer.cfg index 5fcdf104..a6919624 100644 --- a/config/menus_multiplayer.cfg +++ b/config/menus_multiplayer.cfg @@ -140,9 +140,11 @@ menuinitselection 1 newmenu getmap menuitem " This server is using a different version of this map..." -1 menuitem "" -1 +menuitemvar "result $__getmaprevisions" +menuitem "" -1 menuitem " 2 [ Download the servers map version and join the game ]" "getmap" menuitem " 3 [ Keep the local map version and disconnect from the server ]" [ showmenu ""; disconnect ] -menuinitselection 2 +menuinitselection 4 newmenu voicecom 1 1 diff --git a/config/servercmdline.txt b/config/servercmdline.txt index 7c0a3f51..23a4b889 100644 --- a/config/servercmdline.txt +++ b/config/servercmdline.txt @@ -52,9 +52,10 @@ // default: fkBMasRCDE (lowercase: a user can vote for it; uppercase: admin role is required to vote) // -M // sendmap permission string (read docs/commandline.html) // c create new map (initial map upload) -// u update existing map +// u update existing map (same or newer revision) +// r revert existing map to older revision // d delete map (default: no one can) -// default: CU (lowercase: any user is allowed to do it; uppercase: admin role is required) +// default: CRU (lowercase: any user is allowed to do it; uppercase: admin role is required) // rarely used switches: diff --git a/docs/commandline.html b/docs/commandline.html index e7cab595..a583ddc9 100644 --- a/docs/commandline.html +++ b/docs/commandline.html @@ -488,14 +488,16 @@ c create new map (initial map upload) u - update existing map + update existing map (with same or newer revision) + r + revert existing map to older revision d delete map (default: no one can) For example: -MCuD enables updating maps for normal players but only admins can create or delete maps. The '-M' parameter can be used several times on the commandline. For example, '-MCb -Mu -MD' is equivalent to '-MCuD'. - The default settings are: "CU".
+ The default settings are: "CRU".
Note: Only maps in the 'incoming' folder are affected. Maps in 'official' and 'servermaps' are treated read only. diff --git a/source/src/client.cpp b/source/src/client.cpp index 3a726708..91dc20a5 100644 --- a/source/src/client.cpp +++ b/source/src/client.cpp @@ -480,9 +480,10 @@ void sendmap(char *mapname) { if(!*mapname) mapname = getclientmap(); if(securemapcheck(mapname)) return; + if(gamemode == GMODE_COOPEDIT && !strcmp(getclientmap(), mapname)) save_world(mapname); - int mapsize, cfgsize, cfgsizegz; - uchar *mapdata = readmap(path(mapname), &mapsize); + int mapsize, cfgsize, cfgsizegz, revision; + uchar *mapdata = readmap(path(mapname), &mapsize, &revision); if(!mapdata) return; uchar *cfgdata = readmcfggz(path(mapname), &cfgsize, &cfgsizegz); if(!cfgdata) { cfgsize = 0; cfgsizegz = 0; } @@ -495,6 +496,7 @@ void sendmap(char *mapname) putint(p, mapsize); putint(p, cfgsize); putint(p, cfgsizegz); + putint(p, revision); if(MAXMAPSENDSIZE - p.length() < mapsize + cfgsizegz || cfgsize > MAXCFGFILESIZE) { conoutf("map %s is too large to send", mapname); diff --git a/source/src/clients2c.cpp b/source/src/clients2c.cpp index 84c7e5b1..9b377d0c 100644 --- a/source/src/clients2c.cpp +++ b/source/src/clients2c.cpp @@ -23,9 +23,10 @@ void neterr(const char *s) disconnect(); } -VARP(autogetmap, 0, 1, 1); +VARP(autogetmap, 0, 1, 1); // only if the client doesn't have that map +VARP(autogetnewmaprevisions, 0, 1, 1); -void changemapserv(char *name, int mode, int download) // forced map change from the server +void changemapserv(char *name, int mode, int download, int revision) // forced map change from the server { gamemode = mode; if(m_demo) return; @@ -33,17 +34,25 @@ void changemapserv(char *name, int mode, int download) // forced map chan if(download > 0) { if(securemapcheck(name, false)) return; + bool revmatch = hdr.maprevision == revision || revision == 0; bool sizematch = maploaded == download || download < 10; - if(loaded && sizematch) return; - if(autogetmap) + if(loaded && sizematch && revmatch) return; + bool getnewrev = autogetnewmaprevisions && revision > hdr.maprevision; + if(autogetmap || getnewrev) { - if(!loaded) getmap(); // no need to ask - else showmenu("getmap"); + if(!loaded || getnewrev) getmap(); // no need to ask + else + { + s_sprintfd(msg)("map '%s' revision: local %d, provided by server %d", name, hdr.maprevision, revision); + alias("__getmaprevisions", msg); + showmenu("getmap"); + } } else { if(!loaded || download < 10) conoutf("\"getmap\" to download the current map from the server"); - else conoutf("\"getmap\" to download a different version of the current map from the server"); + else conoutf("\"getmap\" to download a %s version of the current map from the server", + revision == 0 ? "different" : (revision > hdr.maprevision ? "newer" : "older")); } } } @@ -268,7 +277,8 @@ void parsemessages(int cn, playerent *d, ucharbuf &p) getstring(text, p); int mode = getint(p); int downloadable = getint(p); - changemapserv(text, mode, downloadable); + int revision = getint(p); + changemapserv(text, mode, downloadable, revision); if(m_arena && joining>2) deathstate(player1); mapchanged = true; break; @@ -894,6 +904,7 @@ void receivefile(uchar *data, int len) int mapsize = getint(p); int cfgsize = getint(p); int cfgsizegz = getint(p); + int revision = getint(p); int size = mapsize + cfgsizegz; if(p.remaining() < size) { diff --git a/source/src/protos.h b/source/src/protos.h index 70107ada..c58267b2 100644 --- a/source/src/protos.h +++ b/source/src/protos.h @@ -328,7 +328,6 @@ extern void c2sinfo(playerent *d); extern void c2skeepalive(); extern void neterr(const char *s); extern int getclientnum(); -extern void changemapserv(char *name, int mode, bool download = false); extern void changeteam(int team, bool respawn = true); extern void getmap(); extern void newteam(char *name); @@ -567,7 +566,7 @@ extern void save_world(char *fname); extern bool load_world(char *mname); extern void writemap(char *name, int size, uchar *data); extern void writecfggz(char *name, int size, int sizegz, uchar *data); -extern uchar *readmap(char *name, int *size); +extern uchar *readmap(char *name, int *size, int *revision); extern uchar *readmcfggz(char *name, int *size, int *sizegz); // physics @@ -753,7 +752,7 @@ struct servercommandline const char *a = arg + 2 + strspn(arg + 2, " "); int ai = atoi(a); switch(arg[1]) - { // todo: egjlqAEGHIJKLOQTUYZ + { // todo: egjlqAEGHIJLOQTUYZ case 'u': uprate = ai; break; case 'f': if(ai > 0 && ai < 65536) serverport = ai; break; case 'i': ip = a; break; diff --git a/source/src/server.cpp b/source/src/server.cpp index e779f3cf..405f7b3c 100644 --- a/source/src/server.cpp +++ b/source/src/server.cpp @@ -33,6 +33,10 @@ servercontroller *svcctrl = NULL; struct log *logger = NULL; struct servercommandline scl; +string copyname; +int copysize, copymapsize, copycfgsize, copycfgsizegz, copyrevision; +uchar *copydata = NULL; + #define valid_flag(f) (f >= 0 && f < 2) #define SERVERMAP_PATH "packages/maps/servermaps/" @@ -2202,6 +2206,7 @@ void resetmap(const char *newname, int newmode, int newtime, bool notify) if(e.fitsmode(smode)) sents[i].spawned = true; } notgotitems = false; + copyrevision = ms->hdr.maprevision; } scores.setsize(0); ctfreset(); @@ -2482,10 +2487,6 @@ void sendwhois(int sender, int cn) // sending of maps between clients -string copyname; -int copysize, copymapsize, copycfgsize, copycfgsizegz; -uchar *copydata = NULL; - int mapavailable(const char *mapname) { return copydata && !strcmp(copyname, behindpath(mapname)) ? copymapsize : 0; } bool sendmapserv(int n, const char *mapname, int mapsize, int cfgsize, int cfgsizegz, uchar *data) @@ -2502,6 +2503,7 @@ bool sendmapserv(int n, const char *mapname, int mapsize, int cfgsize, int cfgsi copycfgsize = cfgsize; copycfgsizegz = cfgsizegz; copysize = mapsize + cfgsizegz; + copyrevision = 0; DELETEA(copydata); copydata = new uchar[copysize]; memcpy(copydata, data, copysize); @@ -2541,6 +2543,7 @@ ENetPacket *getmapserv(int n) putint(p, copymapsize); putint(p, copycfgsize); putint(p, copycfgsizegz); + putint(p, copyrevision); p.put(copydata, copysize); enet_packet_resize(packet, p.length()); return packet; @@ -2623,6 +2626,7 @@ void getservermap(void) copymapsize = cgzsize; copycfgsize = cfgsize; copycfgsizegz = cfgsizegz; + copyrevision = 0; copysize = cgzsize + cfgsizegz; DELETEA(copydata); copydata = new uchar[copysize]; @@ -2684,6 +2688,7 @@ void welcomepacket(ucharbuf &p, int n, ENetPacket *packet, bool forcedeath) sendstring(smapname, p); putint(p, smode); putint(p, mapavailable(smapname)); + putint(p, copyrevision); if(smode>1 || (smode==0 && numnonlocalclients()>0)) { putint(p, SV_TIMEUP); @@ -3287,6 +3292,7 @@ void process(ENetPacket *packet, int sender, int chan) // sender may be -1 int mapsize = getint(p); int cfgsize = getint(p); int cfgsizegz = getint(p); + int revision = getint(p); if(p.remaining() < mapsize + cfgsizegz) { p.forceoverread(); @@ -3304,18 +3310,23 @@ void process(ENetPacket *packet, int sender, int chan) // sender may be -1 reject = "no permission for initial upload"; sendservmsg("\f3initial map upload rejected: you need to be admin", sender); } - else if(mp == MAP_TEMP && !strchr(scl.mapperm, 'u') && cl->role < CR_ADMIN) // default: only admins can update maps + else if(mp == MAP_TEMP && revision >= copyrevision && !strchr(scl.mapperm, 'u') && cl->role < CR_ADMIN) // default: only admins can update maps { reject = "no permission to update"; sendservmsg("\f3map update rejected: you need to be admin", sender); } + else if(mp == MAP_TEMP && revision < copyrevision && !strchr(scl.mapperm, 'r') && cl->role < CR_ADMIN) // default: only admins can revert maps to older revisions + { + reject = "no permission to revert revision"; + sendservmsg("\f3map revert to older revision rejected: you need to be admin to upload an older map", sender); + } else { if(sendmapserv(sender, sentmap, mapsize, cfgsize, cfgsizegz, &p.buf[p.len])) { - logger->writeline(log::info,"[%s] %s sent map %s, %d + %d(%d) bytes written", - clients[sender]->hostname, clients[sender]->name, sentmap, mapsize, cfgsize, cfgsizegz); - s_sprintfd(msg)("%s (%d) up%sed map %s%s", clients[sender]->name, sender, mp == MAP_NOTFOUND ? "load": "dat", sentmap, + logger->writeline(log::info,"[%s] %s sent map %s, rev %d, %d + %d(%d) bytes written", + clients[sender]->hostname, clients[sender]->name, sentmap, revision, mapsize, cfgsize, cfgsizegz); + s_sprintfd(msg)("%s (%d) up%sed map %s, rev %d%s", clients[sender]->name, sender, mp == MAP_NOTFOUND ? "load": "dat", sentmap, revision, strcmp(sentmap, behindpath(smapname)) || smode == GMODE_COOPEDIT ? "" : " (restart game to use new map version)"); sendservmsg(msg); } @@ -3327,8 +3338,8 @@ void process(ENetPacket *packet, int sender, int chan) // sender may be -1 } if (reject) { - logger->writeline(log::info,"[%s] %s sent map %s, rejected: %s", - clients[sender]->hostname, clients[sender]->name, sentmap, reject); + logger->writeline(log::info,"[%s] %s sent map %s rev %d, rejected: %s", + clients[sender]->hostname, clients[sender]->name, sentmap, revision, reject); } p.len += mapsize + cfgsizegz; break; diff --git a/source/src/tools.cpp b/source/src/tools.cpp index 02eb2c51..2900d889 100644 --- a/source/src/tools.cpp +++ b/source/src/tools.cpp @@ -332,7 +332,12 @@ mapstats *loadmapstats(const char *filename, bool getlayout) if(gzread(f, &s.hdr, sizeof(header)-sizeof(int)*16)!=sizeof(header)-sizeof(int)*16 || (strncmp(s.hdr.head, "CUBE", 4) && strncmp(s.hdr.head, "ACMP",4))) { gzclose(f); return NULL; } endianswap(&s.hdr.version, sizeof(int), 4); if(s.hdr.version>MAPVERSION || s.hdr.numents > MAXENTITIES || (s.hdr.version>=4 && gzread(f, &s.hdr.waterlevel, sizeof(int)*16)!=sizeof(int)*16)) { gzclose(f); return NULL; } - if(s.hdr.version>=4) endianswap(&s.hdr.waterlevel, sizeof(int), 1); else s.hdr.waterlevel = -100000; + if(s.hdr.version>=4) + { + endianswap(&s.hdr.waterlevel, sizeof(int), 1); + endianswap(&s.hdr.maprevision, sizeof(int), 1); + } + else s.hdr.waterlevel = -100000; entity e; enttypes = new uchar[s.hdr.numents]; entposs = new short[s.hdr.numents * 3]; diff --git a/source/src/world.h b/source/src/world.h index 1d2d2ad8..64a67be3 100644 --- a/source/src/world.h +++ b/source/src/world.h @@ -45,7 +45,8 @@ struct header // map file format header uchar texlists[3][256]; int waterlevel; uchar watercolor[4]; - int reserved[14]; + int maprevision; + int reserved[13]; }; struct mapstats diff --git a/source/src/worldio.cpp b/source/src/worldio.cpp index f24bfc62..5b4da689 100644 --- a/source/src/worldio.cpp +++ b/source/src/worldio.cpp @@ -119,11 +119,13 @@ void writemap(char *name, int msize, uchar *mdata) conoutf("wrote map %s as file %s", name, cgzname); } -uchar *readmap(char *name, int *size) +uchar *readmap(char *name, int *size, int *revision) { setnames(name); uchar *data = (uchar *)loadfile(cgzname, size); if(!data) { conoutf("\f3could not read map %s", cgzname); return NULL; } + mapstats *ms = loadmapstats(cgzname, false); + if(revision) *revision = ms->hdr.maprevision; return data; } @@ -177,11 +179,12 @@ uchar *readmcfggz(char *name, int *size, int *sizegz) return gzbuf; } - // save map as .cgz file. uses 2 layers of compression: first does simple run-length // encoding and leaves out data for certain kinds of cubes, then zlib removes the // last bits of redundancy. Both passes contribute greatly to the miniscule map sizes. +VAR(advancemaprevision, 1, 1, 100); + void save_world(char *mname) { if(!*mname) mname = getclientmap(); @@ -203,6 +206,8 @@ void save_world(char *mname) header tmp = hdr; endianswap(&tmp.version, sizeof(int), 4); endianswap(&tmp.waterlevel, sizeof(int), 1); + tmp.maprevision += advancemaprevision; + endianswap(&tmp.maprevision, sizeof(int), 1); gzwrite(f, &tmp, sizeof(header)); int ne = hdr.numents; loopv(ents) @@ -271,10 +276,12 @@ void save_world(char *mname) extern void preparectf(bool cleanonly = false); int numspawn[3], maploaded = 0, numflagspawn[2]; +VAR(curmaprevision, 1, 0, 0); bool load_world(char *mname) // still supports all map formats that have existed since the earliest cube betas! { int loadmillis = SDL_GetTicks(); + advancemaprevision = 1; setnames(mname); maploaded = getfilesize(ocgzname); if(maploaded > 0) @@ -300,6 +307,8 @@ bool load_world(char *mname) // still supports all map formats that have { endianswap(&hdr.waterlevel, sizeof(int), 1); if(!hdr.watercolor[3]) setwatercolor(); + endianswap(&hdr.maprevision, sizeof(int), 1); + curmaprevision = hdr.maprevision; } else { @@ -416,7 +425,7 @@ bool load_world(char *mname) // still supports all map formats that have if(f) gzclose(f); c2skeepalive(); calclight(); - conoutf("read map %s (%d milliseconds)", cgzname, SDL_GetTicks()-loadmillis); + conoutf("read map %s rev %d (%d milliseconds)", cgzname, hdr.maprevision, SDL_GetTicks()-loadmillis); conoutf("%s", hdr.maptitle); pushscontext(IEXC_MAPCFG); // untrusted altogether @@ -447,3 +456,6 @@ bool load_world(char *mname) // still supports all map formats that have COMMANDN(savemap, save_world, ARG_1STR); +// FIXME - remove this before release +void setmaprevision(int rev) { hdr.maprevision = rev; } +COMMAND(setmaprevision, ARG_1INT);