Easier LAN coop play of classic Quake

Discuss programming topics for the various GPL'd game engine sources.
Baker
Posts: 3666
Joined: Tue Mar 14, 2006 5:15 am

Easier LAN coop play of classic Quake

Post by Baker »

I think wireless routers are pretty common, but networks not so common. I've noticed that Dropbox has "LAN" detection and will propogate file changes locally (nice!) which is very fast.

Any ideas of the best strategy to detect a possible Quake game (an open 26000) on a router?
The obvious thought that comes to mind is if the ip address is 192.168.x.y, start looking at 192.168.x.0 through x.20 and running a quick "test2" command on each of those. Or is there a better way?
To kill another annoyance, I plan to make a slightly altered protocol "670" (Fitz 666 plus 4), and if so, stick "game warp -quoth" or "game hipnotic -hipnotic" in a string and switch the gamedir. Clearly not literally like that. Probably serialize the gamedirs like "quoth;warp" and then another string with "-hipnotic" or "-rogue"
Sure, not a "master plan". Just an incremental one. A better plan would involve adding client/server ... map/model download too, but I'm more interested in the two modifications. A plus about the gamedir serialization is that it would make any demo recorded using "protocol 670" easier to play back as the client would have a better idea of what to expect for requirements.
I don't know if I'll do this soon ... then again the above annoys me greatly on a LAN playing to play coop.
The night is young. How else can I annoy the world before sunsrise? 8) Inquisitive minds want to know ! And if they don't -- well like that ever has stopped me before ..
Spike
Posts: 2914
Joined: Fri Nov 05, 2004 3:12 am
Location: UK
Contact:

Re: Easier LAN coop play of classic Quake

Post by Spike »

Make your network socket broadcast capable, send a CCREQ_SERVER_INFO message to 255.255.255.255:26000, and see who replies. No need to send to every node individually (routers and isps will automatically restrict broadcasts to be site-local).

Do you really need a new distinct protocol? Can you not hide the difference within a stringcmd? Possibly one with a leading // ?
Baker
Posts: 3666
Joined: Tue Mar 14, 2006 5:15 am

Re: Easier LAN coop play of classic Quake

Post by Baker »

Spike wrote:Make your network socket broadcast capable, send a CCREQ_SERVER_INFO message to 255.255.255.255:26000, and see who replies. No need to send to every node individually (routers and isps will automatically restrict broadcasts to be site-local).

Do you really need a new distinct protocol? Can you not hide the difference within a stringcmd? Possibly one with a leading // ?
No, I dislike every little adjustment requiring a new protocol ---but I'm a bit lacking in imagination with your suggestion. Are you recommending tossing this in one of the signon replies or in SV_SendServerinfo? The significance of the // is so it gets treated as a comment and doesn't say "invalid command" to a non-supported client?

You recommended this before with a conversation with MH, but I believe that was regarding worldspawn keys in maps.
The night is young. How else can I annoy the world before sunsrise? 8) Inquisitive minds want to know ! And if they don't -- well like that ever has stopped me before ..
Spike
Posts: 2914
Joined: Fri Nov 05, 2004 3:12 am
Location: UK
Contact:

Re: Easier LAN coop play of classic Quake

Post by Spike »

stuffcmd(self, "//gamedir \"MYMAGICGAMEDIR\"\n");

clients that understand '//gamedir' stuffcmds will change gamedir. clients that do not will silently discard it. its like a hidden svc.
so things won't break just because you added a hint.
The // is optional of course. omiting it means it'll work in existing clients that support a gamedir console command, but will be spammy. But hey, maybe that extra line of spam will give the motivation required for more engine authors to add support.
You'll of course need to ensure that its sent before the (nq) svc_serverinfo message, to ensure that its present before the precaches are seen.

If you want something more complex, you can try adding a handshake. You can mimic fte and do:
stuffcmd(self, "cmd pext\n");
and have the client translate that to:
localcmdf("cmd pext \"%#x\" \"%#x\" \"%#x\" \"%#x\"\n", e1_vendorid, e1_flags, e2_vendorid, e2_flags);
then have the serverdata mention the activated vendorid+flags in the serverdata just before the normal version (with the base protocol version acting as a terminator), so the client knows what's actually in use by the server too.
Which is how fte negotiates its protocol extensions when using NQ protocols.
If the client doesn't understand it, the server still sees a 'pext' clientcommand with no arguments, so no extensions, and if the client does support stuff, the server can read the argument pairs to see the vendor+flags info and pull out the things that it does know.
Yeah, okay, with this handshake demos will probably need a matching client for playback, but hey, you'd need that if the server was using some forced extension too.

worldspawn keys can be prefixed with a _ in order to silence them, a // prefix won't work there.
Baker
Posts: 3666
Joined: Tue Mar 14, 2006 5:15 am

Re: Easier LAN coop play of classic Quake

Post by Baker »

Tried out your trick. :D Now of course you know it worked too, otherwise you wouldn't have suggested it.

