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);