Very nice trickery there. With the "//" it doesn't even print bad command but CL_Parse sure can look through it.
The night is young. How else can I annoy the world before sunsrise? 8) Inquisitive minds want to know ! And if they don't -- well like that ever has stopped me before ..
Baker
Posts: 3666
Joined: Tue Mar 14, 2006 5:15 am

Re: Easier LAN coop play of classic Quake

Post by Baker »

Stockpiling for info purposes only (on LAN, I don't want clients to have to download stuff from internet, get from server).

But I'm not sure the "individual" file way that, say, Quakeworld does download is "right" for single player LAN games.

At least for single player coop, I have something else in mind, but what I have in mind doesn't even actually involve "gamedirs" as I'm sort of wondering if .zip files of a single player mod should ever even be unzipped at all.

I've seen Nintendo 64 emulators and such that don't seem to ever bother unzipping files. But I'm going to have to plan that out and that is likely months down the road ...

Code: Select all

//////////////////////////////////////////////////////
client.h / protocol.h
//////////////////////////////////////////////////////

#define	svc_download		41		// [short] size [size bytes]

typedef enum {
	dl_none,
	dl_model,
	dl_sound,
	dl_skin,
	dl_single
} dltype_t;		// download type

typedef struct
{
	...
	FILE		*download;		// file transfer from server
	char		downloadtempname[MAX_OSPATH];
	char		downloadname[MAX_OSPATH];
	int			downloadnumber;
	dltype_t	downloadtype;
	int			downloadpercent;
	...
} client_static_t;

extern client_static_t	cls;

qboolean	CL_CheckOrDownloadFile (char *filename);
qboolean CL_IsUploading(void);

Code: Select all

//////////////////////////////////////////////////////
server.h
//////////////////////////////////////////////////////

typedef struct client_s
{
	...
	FILE			*download;			// file being downloaded
	int				downloadsize;		// total bytes
	int				downloadcount;		// bytes sent
	...
} client_t;

Code: Select all

//////////////////////////////////////////////////////
cl_demo.c
//////////////////////////////////////////////////////

CL_Record_f

	// get the client to check and download skins
	// when that is completed, a begin command will be issued
	MSG_WriteByte (&buf, svc_stufftext);
	MSG_WriteString (&buf, va("skins\n") );

Code: Select all

//////////////////////////////////////////////////////
cl_main.c 
//////////////////////////////////////////////////////

/*
=====================
CL_Disconnect

Sends a disconnect message to the server
This is also called on Host_Error, so it shouldn't cause any errors
=====================
*/
void CL_Disconnect (void)
{
	byte	final[10];

	connect_time = -1;

#ifdef _WIN32
	SetWindowText (mainwindow, "QuakeWorld: disconnected");
#endif

// stop sounds (especially looping!)
	S_StopAllSounds (true);
	
// if running a local server, shut it down
	if (cls.demoplayback)
		CL_StopPlayback ();
	else if (cls.state != ca_disconnected)
	{
		if (cls.demorecording)
			CL_Stop_f ();

		final[0] = clc_stringcmd;
		strcpy (final+1, "drop");
		Netchan_Transmit (&cls.netchan, 6, final);
		Netchan_Transmit (&cls.netchan, 6, final);
		Netchan_Transmit (&cls.netchan, 6, final);

		cls.state = ca_disconnected;

		cls.demoplayback = cls.demorecording = cls.timedemo = false;
	}
	Cam_Reset();

	if (cls.download) {
		fclose(cls.download);
		cls.download = NULL;
	}

	CL_StopUpload();

}


/*
=================
CL_Changing_f

Just sent as a hint to the client that they should
drop to full console
=================
*/
void CL_Changing_f (void)
{
	if (cls.download)  // don't change when downloading
		return;

	S_StopAllSounds (true);
	cl.intermission = 0;
	cls.state = ca_connected;	// not active anymore, but not disconnected
	Con_Printf ("\nChanging map...\n");
}

/*
=================
CL_Reconnect_f

The server is changing levels
=================
*/
void CL_Reconnect_f (void)
{
	if (cls.download)  // don't change when downloading
		return;

	S_StopAllSounds (true);

	if (cls.state == ca_connected) {
		Con_Printf ("reconnecting...\n");
		MSG_WriteChar (&cls.netchan.message, clc_stringcmd);
		MSG_WriteString (&cls.netchan.message, "new");
		return;
	}

	if (!*cls.servername) {
		Con_Printf("No server to reconnect to...\n");
		return;
	}

	CL_Disconnect();
	CL_BeginServerConnect();
}

/*
=====================
CL_Download_f
=====================
*/
void CL_Download_f (void)
{
	char *p, *q;

	if (cls.state == ca_disconnected)
	{
		Con_Printf ("Must be connected.\n");
		return;
	}

	if (Cmd_Argc() != 2)
	{
		Con_Printf ("Usage: download <datafile>\n");
		return;
	}

	sprintf (cls.downloadname, "%s/%s", com_gamedir, Cmd_Argv(1));

	p = cls.downloadname;
	for (;;) {
		if ((q = strchr(p, '/')) != NULL) {
			*q = 0;
			Sys_mkdir(cls.downloadname);
			*q = '/';
			p = q + 1;
		} else
			break;
	}

	strcpy(cls.downloadtempname, cls.downloadname);
	cls.download = fopen (cls.downloadname, "wb");
	cls.downloadtype = dl_single;

	MSG_WriteByte (&cls.netchan.message, clc_stringcmd);
	SZ_Print (&cls.netchan.message, va("download %s\n",Cmd_Argv(1)));
}

CL_Init ...

	Cmd_AddCommand ("download", CL_Download_f);

Code: Select all

//////////////////////////////////////////////////////
cl_parse.c
//////////////////////////////////////////////////////

	"svc_download", // Baker:  41 I think ...
	
/*
===============
CL_CheckOrDownloadFile

Returns true if the file exists, otherwise it attempts
to start a download from the server.
===============
*/
qboolean	CL_CheckOrDownloadFile (char *filename)
{
	FILE	*f;

	if (strstr (filename, ".."))
	{
		Con_Printf ("Refusing to download a path with ..\n");
		return true;
	}

	COM_FOpenFile (filename, &f);
	if (f)
	{	// it exists, no need to download
		fclose (f);
		return true;
	}

	//ZOID - can't download when recording
	if (cls.demorecording) {
		Con_Printf("Unable to download %s in record mode.\n", cls.downloadname);
		return true;
	}
	//ZOID - can't download when playback
	if (cls.demoplayback)
		return true;

	strcpy (cls.downloadname, filename);
	Con_Printf ("Downloading %s...\n", cls.downloadname);

	// download to a temp name, and only rename
	// to the real name when done, so if interrupted
	// a runt file wont be left
	COM_StripExtension (cls.downloadname, cls.downloadtempname);
	strcat (cls.downloadtempname, ".tmp");

	MSG_WriteByte (&cls.netchan.message, clc_stringcmd);
	MSG_WriteString (&cls.netchan.message, va("download %s", cls.downloadname));

	cls.downloadnumber++;

	return false;
}

/*
=================
Model_NextDownload
=================
*/
void Model_NextDownload (void)
{
	char	*s;
	int		i;
	extern	char gamedirfile[];

	if (cls.downloadnumber == 0)
	{
		Con_Printf ("Checking models...\n");
		cls.downloadnumber = 1;
	}

	cls.downloadtype = dl_model;
	for ( 
		; cl.model_name[cls.downloadnumber][0]
		; cls.downloadnumber++)
	{
		s = cl.model_name[cls.downloadnumber];
		if (s[0] == '*')
			continue;	// inline brush model
		if (!CL_CheckOrDownloadFile(s))
			return;		// started a download
	}

	for (i=1 ; i<MAX_MODELS ; i++)
	{
		if (!cl.model_name[i][0])
			break;

		cl.model_precache[i] = Mod_ForName (cl.model_name[i], false);

		if (!cl.model_precache[i])
		{
			Con_Printf ("\nThe required model file '%s' could not be found or downloaded.\n\n"
				, cl.model_name[i]);
			Con_Printf ("You may need to download or purchase a %s client "
				"pack in order to play on this server.\n\n", gamedirfile);
			CL_Disconnect ();
			return;
		}
	}

	// all done
	cl.worldmodel = cl.model_precache[1];	
	R_NewMap ();
	Hunk_Check ();		// make sure nothing is hurt

	// done with modellist, request first of static signon messages
	MSG_WriteByte (&cls.netchan.message, clc_stringcmd);
//	MSG_WriteString (&cls.netchan.message, va("prespawn %i 0 %i", cl.servercount, cl.worldmodel->checksum2));
	MSG_WriteString (&cls.netchan.message, va(prespawn_name, cl.servercount, cl.worldmodel->checksum2));
}

/*
=================
Sound_NextDownload
=================
*/
void Sound_NextDownload (void)
{
	char	*s;
	int		i;

	if (cls.downloadnumber == 0)
	{
		Con_Printf ("Checking sounds...\n");
		cls.downloadnumber = 1;
	}

	cls.downloadtype = dl_sound;
	for ( 
		; cl.sound_name[cls.downloadnumber][0]
		; cls.downloadnumber++)
	{
		s = cl.sound_name[cls.downloadnumber];
		if (!CL_CheckOrDownloadFile(va("sound/%s",s)))
			return;		// started a download
	}

	for (i=1 ; i<MAX_SOUNDS ; i++)
	{
		if (!cl.sound_name[i][0])
			break;
		cl.sound_precache[i] = S_PrecacheSound (cl.sound_name[i]);
	}

	// done with sounds, request models now
	memset (cl.model_precache, 0, sizeof(cl.model_precache));
	cl_playerindex = -1;
	cl_spikeindex = -1;
	cl_flagindex = -1;
	MSG_WriteByte (&cls.netchan.message, clc_stringcmd);
//	MSG_WriteString (&cls.netchan.message, va("modellist %i 0", cl.servercount));
	MSG_WriteString (&cls.netchan.message, va(modellist_name, cl.servercount, 0));
}


/*
======================
CL_RequestNextDownload
======================
*/
void CL_RequestNextDownload (void)
{
	switch (cls.downloadtype)
	{
	case dl_single:
		break;
	case dl_skin:
		Skin_NextDownload ();
		break;
	case dl_model:
		Model_NextDownload ();
		break;
	case dl_sound:
		Sound_NextDownload ();
		break;
	case dl_none:
	default:
		Con_DPrintf("Unknown download type.\n");
	}
}

/*
=====================
CL_ParseDownload

A download message has been received from the server
=====================
*/
void CL_ParseDownload (void)
{
	int		size, percent;
	byte	name[1024];
	int		r;


	// read the data
	size = MSG_ReadShort ();
	percent = MSG_ReadByte ();

	if (cls.demoplayback) {
		if (size > 0)
			msg_readcount += size;
		return; // not in demo playback
	}

	if (size == -1)
	{
		Con_Printf ("File not found.\n");
		if (cls.download)
		{
			Con_Printf ("cls.download shouldn't have been set\n");
			fclose (cls.download);
			cls.download = NULL;
		}
		CL_RequestNextDownload ();
		return;
	}

	// open the file if not opened yet
	if (!cls.download)
	{
		if (strncmp(cls.downloadtempname,"skins/",6))
			sprintf (name, "%s/%s", com_gamedir, cls.downloadtempname);
		else
			sprintf (name, "qw/%s", cls.downloadtempname);

		COM_CreatePath (name);

		cls.download = fopen (name, "wb");
		if (!cls.download)
		{
			msg_readcount += size;
			Con_Printf ("Failed to open %s\n", cls.downloadtempname);
			CL_RequestNextDownload ();
			return;
		}
	}

	fwrite (net_message.data + msg_readcount, 1, size, cls.download);
	msg_readcount += size;

	if (percent != 100)
	{
// change display routines by zoid
		// request next block
#if 0
		Con_Printf (".");
		if (10*(percent/10) != cls.downloadpercent)
		{
			cls.downloadpercent = 10*(percent/10);
			Con_Printf ("%i%%", cls.downloadpercent);
		}
#endif
		cls.downloadpercent = percent;

		MSG_WriteByte (&cls.netchan.message, clc_stringcmd);
		SZ_Print (&cls.netchan.message, "nextdl");
	}
	else
	{
		char	oldn[MAX_OSPATH];
		char	newn[MAX_OSPATH];

#if 0
		Con_Printf ("100%%\n");
#endif

		fclose (cls.download);

		// rename the temp file to it's final name
		if (strcmp(cls.downloadtempname, cls.downloadname)) {
			if (strncmp(cls.downloadtempname,"skins/",6)) {
				sprintf (oldn, "%s/%s", com_gamedir, cls.downloadtempname);
				sprintf (newn, "%s/%s", com_gamedir, cls.downloadname);
			} else {
				sprintf (oldn, "qw/%s", cls.downloadtempname);
				sprintf (newn, "qw/%s", cls.downloadname);
			}
			r = rename (oldn, newn);
			if (r)
				Con_Printf ("failed to rename.\n");
		}

		cls.download = NULL;
		cls.downloadpercent = 0;

		// get another file if needed

		CL_RequestNextDownload ();
	}
}


CL_Parse_ServerData ...

	// ask for the sound list next
	memset(cl.sound_name, 0, sizeof(cl.sound_name));
	MSG_WriteByte (&cls.netchan.message, clc_stringcmd);
//	MSG_WriteString (&cls.netchan.message, va("soundlist %i 0", cl.servercount));
	MSG_WriteString (&cls.netchan.message, va(soundlist_name, cl.servercount, 0));

	// now waiting for downloads, etc
	cls.state = ca_onserver;
}


/*
==================
CL_ParseSoundlist
==================
*/
void CL_ParseSoundlist (void)
{
	int	numsounds;
	char	*str;
	int n;

// precache sounds
//	memset (cl.sound_precache, 0, sizeof(cl.sound_precache));

	numsounds = MSG_ReadByte();

	for (;;) {
		str = MSG_ReadString ();
		if (!str[0])
			break;
		numsounds++;
		if (numsounds == MAX_SOUNDS)
			Host_EndGame ("Server sent too many sound_precache");
		strcpy (cl.sound_name[numsounds], str);
	}

	n = MSG_ReadByte();

	if (n) {
		MSG_WriteByte (&cls.netchan.message, clc_stringcmd);
//		MSG_WriteString (&cls.netchan.message, va("soundlist %i %i", cl.servercount, n));
		MSG_WriteString (&cls.netchan.message, va(soundlist_name, cl.servercount, n));
		return;
	}

	cls.downloadnumber = 0;
	cls.downloadtype = dl_sound;
	Sound_NextDownload ();
}

/*
==================
CL_ParseModellist
==================
*/
void CL_ParseModellist (void)
{
	int	nummodels;
	char	*str;
	int n;

// precache models and note certain default indexes
	nummodels = MSG_ReadByte();

	for (;;)
	{
		str = MSG_ReadString ();
		if (!str[0])
			break;
		nummodels++;
		if (nummodels==MAX_MODELS)
			Host_EndGame ("Server sent too many model_precache");
		strcpy (cl.model_name[nummodels], str);

		if (!strcmp(cl.model_name[nummodels],"progs/spike.mdl"))
			cl_spikeindex = nummodels;
		if (!strcmp(cl.model_name[nummodels],"progs/player.mdl"))
			cl_playerindex = nummodels;
		if (!strcmp(cl.model_name[nummodels],"progs/flag.mdl"))
			cl_flagindex = nummodels;
	}

	n = MSG_ReadByte();

	if (n) {
		MSG_WriteByte (&cls.netchan.message, clc_stringcmd);
//		MSG_WriteString (&cls.netchan.message, va("modellist %i %i", cl.servercount, n));
		MSG_WriteString (&cls.netchan.message, va(modellist_name, cl.servercount, n));
		return;
	}

	cls.downloadnumber = 0;
	cls.downloadtype = dl_model;
	Model_NextDownload ();
}

CL_Parse_ServerMessage

		case svc_download:
			CL_ParseDownload ();
			break;

Code: Select all

//////////////////////////////////////////////////////
common.c
//////////////////////////////////////////////////////


/*
============
COM_CreatePath

Only used for CopyFile and download
============
*/
void	COM_CreatePath (char *path)
{
	char	*ofs;
	
	for (ofs = path+1 ; *ofs ; ofs++)
	{
		if (*ofs == '/')
		{	// create the directory
			*ofs = 0;
			Sys_mkdir (path);
			*ofs = '/';
		}
	}
}

Code: Select all

//////////////////////////////////////////////////////
console.c
//////////////////////////////////////////////////////

Draw Console ... (download progress)

	// draw the download bar
	// figure out width
	if (cls.download) {
		if ((text = strrchr(cls.downloadname, '/')) != NULL)
			text++;
		else
			text = cls.downloadname;

		x = con_linewidth - ((con_linewidth * 7) / 40);
		y = x - strlen(text) - 8;
		i = con_linewidth/3;
		if (strlen(text) > i) {
			y = x - i - 11;
			strncpy(dlbar, text, i);
			dlbar[i] = 0;
			strcat(dlbar, "...");
		} else
			strcpy(dlbar, text);
		strcat(dlbar, ": ");
		i = strlen(dlbar);
		dlbar[i++] = '\x80';
		// where's the dot go?
		if (cls.downloadpercent == 0)
			n = 0;
		else
			n = y * cls.downloadpercent / 100;
			
		for (j = 0; j < y; j++)
			if (j == n)
				dlbar[i++] = '\x83';
			else
				dlbar[i++] = '\x81';
		dlbar[i++] = '\x82';
		dlbar[i] = 0;

		sprintf(dlbar + strlen(dlbar), " %02d%%", cls.downloadpercent);

		// draw it
		y = con_vislines-22 + 8;
		for (i = 0; i < strlen(dlbar); i++)
			Draw_Character ( (i+1)<<3, y, dlbar[i]);
	}


// draw the input prompt, user text, and cursor if desired
	Con_DrawInput ();
}

Code: Select all

//////////////////////////////////////////////////////
draw.c / gl_draw.c
//////////////////////////////////////////////////////



	if (cls.download) {
		sprintf (ver, "%4.2f", VERSION);
		dest = conback->data + 320 + 320*186 - 11 - 8*strlen(ver);
	} else {

Code: Select all

//////////////////////////////////////////////////////
sv_main.c
//////////////////////////////////////////////////////


cvar_t	allow_download = {"allow_download", "1"};
cvar_t	allow_download_skins = {"allow_download_skins", "1"};
cvar_t	allow_download_models = {"allow_download_models", "1"};
cvar_t	allow_download_sounds = {"allow_download_sounds", "1"};
cvar_t	allow_download_maps = {"allow_download_maps", "1"};

	Cvar_RegisterVariable (&allow_download);
	Cvar_RegisterVariable (&allow_download_skins);
	Cvar_RegisterVariable (&allow_download_models);
	Cvar_RegisterVariable (&allow_download_sounds);
	Cvar_RegisterVariable (&allow_download_maps);


SV_DropClient ...

	if (drop->download)
	{
		fclose (drop->download);
		drop->download = NULL;
	}

Code: Select all

//////////////////////////////////////////////////////
sv_user,c
//////////////////////////////////////////////////////

	{"download", SV_BeginDownload_f},
	{"nextdl", SV_NextDownload_f},


/*
==================
SV_NextDownload_f
==================
*/
void SV_NextDownload_f (void)
{
	byte	buffer[1024];
	int		r;
	int		percent;
	int		size;

	if (!host_client->download)
		return;

	r = host_client->downloadsize - host_client->downloadcount;
	if (r > 768)
		r = 768;
	r = fread (buffer, 1, r, host_client->download);
	ClientReliableWrite_Begin (host_client, svc_download, 6+r);
	ClientReliableWrite_Short (host_client, r);

	host_client->downloadcount += r;
	size = host_client->downloadsize;
	if (!size)
		size = 1;
	percent = host_client->downloadcount*100/size;
	ClientReliableWrite_Byte (host_client, percent);
	ClientReliableWrite_SZ (host_client, buffer, r);

	if (host_client->downloadcount != host_client->downloadsize)
		return;

	fclose (host_client->download);
	host_client->download = NULL;

}

void OutofBandPrintf(netadr_t where, char *fmt, ...)
{
	va_list		argptr;
	char	send[1024];
	
	send[0] = 0xff;
	send[1] = 0xff;
	send[2] = 0xff;
	send[3] = 0xff;
	send[4] = A2C_PRINT;
	va_start (argptr, fmt);
	vsprintf (send+5, fmt, argptr);
	va_end (argptr);

	NET_SendPacket (strlen(send)+1, send, where);
}

/*
==================
SV_NextUpload
==================
*/
void SV_NextUpload (void)
{
	byte	buffer[1024];
	int		r;
	int		percent;
	int		size;
	client_t *client;

	if (!*host_client->uploadfn) {
		SV_ClientPrintf(host_client, PRINT_HIGH, "Upload denied\n");
		ClientReliableWrite_Begin (host_client, svc_stufftext, 8);
		ClientReliableWrite_String (host_client, "stopul");

		// suck out rest of packet
		size = MSG_ReadShort ();	MSG_ReadByte ();
		msg_readcount += size;
		return;
	}

	size = MSG_ReadShort ();
	percent = MSG_ReadByte ();

	if (!host_client->upload)
	{
		host_client->upload = fopen(host_client->uploadfn, "wb");
		if (!host_client->upload) {
			Sys_Printf("Can't create %s\n", host_client->uploadfn);
			ClientReliableWrite_Begin (host_client, svc_stufftext, 8);
			ClientReliableWrite_String (host_client, "stopul");
			*host_client->uploadfn = 0;
			return;
		}
		Sys_Printf("Receiving %s from %d...\n", host_client->uploadfn, host_client->userid);
		if (host_client->remote_snap)
			OutofBandPrintf(host_client->snap_from, "Server receiving %s from %d...\n", host_client->uploadfn, host_client->userid);
	}

	fwrite (net_message.data + msg_readcount, 1, size, host_client->upload);
	msg_readcount += size;

Con_DPrintf ("UPLOAD: %d received\n", size);

	if (percent != 100) {
		ClientReliableWrite_Begin (host_client, svc_stufftext, 8);
		ClientReliableWrite_String (host_client, "nextul\n");
	} else {
		fclose (host_client->upload);
		host_client->upload = NULL;

		Sys_Printf("%s upload completed.\n", host_client->uploadfn);

		if (host_client->remote_snap) {
			char *p;

			if ((p = strchr(host_client->uploadfn, '/')) != NULL)
				p++;
			else
				p = host_client->uploadfn;
			OutofBandPrintf(host_client->snap_from, "%s upload completed.\nTo download, enter:\ndownload %s\n", 
				host_client->uploadfn, p);
		}
	}

}

/*
==================
SV_BeginDownload_f
==================
*/
void SV_BeginDownload_f(void)
{
	char	*name;
	extern	cvar_t	allow_download;
	extern	cvar_t	allow_download_skins;
	extern	cvar_t	allow_download_models;
	extern	cvar_t	allow_download_sounds;
	extern	cvar_t	allow_download_maps;
	extern	int		file_from_pak; // ZOID did file come from pak?

	name = Cmd_Argv(1);
// hacked by zoid to allow more conrol over download
		// first off, no .. or global allow check
	if (strstr (name, "..") || !allow_download.value
		// leading dot is no good
		|| *name == '.' 
		// leading slash bad as well, must be in subdir
		|| *name == '/'
		// next up, skin check
		|| (strncmp(name, "skins/", 6) == 0 && !allow_download_skins.value)
		// now models
		|| (strncmp(name, "progs/", 6) == 0 && !allow_download_models.value)
		// now sounds
		|| (strncmp(name, "sound/", 6) == 0 && !allow_download_sounds.value)
		// now maps (note special case for maps, must not be in pak)
		|| (strncmp(name, "maps/", 6) == 0 && !allow_download_maps.value)
		// MUST be in a subdirectory	
		|| !strstr (name, "/") )	
	{	// don't allow anything with .. path
		ClientReliableWrite_Begin (host_client, svc_download, 4);
		ClientReliableWrite_Short (host_client, -1);
		ClientReliableWrite_Byte (host_client, 0);
		return;
	}

	if (host_client->download) {
		fclose (host_client->download);
		host_client->download = NULL;
	}

	// lowercase name (needed for casesen file systems)
	{
		char *p;

		for (p = name; *p; p++)
			*p = (char)tolower(*p);
	}


	host_client->downloadsize = COM_FOpenFile (name, &host_client->download);
	host_client->downloadcount = 0;

	if (!host_client->download
		// special check for maps, if it came from a pak file, don't allow
		// download  ZOID
		|| (strncmp(name, "maps/", 5) == 0 && file_from_pak))
	{
		if (host_client->download) {
			fclose(host_client->download);
			host_client->download = NULL;
		}

		Sys_Printf ("Couldn't download %s to %s\n", name, host_client->name);
		ClientReliableWrite_Begin (host_client, svc_download, 4);
		ClientReliableWrite_Short (host_client, -1);
		ClientReliableWrite_Byte (host_client, 0);
		return;
	}

	SV_NextDownload_f ();
	Sys_Printf ("Downloading %s to %s\n", name, host_client->name);
}
The night is young. How else can I annoy the world before sunsrise? 8) Inquisitive minds want to know ! And if they don't -- well like that ever has stopped me before ..
Spike
Posts: 2914
Joined: Fri Nov 05, 2004 3:12 am
Location: UK
Contact:

Re: Easier LAN coop play of classic Quake

Post by Spike »

copy-pastaing from quakeworld?
things to look out for:
nextdl is reliable and syncronous. this requires round-trips.
dltype_t is a little bit evil.
dp's download protocol might be slightly more supported amongst NQ clients, and is indeed detected using some stufftext hack.

package-at-a-time is generally prefered over file-at-a-time, yes. You probably don't want the engine to recognise .zip files, only .pak and .pk3 files.
note that if you connect to an fte qw server, the server will send a few package names and hashes. the client can then download/load the named packages. because the hashes are known to the client, the client can avoid infecting games when playing on other servers, plus has a mechanism to cope with serverside mod upgrades, but you do have to write code to cope with paks potentially being loaded/purged between maps.

of course, many mods simply don't provide paks, or engines already downloaded a file-at-a-time, so you typically won't really get a choice.
Baker
Posts: 3666
Joined: Tue Mar 14, 2006 5:15 am

Re: Easier LAN coop play of classic Quake

Post by Baker »

Spike wrote:copy-pastaing from quakeworld?
I'm posted in the code so I could mentally make a note of the different oddball conditions in the source like can't download while recording demo, clear downloads when X, Y or Z happen, etc.

things to look out for: ..
Spike wrote:nextdl is reliable and syncronous. this requires round-trips.
Noticed that.
Spike wrote:dp's download protocol might be slightly more supported amongst NQ clients, and is indeed detected using some stufftext hack.
And DarkPlaces download, you can play while the models/etc are downloading.
You probably don't want the engine to recognise .zip files
I don't plan on doing that per se. Yet if I have 50 zip files of single player releases sitting around that get very occasional usage individually, I'm not sure they need to be eating up hard drive space.

And it makes them less transportable. Plus if I don't unzip them, I don't need to zip them for LAN propagation. I'm somewhat thinking of a more orderly Quake folder.
note that if you connect to an fte qw server, the server will send a few package names and hashes. the client can then download/load the named packages. because the hashes are known to the client, the client can avoid infecting games when playing on other servers
Exactly. Actually the "hashes" angle is something I need to factor in here a little. (i.e. somehow a .zip file is infected by a creative virus and LAN propogation provides an "assist" to spreading it.).

Fast LAN propogation is important to me. Some of these single player releases can require essentially 100 MB of zipped stuff (80 MB of Quoth files, then the 40MB mod itself) or Travail is about 100 MB. Over wireless, I'm not up to the latest top speed, but I think 1MB per second locally is about as fast as you can get.

A physical ethernet cable obviously increases the speed quite a bit (1 gigabit = 100 MB per second +/-)
The night is young. How else can I annoy the world before sunsrise? 8) Inquisitive minds want to know ! And if they don't -- well like that ever has stopped me before ..
Spike
Posts: 2914
Joined: Fri Nov 05, 2004 3:12 am
Location: UK
Contact:

Re: Easier LAN coop play of classic Quake

Post by Spike »

if you want speed, the easiest way is to just make the server act as an http server too.
quakeworld's downloads are syncronous, so slow.
dp downloads are pretty fast, but if you get packetloss it'll reset to an earlier point with lots of wasted bandwidth (mitigated by conservative rate settings, this isn't a real issue, but those rate settings will slow things down).
fte's downloads also lack a slow-start bandwidth estimation (this is more a client-side omission rather than a protocol issue). It doesn't care too much about packetloss, but can easily ping you off the net if your (d)rate is too high.
Set up a basic http 1.0 server, get the client to download from that. No demo bloat, proper flow control, fork a thread on your data connection (once the file has been opened on the main thread) and it'll just fly over the (inter?)net with no framerate/quake limitations. But does unfortunatly require another port to be opened, and is thus more likely to be unusable than a udp-based download. Your choice really.
Baker
Posts: 3666
Joined: Tue Mar 14, 2006 5:15 am

Re: Easier LAN coop play of classic Quake

Post by Baker »

Spike wrote:if you want speed, the easiest way is to just make the server act as an http server too.
quakeworld's downloads are syncronous, so slow.
dp downloads are pretty fast, but if you get packetloss it'll reset to an earlier point with lots of wasted bandwidth (mitigated by conservative rate settings, this isn't a real issue, but those rate settings will slow things down).
fte's downloads also lack a slow-start bandwidth estimation (this is more a client-side omission rather than a protocol issue). It doesn't care too much about packetloss, but can easily ping you off the net if your (d)rate is too high.
Set up a basic http 1.0 server, get the client to download from that. No demo bloat, proper flow control, fork a thread on your data connection (once the file has been opened on the main thread) and it'll just fly over the (inter?)net with no framerate/quake limitations. But does unfortunatly require another port to be opened, and is thus more likely to be unusable than a udp-based download. Your choice really.
I'd rather work up the ladder in natural progression. I'm eyeing FTE's chunked downloads. I'm far better at working with the client/server than in the past, but I want to get more of a handle on this aspect so eventually I can upgrade "NetQuake" to use connectionless connections, ip6, sv_heartbeat and co.

Note to self: About ready to test the gamedir modification and already found a future "todo". Add capability for server to live switch gamedir with a map change somehow ("changelevel start game warp -quoth" or something?) for a seemless server-side gamedir switch both on server and client. NOTE: This would have to stop any client demo in progress. Or would it? No ... a multigame demo would be annoying as hell ... then again I guess a multimap demo might be missing a map.

Does FTE exclude file downloads from demos? Or does FTE prevent downloads during demo recording like original Quakeworld? Just wondering ...
The night is young. How else can I annoy the world before sunsrise? 8) Inquisitive minds want to know ! And if they don't -- well like that ever has stopped me before ..
frag.machine
Posts: 2126
Joined: Sat Nov 25, 2006 1:49 pm

Re: Easier LAN coop play of classic Quake

Post by frag.machine »

Spike wrote:if you want speed, the easiest way is to just make the server act as an http server too.
OR, you will be more pragmatic and set a low footprint HTTP server to handle the downloads in a much nicer, efficient way. ;)
Just because you can throw the kitchen sink into the engine, it doesn't mean you should.
I know FrikaC made a cgi-bin version of the quakec interpreter once and wrote part of his website in QuakeC :) (LordHavoc)
Baker
Posts: 3666
Joined: Tue Mar 14, 2006 5:15 am

Re: Easier LAN coop play of classic Quake

Post by Baker »

frag.machine wrote:
Spike wrote:if you want speed, the easiest way is to just make the server act as an http server too.
OR, you will be more pragmatic and set a low footprint HTTP server to handle the downloads in a much nicer, efficient way. ;)
Just because you can throw the kitchen sink into the engine, it doesn't mean you should.
I'm listening and keeping an open mind ...
The night is young. How else can I annoy the world before sunsrise? 8) Inquisitive minds want to know ! And if they don't -- well like that ever has stopped me before ..
Spike
Posts: 2914
Joined: Fri Nov 05, 2004 3:12 am
Location: UK
Contact:

Re: Easier LAN coop play of classic Quake

Post by Spike »

a basic http server is not fantastically hard.
create a listening socket. bind it to a port. call listen.
periodically call accept on it to see if anyone tried to connect. this gives you a new socket.
wait for the client to send 'GET /resource HTTP/1.X\n\n'. ignore any extra lines between the 1 and the new line.
open the file. create a new thread. send "HTTP/1.0 200 OK\nContent-Length: %i\nConnection: close\n\n$FILE" close the socket, close the file, terminate the thread.

connection: close is optional. without it the client is free to send a second request after the first, which while otherwise desirable, results in sync requirements with the main thread.
add Content-Encoding: gzip if the contents are gzipped, and the receiver is meant to un-gzip it before use.
new lines (in http's headers) should ALWAYS be \r\n. I'm lazy in my descriptions. Be prepared to accept just \n too, but don't generate it. an empty line demotes the end of the http 1 headers.

or just grab fte's httpserver.c file (with curl or fte's httpclient.c file for the clientside part).
by all means use some other server, or directly use a simple tcp connection with no http semantics (ie: origional http...) but I do recommend making sure the server parts are automagic, and have the same file/copyright access restrictions as other quake servers (ie: no access to pak*.pak or its (map?) contents, and no access to root directory or configs directory, etc). in-process is imho the easiest to configure.

if you really want to get creative, look out for websocket connections too, and allow people to actually play via them. :)
Baker
Posts: 3666
Joined: Tue Mar 14, 2006 5:15 am

Re: Easier LAN coop play of classic Quake

Post by Baker »

Spike wrote:I'm lazy in my descriptions.
Usually a couple of vague descriptions [sometimes a bit of criticism seems to help too] and a hint or 2 like above sends me on my way.

Go figure ... I guess I get curious and start conducting obsessive experiments after resisting ideas initially ... we'll see what comes from this ...
The night is young. How else can I annoy the world before sunsrise? 8) Inquisitive minds want to know ! And if they don't -- well like that ever has stopped me before ..
Baker
Posts: 3666
Joined: Tue Mar 14, 2006 5:15 am

Re: Easier LAN coop play of classic Quake

Post by Baker »

Dissecting tiny httpd and friends (mini web)...
The night is young. How else can I annoy the world before sunsrise? 8) Inquisitive minds want to know ! And if they don't -- well like that ever has stopped me before ..
Post Reply