Page 1 of 2

how to use <insert here> in CSQC

Posted: Fri Jan 23, 2015 12:22 pm
by gnounc
Theres been a fair amount of "how do I USE this function" going on lately, and I thought this thread would be a great way to clear some of that up!

So if you use CSQC and find yourself having to ask how to use a function because you cant find any doxx, run back here and drop off
what you learn!

To start things off shpuld and I will cover the most used basic functions, from there I hope this thread becomes the place to go for a real quick
"how do I?"

NOTE: for anyone posting, I'd love it if you guys threw the functions prototype up in a codeblock up top so that the readers can instantly see what arguments it takes.
thx :)

Re: how to use <insert here> in CSQC

Posted: Fri Jan 23, 2015 12:23 pm
by gnounc
First things first.

Code: Select all

	noref void(float vwidth, float vheight, float notmenu) CSQC_UpdateView;	/* Called every single video frame. The CSQC is responsible for rendering the entire screen. */

If you're going to be using csqc, you're probably going to be doing a bit of 2d drawing.
For that you will want to set up your screen size, and probably screen center globals.

In fte and dp both, you'll want to do this in CSQC_UpdateView();

In fte its as simple as declaring a global screen_size in your defs,
and feeding it the parameters of CSQC_UpdateView vwidth and vheight

screen_center then of course, would be half of that.
or

Code: Select all

	screen_center = [screen_size_x/2, screen_size_y/2];
Darkplaces is slightly different. The parameters to CSQC_UpdateView in Darkplaces
give you virtual pixels. If you try to position things the way you would in fte using the method above, things
will look wonky.

Instead we use cvars.

Code: Select all

	screen_size_x = cvar("vid_conwidth");
	screen_size_y = cvar("vid_conheight");
now you can

Code: Select all

	drawstring(vector position, string text, vector size, vector rgb, float alpha, float drawflag);
	drawpic(vector position, string pic, vector size, vector rgb, float alpha, optional float drawflag);
and

Code: Select all

	drawfill(vector position, vector size, vector rgb, float alpha, optional float drawflag);
to the topleft corner with position vector '0 0'
or to the bottomleft corner with screen_size. (remembering of course to subtract the size of the image from its position,
or it will be drawn offscreen)

Re: how to use <insert here> in CSQC

Posted: Fri Jan 23, 2015 1:14 pm
by Shpuld
First I'd like to point out that the stuff I'm gonna write is FTEQW ONLY. Darkplaces doesn't have multicasts or sendevents, in DP you can hack temp entities and sv_parseclientcommand.

Your game/mod is most likely going to need the client and server parts to work together, this requires information exchange between those 2, which in our case are of course CSQC and SSQC.
A lot of this stuff happens automatically in the engine, for example calling 'AddEntities' will make the client know about the entities that are sent by the server in the existing protocol already.

Now let's say you want to do some client<->server communication manually. CSQC provides good ways to do this.
For server -> client I generally like to use a multicast in SSQC and then handle it client-side in CSQC_Parse_Event. You get to decide exactly what you send and how it's interpreted. Very nice.

Example: We want to know client-side when we touch a thing serverside and we also want to get the things classname and origin for some reason.

In SSQC we would have a .touch function for the entity that looks like this:

Code: Select all

const float EVENT_THINGTOUCH = 123; //you probably wanna keep lines like this in defs or something

void() thing_touch =
{
	if(other.classname != "player")
		return;

	msg_entity = other; //make the client who touches this thing to be the receiver
	WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET); //when sending a SVC_CGAMEPACKET we can handle it clientside, note: it has to be a MSG_MULTICAST.
	WriteByte(MSG_MULTICAST, EVENT_THINGTOUCH); //it's smart to use the first byte of the actual message as an identifier, and it's also smart to name them.
	WriteString(MSG_MULTICAST, self.classname);
	WriteCoord(MSG_MULTICAST, self.origin_x); //sending vectors unfortunately requires 3 lines instead of a handy WriteVector
	WriteCoord(MSG_MULTICAST, self.origin_y);
	WriteCoord(MSG_MULTICAST, self.origin_z);
	multicast('0 0 0', MULTICAST_ONE); //send the message only to the dude that touched it, there's plenty of other ways to send these messages
}
I'd like to elaborate a bit on the last line before moving on to the client-side part. The first argument is a position where the message comes from, which is not useful when doing a direct MULTICAST_ONE to a single client, but it's crucial when you wanna do stuff like sending the message to all clients in the same Potentially Visible Set (PVS) as our self.origin for example.
Here are all the MULTICAST_* options and short documentation by Spike himself:

Code: Select all

const float MULTICAST_ALL = 0;	/* The multicast message is unreliably sent to all players. MULTICAST_ constants are valid arguments for the multicast builtin, which ignores the specified origin when given this constant. */
const float MULTICAST_PHS = 1;	/* The multicast message is unreliably sent to only players that can potentially hear the specified origin. Its quite loose. */
const float MULTICAST_PVS = 2;	/* The multicast message is unreliably sent to only players that can potentially see the specified origin. */
const float MULTICAST_ONE = 6;	/* The multicast message is unreliably sent to the player specified in the msg_entity global. The specified origin is ignored. */
const float MULTICAST_ALL_R = 3;	/* The multicast message is reliably sent to all players. The specified origin is ignored. */
const float MULTICAST_PHS_R = 4;	/* The multicast message is reliably sent to only players that can potentially hear the specified origin. Players might still not receive it if they are out of range. */
const float MULTICAST_PVS_R = 5;	/* The multicast message is reliably sent to only players that can potentially see the specified origin. Players might still not receive it if they cannot see the event. */
const float MULTICAST_ONE_R = 7;	/* The multicast message is reliably sent to the player specified in the msg_entity global. The specified origin is ignored */

Now let's get to the client-side handling for the event, it happens in CSQC_Parse_Event, a built-in entry point.

Code: Select all

const float EVENT_THINGTOUCH = 123; //make sure this matches between client and server

//function is called whenever the client receives a SVC_CGAMEPACKET
noref void() CSQC_Parse_Event =
{
	local float first = readbyte(); //we read the first byte after the initial SVC_CGAMEPACKET, in our case it's 123, or EVENT_THINGTOUCH
	if(first == EVENT_THINGTOUCH)
	{
		local vector pos;
		local string thing_msg;
		//we proceed to read all the information sent server-side, make sure you do it in the same order as it was sent 
		//IMPORTANT: make sure you read EVERYTHING you sent in the packet
		//if you send more stuff than you read here, the engine is going to crash, which is not the preferred outcome usually
		//in case you have a problem with this and need to find it, you can use sv_csqcdebug to help debugging

		thing_msg = readstring();
		pos_x = readcoord();
		pos_y = readcoord();
		pos_z = readcoord();

		print(strcat("You touched a thing that says: ", thing_msg, " and is located at " vtos(pos) ". \n"));
	}
	else if(first == EVENT_SOMETHINGELSE)
	{
		...
	}
}
Now this example isn't very useful but I hope you get the idea.

In the next post I'll tell how to tell the server to do things from the client using sendevent.

Re: how to use <insert here> in CSQC

Posted: Fri Jan 23, 2015 1:43 pm
by Shpuld
Let's go through one way of doing client -> server communication.
Like mentioned in the last post I'll go through sendevent:

Code: Select all

void(string evname, string evargs, ...) sendevent = #359; /*
		Invoke Cmd_evname_evargs in ssqc. evargs must be a string of initials refering to the types of the arguments to pass. v=vector, e=entity(.entnum field is sent), f=float, i=int. 6 arguments max - you can get more if you pack your floats into vectors. */
So basically with this we can call our custom functions in SSQC from CSQC.

Example time: let's say we have Deus Ex -style numpads in our game, touching/using them opens a nice CSQC based numpad GUI on the screen, we input the code and want to let the server know what kind of bollocks we typed in.

CSQC:

Code: Select all

//let's imagine this function gets called when clicks on the "Enter" button on the UI
void() numpad_send =
{
	local string code; //we somehow set this to what the user has inputted, not the focus of this post
...
	sendevent("checkcode", "s", code); 	
	//we're sending only one arg here, if you want to send more and different types it would work like this:
	//sendevent("checkcode", "svf", code, somevector, somefloat);
	//of course this will call a different function in server-side because of the different inputs
}
Now let's get into the SSQC:

Code: Select all

//the name of this function is important, it has to match with the stuff we've done in CSQC, if we had the previous example with more arguments, it would have to look like this
//void(string code, vector somevector, float somefloat) Cmd_checkcode_svf =
//and this of course is a different function already

//self will also be the client that sent the event, so we can easily give him feedback here
void(string code) Cmd_checkcode_s = 
{
	if(code != "1337")
		sprint(self, PRINT_HIGH, "Wrong code you dumbo!\n");
	else
	{
		sprint(self, PRINT_HIGH, "You are truly one with the 1337 h4x0rZ\n");
	}
}

Quite simple, isn't it? Now you should have a good knowledge of how to do some basic information exchange between client and server with the UNLIMITED POWER of CSQC.

Re: how to use <insert here> in CSQC

Posted: Sat Jan 24, 2015 11:23 am
by gnounc
Now on to some basic string handling.
strings are important for a variety of reasons, one of the most obvious, is constructing and deconstructing filepaths

most recently i've been using a custom save stuffcmd, adding things like the players name, mapname, kills and secrets right into the save filename
so i can parse it out later.

to concatenate strings together, we use strcat(); strcat takes up to 8 arguments, but all it does is smoosh the strings together.
on the ssqc side, ive been doing something like

Code: Select all

	path = strcat(name, "\", mapname, "\", kills, "-", secrets);
to construct the save paths in the folders gnounc\e1m1 for example
with a file name of 5-4.sav

then I use tokenizebystring(); to get the information back out in csqc like so

Code: Select all

	idx = tokenizebystring(path, "\", "-");
I believe tokenize does the same job, with all the obvious delimiters assumed, so use tokenize by string if you DONT want to split by \ or - or .
tokenize and tokenizebystring both return the number of items the string was split into
and populate argv();

Code: Select all

	name = argv(0);
	mapname = argv(1);
	etc...
or of course, you could just use a for loop, and the idx that was returned by tokenize

Code: Select all

	for(x = 0; x < idx - 1; x++)
	{
		print(argv(idx);
	}
really quickly, a few other useful string functions are

Code: Select all

	strlen(string s); which returns the number of characters in the string,
really useful for positioning string elements on screen.

Code: Select all

	float stringwidth(string text, float allowColorCodes, vector size) = #327; // get a width of string with given font and char size
returns the size the string will appear on screen..horizontally obv

and lastly, if your temporary strings are getting garbled, being blank, or anything like that try strzone()
it returns a string that is protected from being freed.
which of course means you'll need to free it yourself when you're finished with ti

Code: Select all

	stringVar = strzone("i'd like to preserve this string so i can use it in five minutes please");
and

Code: Select all

	strunzone(stringVar);


addendum: apparently tokenizebystring is called

Code: Select all

	float(string s, string separator1, ...) tokenizebyseparator = #479;
in darkplaces.

..more to come

Re: how to use <insert here> in CSQC

Posted: Sat Jan 24, 2015 12:15 pm
by Shpuld
If you want to do manipulation in CSQC for certain entities that share a model, for example players, you can use deltalisten().

Code: Select all

float(string modelname, float(float isnew) updatecallback, float flags) deltalisten = #371; /*
		Specifies a per-modelindex callback to listen for engine-networking entity updates. Such entities are automatically interpolated by the engine (unless flags specifies not to).
		The various standard entity fields will be overwritten each frame before the updatecallback function is called. */
I'll be using an example where we scale players up and down in CSQC.
EDIT: originally I said call deltalisten every frame, which is not required.
We call the deltalisten() in CSQC_Init, and for every entity with the model "progs/player.mdl" the specified listen function will be called every frame.

Code: Select all

noref void(float apiver, string enginename, float enginever) CSQC_Init =
{
	...
	deltalisten("progs/player.mdl", our_custom_listen, 0);
	...
}
Now we need to define our_custom_listen. I just put it above the CSQC_UpdateView function.

Code: Select all

float(float isnew) our_custom_listen =
{
	//this prints when you "see" an ent for the first time, or when it leaves PVS and enters it again.
	if(isnew)
		print("I am new\n");

	self.scale = 1 + (0.5 * sin(time * 4));
	return 1;
};
Note, you want to make sure you have CSQC_Ent_Remove defined, you're gonna get null function calls otherwise whenever the object is removed from client, like when leaving PVS.
You probably want it to look like this for most cases:

Code: Select all

void() CSQC_Ent_Remove =
{
	remove(self);
};

Re: how to use <insert here> in CSQC

Posted: Sun Jan 25, 2015 9:37 am
by gnounc
Now it's time to talk about one of the two functions that inspired this thread.
I found

Code: Select all

	void() calltimeofday = #231; /* Part of FTE_CALLTIMEOFDAY*/
after speaking with spike and finding a page on the linux system call, I had it all sorted out.

calltimeofday fills a global (I believe) and calling

Code: Select all

	string(float uselocaltime, string format, ...) strftime = #478; /* Part of DP_QC_STRFTIME*/
checks it, and returns the string in the format given as an argument

the applicable string formats can be found in the linux manpages, as strftime is a unix system call
http://linux.die.net/man/3/strftime

below is the call I've been using, remember you may need to strzone it, depending on how you use the string.

Code: Select all

	calltimeofday();
	tod = strftime(1, "%Y-%m-%d %H-%M-%S");

Re: how to use <insert here> in CSQC

Posted: Sun Jan 25, 2015 9:45 am
by gnounc
quick followup, as I believe calltimeofday is in fte and not darkplaces, you may desire to use

Code: Select all

	float(float tmr) gettime = #519;
instead, which will work in either engine.

the list of available arguments to gettime are
(taken straight from darkplaces copy of csprogsdef.qc

Code: Select all

	float GETTIME_FRAMESTART = 0; // time of start of frame
	float GETTIME_REALTIME = 1; // current time (may be OS specific)
	float GETTIME_HIRES = 2; // like REALTIME, but may reset between QC invocations and thus can be higher precision
	float GETTIME_UPTIME = 3; // time since start of the engine
gettime fills the same constants as calltimeofday, so simply stftime it the same as before

Code: Select all

	gettime(GETTIME_REALTIME);
	s_timeofday = strftime(1, "%Y-%m-%d_%H-%M-%S");
and done. remember to check your linux manpage for strftime use!

Re: how to use <insert here> in CSQC

Posted: Sun Jan 25, 2015 10:12 am
by Spike

Code: Select all

void(float sec, float min, float hour, float day, float mon, float year, string datestring) timeofday = //called by engine
{
//you're expected to write the stuff to some global and then just return so the caller can do stuff. this gets around the fact that qc cannot return values at once.
print(sprintf("sec=%g, min=%g, hour=%g\nday=%g,month=%g,year=%g\n\"%s\"\n", sec, min, hour, day, mon, year, datestring);
//note that the date string will be something like "Sun Jan 26, 10:07:21 2015". Why it uses that weird format I have no idea.
};
void() somefunction = //called from qc from elsewhere or whatever
{
calltimeofday(); //get the engine to invoke the timeofday function with the current date+time info.
}
obviously its kinda clumsy and not supported in dp, so just use strftime multiple times and hope the clock doesn't tick weirdly in the race condition between calls, or use substring to make it consistant or something.

Re: how to use <insert here> in CSQC

Posted: Mon Jan 26, 2015 8:26 am
by gnounc
Ok, now for some basic file handling.
Today we'll be working with

Code: Select all

	float(string pattern, float caseinsensitive, float quiet) search_begin = #444; /* Part of DP_QC_FS_SEARCH*/
	float(float handle) search_getsize = #446; /* Part of DP_QC_FS_SEARCH*/
	string(float handle, float num) search_getfilename = #447; /* Part of DP_QC_FS_SEARCH*/
	void(float handle) search_end = #445; /* Part of DP_QC_FS_SEARCH*/
This is the last group of functions I have in my back pocket to do a showcase on. Its the other one I had to look up recently to figure out how to use.
I googled xonotic and nexuiz and search_begin to find a reference use.

search_begin's argument list is pretty self explainatory, with the exception that its first argument "pattern" is just filepath, except that it accepts wildcards */* and the like
search_begin returns a handle to the file list

search_getsize returns how many files were found and added to that list

search_getfilename takes the handle, and the index number of the list as arguments, and tells you the filename of that item

and lastly search_end takes the handle, and closes the file, freeing the memory.

put together it looks something like this. (note i'm using a nonstandard filepath.)

Code: Select all

void printSaves()
{
	string path;
	path = "saves/*/*.fsv";

	float globhandle, idx, fileCount;
	globhandle = search_begin(path, TRUE, FALSE);		//get the file handle

	//return so we dont try to access garbage data
	if (globhandle <= 0)
		return;

	fileCount = search_getsize(globhandle);		//get number of files
	for (idx = 0; idx < fileCount; ++idx)
		print(search_getfilename(globhandle, idx));	//print filenames

	search_end(globhandle);					//close the search
}

Re: how to use <insert here> in CSQC

Posted: Mon Jan 26, 2015 9:17 am
by gnounc
It dawned on me, that I cant very well show how to get a list of files
and not show how to use the most common reason for wanting a list of files.
Namely reading and writing to the files.
for this we will be using

Code: Select all

	float(string filename, float mode, optional float mmapminsize) fopen = #110; /* Part of FRIK_FILE*/
	string(float fhandle) fgets = #112; /* Part of FRIK_FILE*/
	void(float fhandle, string s, optional string s2, optional string s3, optional string s4, optional string s5, optional string s6, optional string s7) fputs = #113; /* Part of FRIK_FILE*/
	void(float fhandle) fclose = #111; /* Part of FRIK_FILE*/
first up, fopen();
The filename argument is obvious. so we'll move on to the mode.
Mode is read, write, append, that sort of thing, for the opened file.
fopen() returns a filehandle, as you might've guessed

Code: Select all

	const float FILE_READ = 0;
	const float FILE_APPEND = 1;
	const float FILE_WRITE = 2;
	const float FILE_READNL = 4;
	const float FILE_MMAP_READ = 5;
	const float FILE_MMAP_RW = 6;
Write overwrites the file, append tacks your additions to the end of it.
not sure what memory map minimum size argument is for, but its optional, so I elect to ignore it.

fgets(), accepts the filehandle, and returns the first line of the file the first time you use it,
and the second line of the file the next, returning an empty string for a newline, and null for end of file.

fputs(), accepts the filehandle, and strings,
then shoves the strings specified in your argument into the file, remember that write overwrites, and append puts it at the bottom.

and fclose(), closes the file freeing the memory.

used altogether it would look something like this

Code: Select all

	float idx;
	filehandle = fopen(myFile, FILE_READ);

	string line;
	while(line = fgets(filehandle))
	{
		print(line);
	}

	fclose(filehandle);
or..

Code: Select all

	filehandle = fopen("playerList", FILE_WRITE);

	entity e;
	string line;
	for(e = find(world, classname, "player"); e ;e = find(e, classname, "player"))
	{
		fputs(filehandle, line);
	}

	fclose(filehandle);

Re: how to use <insert here> in CSQC

Posted: Sun Feb 08, 2015 12:55 am
by gnounc
Today we'll be learning to use draw rotated pic.

Code: Select all

	void(vector pivot, vector mins, vector maxs, string pic, vector rgb, float alpha, float angle) drawrotpic = #0;
		//Draws an image rotating at the pivot. To rotate in the center, use mins+maxs of half the size with mins negated. Angle is in degrees. */

drawrotpic is a brand new function in fteqw, as such, you'll need the latest version (NOT STABLE VERSION..thats insanely outdated)
the latest versions of fte eschew traditional builtin numbers, any builtin you see registered as #0 requires the latest fte, and wont
work in darkplaces.

drawrotpic does not use position and size arguments like drawpic does,
instead the mins is the origin position, and the maxs is the bottom right corner
a good way to do this is to use a variable named img_pos or something along those lines for the mins,
and for maxs, use img_pos + img_size.

the pivot vector is also in screenspace, and not relative to the image.
As noted in the builtin description, find the center vector for the image,
and use that as the pivot if you want the image to rotate around its middle like a turntable


if you'd rather it rotate out like a pocketknife, use the origin (or any corner really) of the image as the pivot instead.

thus we end up with some code like this

Code: Select all


		vector img_pos, img_size, img_color;
		string img_path;
		float img_alpha, img_angles;

		img_pos = '-32 -32';
		img_size = '64 64';
		img_color = '1 1 1';
		img_path = "gfx/icon";
		img_alpha = 1;
		img_angles = time*100;

		drawrotpic('0 0', img_pos, img_pos + img_size, img_path, img_color, img_alpha, img_angles);
and the effect, is an image mostly offscreen on the topleft (only the bottom right quadrant is visible),
that rotates like a turntable.

Re: how to use <insert here> in CSQC

Posted: Sat Feb 28, 2015 2:23 am
by PrimalLove
First want to start out by saying this is similar to Shplud's post about CSQC_Parse_Event. As far as I know this is what you would use for FTE instead of CSQC_Parse_TempEntity(). But with DP it seems we are stuck with this.. :( So I suppose think of this as a continuation of his post but more DP specific.

I recently had some misunderstandings about CSQC_Parse_TempEntity that was cleared up by Spike (Thanks again!) and I noticed little to no reference to this on the forums. Gnounc has a small explanation of it in his very helpful clean csqc but otherwise I rarely see it. Spike mentioned how evil it is but in any event I wanted to give hopefully a more thorough explanation of it its use with some examples. This will apply to both NQ or QW with slight differences in usage that will be noted. This will also assume you have all the appropriate and relevant builtin extensions defined.

Code: Select all

// CSQC_Parse_TempEntity : Handles all temporary entity network data in the CSQC layer.
// You must ALWAYS first acquire the temporary ID, which is sent as a byte.
// Return value should be 1 if CSQC handled the temporary entity, otherwise return 0 to have the engine process the event.
This does exactly what it sounds like. It takes write events from the server and handles them in CSQC.
So on the SSQC side we will look at one example in W_FireAxe. At the bottom of the function you will see something like this:

Code: Select all

NetQuake
	{	// hit wall
		sound (self, CHAN_WEAPON, "player/axhit2.wav", 1, ATTN_NORM);
		WriteByte (MSG_BROADCAST, SVC_TEMPENTITY);
		WriteByte (MSG_BROADCAST, TE_GUNSHOT);
		WriteCoord (MSG_BROADCAST, org_x);
		WriteCoord (MSG_BROADCAST, org_y);
		WriteCoord (MSG_BROADCAST, org_z);
	}

QuakeWorld

	{       // hit wall
		sound (self, CHAN_WEAPON, "player/axhit2.wav", 1, ATTN_NORM);

		WriteByte (MSG_MULTICAST, SVC_TEMPENTITY);
		WriteByte (MSG_MULTICAST, TE_GUNSHOT);
		WriteByte (MSG_MULTICAST, 3);
		WriteCoord (MSG_MULTICAST, org_x);
		WriteCoord (MSG_MULTICAST, org_y);
		WriteCoord (MSG_MULTICAST, org_z);
		multicast (org, MULTICAST_PVS);
	}
You'll notice that QW has some differences. NQ does not support MSG_MULTICAST (Unless you use it in FTE : Credit Spike). MSG_BROADCAST is unreliable. In QW you can use MSG_MULTICAST which is better but still not perfect and its still an unreliable data stream(because it has to be). I should also note that you can still use MSG_BROADCAST in qw its just avoided. (Corrections made here thanks to Spike's input! :) )

The other thing to note is that the latter arugments in QW have two extra calls. The first is a count argument. In some cases (TE_BLOOD) the count argument will only accept 1 as a value. The last call is to let the engine know that this should only be sent to those that meet the requirements. In this case, only to players that can actually see the origin. This will additionally lower bandwidth usage and network traffic. Other than these differences, the code is essentially used the same way on both QW and NQ. (Corrections made here thanks to Spike's input! :) )
In QW particle() is not used so blood is also handled as a temp entity writebyte event. It looks like this:

Code: Select all

/*
================
SpawnBlood
================
*/
void(vector org, float damage) SpawnBlood =
{
	WriteByte (MSG_MULTICAST, SVC_TEMPENTITY);
	WriteByte (MSG_MULTICAST, TE_BLOOD);
	WriteByte (MSG_MULTICAST, 1);
	WriteCoord (MSG_MULTICAST, org_x);
	WriteCoord (MSG_MULTICAST, org_y);
	WriteCoord (MSG_MULTICAST, org_z);
	multicast (org, MULTICAST_PVS);
};
You can do this same thing in NQ source so that all of the blood calls will also be sent as temp entities. Here is an example:

Code: Select all

void(vector org, vector vel, float damage) SpawnBlood =
{
	damage = damage*2;
	vel = vel*0.1;
	WriteByte (MSG_BROADCAST, SVC_TEMPENTITY);
	WriteByte (MSG_BROADCAST, TE_BLOOD);
	WriteCoord (MSG_BROADCAST, org_x);
	WriteCoord (MSG_BROADCAST, org_y);
	WriteCoord (MSG_BROADCAST, org_z);
	WriteCoord (MSG_BROADCAST, vel_x);
	WriteCoord (MSG_BROADCAST, vel_y);
	WriteCoord (MSG_BROADCAST, vel_z);
	WriteByte (MSG_BROADCAST, damage);
	//particle (org, vel*0.1, 73, damage*2); // reference....
};
This is how many if not most of the particle effects are handled in standard NQ and QW game source. For the most part nothing to change on the SSQC side of things. Here are the constant float values you will need in both SSQC and CSQC to use this builtin CSQC Temp Entity Parse function. (Note: this could be different depending on the source the important thing is they match on both sides)

Code: Select all

NQ:
float	TE_SPIKE		= 0;
float	TE_SUPERSPIKE	= 1;
float	TE_GUNSHOT		= 2;
float	TE_EXPLOSION	= 3;
float	TE_TAREXPLOSION	= 4;
float	TE_LIGHTNING1	= 5;
float	TE_LIGHTNING2	= 6;
float	TE_WIZSPIKE		= 7;
float	TE_KNIGHTSPIKE	= 8;
float	TE_LIGHTNING3	= 9;
float	TE_LAVASPLASH	= 10;
float	TE_TELEPORT		= 11;
float TE_BLOOD = 50; // IF using Dpextensions this is already defined as 50 so I used it here for the example below.

QW:
float   TE_SPIKE                = 0;
float   TE_SUPERSPIKE   = 1;
float   TE_GUNSHOT              = 2;
float   TE_EXPLOSION    = 3;
float   TE_TAREXPLOSION = 4;
float   TE_LIGHTNING1   = 5;
float   TE_LIGHTNING2   = 6;
float   TE_WIZSPIKE             = 7;
float   TE_KNIGHTSPIKE  = 8;
float   TE_LIGHTNING3   = 9;
float   TE_LAVASPLASH   = 10;
float   TE_TELEPORT             = 11;
float   TE_BLOOD                = 12;
float   TE_LIGHTNINGBLOOD = 13;
It may be helpful to place these in a shared.qc file so you can use one file for both ssqc and csqc defs. That is up to you as long as they are defined in both. Now in your main.qc or a new qc file you will want to place this to start:
(NOTE: Please note ReadCoord is the same as readcoord in FTE. The builtin numbers are what is important so look to them for references if needed the rest that follows assumes DP. The method used by Shplud above is as far as I know standard practice for FTE using Use CSQC_Parse_Event and Multicasts.)

Code: Select all

float CSQC_Parse_TempEntity()
{
	float Handled, damage;
	vector pos, vel;
	Handled  = TRUE;
	float TEID = ReadByte();	
	switch(TEID)
	{
		case TE_SPIKE:
			pos_x = ReadCoord();
			pos_y = ReadCoord();
			pos_z = ReadCoord();
			pointparticles(particleeffectnum("TE_SPIKE"), pos, '100 0 1', 1);
			Handled = TRUE;
			break;
		case TE_GUNSHOT:
			pos_x = ReadCoord();
			pos_y = ReadCoord();
			pos_z = ReadCoord();
			pointparticles(particleeffectnum("TE_GUNSHOT"), pos, '0 0 1', 1);
			Handled = TRUE;
			break;
		case TE_SUPERSPIKE:
			pos_x = ReadCoord();
			pos_y = ReadCoord();
			pos_z = ReadCoord();
			pointparticles(particleeffectnum("TE_SUPERSPIKE"), pos, '300 0 1', 3);
			Handled = TRUE;
			break;
		case TE_EXPLOSION:
			pos_x = ReadCoord();
			pos_y = ReadCoord();
			pos_z = ReadCoord();
			pointparticles(particleeffectnum("TE_EXPLOSION"), pos, '0 0 1', 1);
			Handled = TRUE;
			break;
		case TE_TAREXPLOSION:
			pos_x = ReadCoord();
			pos_y = ReadCoord();
			pos_z = ReadCoord();
			pointparticles(particleeffectnum("TE_TAREXPLOSION"), pos, '0 0 1', 1);
			Handled = TRUE;
			break;	
		case TE_LIGHTNING1:
			pos_x = ReadCoord();
			pos_y = ReadCoord();
			pos_z = ReadCoord();
			pointparticles(particleeffectnum("TE_LIGHTNING1"), pos, '0 0 1', 1);
			Handled = TRUE;
			break;	
		case TE_LIGHTNING2	:
			pos_x = ReadCoord();
			pos_y = ReadCoord();
			pos_z = ReadCoord();
			pointparticles(particleeffectnum("TE_LIGHTNING2"), pos, '0 0 0', 1);
			Handled = TRUE;
			break;	
		case TE_WIZSPIKE:
			pos_x = ReadCoord();
			pos_y = ReadCoord();
			pos_z = ReadCoord();
			pointparticles(particleeffectnum("TE_WIZSPIKE"), pos, '0 0 0', 1);
			Handled = TRUE;
			break;
                case TE_KNIGHTSPIKE:
			pos_x = ReadCoord();
			pos_y = ReadCoord();
			pos_z = ReadCoord();
			pointparticles(particleeffectnum("TE_KNIGHTSPIKE"), pos, '0 0 0', 1);
			Handled = TRUE;
			break;
		case TE_LIGHTNING3	:
			pos_x = ReadCoord();
			pos_y = ReadCoord();
			pos_z = ReadCoord();
			pointparticles(particleeffectnum("TE_LIGHTNING3"), pos, '0 0 0', 1);
			Handled = TRUE;
			break;	
		case TE_LAVASPLASH:
			pos_x = ReadCoord();
			pos_y = ReadCoord();
			pos_z = ReadCoord();
			pointparticles(particleeffectnum("TE_LAVASPLASH"), pos, '0 0 0', 1);
			Handled = TRUE;
			break;
		case TE_TELEPORT:
			pos_x = ReadCoord();
			pos_y = ReadCoord();
			pos_z = ReadCoord();
			pointparticles(particleeffectnum("TE_TELEPORT"), pos, '0 0 0', 1);
			Handled = TRUE;
			break;	
		case TE_BLOOD: // An additional example showing the usage of additional arguments 
			pos_x = ReadCoord();
			pos_y = ReadCoord();
			pos_z = ReadCoord();
			vel_x = ReadCoord();
			vel_y = ReadCoord();
			vel_z = ReadCoord();
			damage = ReadByte();
			pointparticles(particleeffectnum("TE_BLOOD"), pos, vel, damage);
			Handled = TRUE;
			break;	
		default:
			Handled = FALSE;
			break;
	}	
	return Handled;
}
The important thing to remember here is, if this function returns TRUE you must read all values sent by the writebyte event otherwise a crash will ensue. You also need to read them in the order they are sent. Lets take TE_GUNSHOT for an example:

Code: Select all

               WriteByte (MSG_BROADCAST, SVC_TEMPENTITY); // This tells the server to send a tempentity event (This is what the function looks for)
		WriteByte (MSG_BROADCAST, TE_GUNSHOT); // This tells the server the name of the event to sent (This is read first by the function at the top (TEID))
		WriteCoord (MSG_BROADCAST, org_x); // The coords need to be read in this same order from x to z.  The same goes for any additional writebytes, writeshorts, or writecoords you do additionally. 
		WriteCoord (MSG_BROADCAST, org_y);
		WriteCoord (MSG_BROADCAST, org_z);
You'll notice this only takes care of the particle event itself. If you also want it to replace the sound event as well you can also do this using pointsound(). So:

Code: Select all

case TE_GUNSHOT:
			pos_x = ReadCoord();
			pos_y = ReadCoord();
			pos_z = ReadCoord();
			pointparticles(particleeffectnum("TE_GUNSHOT"), pos, '0 0 1', 1);
            pointsound(pos, "player/axhit2.wav", 1.0, ATTN_NORM);
			Handled = TRUE;
			break;
And on the ssqc side just comment out the sound() call. Pointsound does not rely on Channels but its attenuates differently from what I can tell from other types of sounds. You can adjust this in DP with snd_soundradius to get better results. You can use any custom effect you have made for any of these particles and put them as your particleeffectnum, etc. Anyway, hope this helps explain its usage a bit. If any errors please PM me or find me on qc# so I can correct any mistakes. Will credit you in EDIT. Thanks. :)

EDIT: Spike also wanted me to mention to use sv_csqcdebug cvar in FTE.
Here is a brief explanation of the cvar:
This cvar changes fte's protocol in a subtle way. It adds size counts on a per-entity basis. This makes it easier to find which entity sizes are read/written incorrectly. This is highly FTE specific, and this cvar should only ever be set by csqc modders (or their testers). This will allow the csqc to remain running and also print which entities were not read correctly.

Re: how to use <insert here> in CSQC

Posted: Sun May 07, 2023 8:56 pm
by gnounc
Today we are going to look at hash tables.
The comments in fteextensions.qc are pretty clear here, but having it all laid out tutorial style can still be helpful, so here goes.

Code: Select all

hashtable(float tabsize, optional float defaulttype) hash_createtab = #287;
void(hashtable table) hash_destroytab = #288;
void(hashtable table, string name, __variant value, optional float typeandflags) hash_add = #289;
__variant(hashtable table, string name, optional __variant deflt, optional float requiretype, optional float index) hash_get = #290;
__variant(hashtable table, string name) hash_delete = #291;
string(hashtable table, float idx) hash_getkey = #292;

and supported EV_ types

Code: Select all

const float EV_STRING = 1;
const float EV_FLOAT = 2;
const float EV_VECTOR = 3;
const float EV_ENTITY = 4;
const float EV_FIELD = 5;
const float EV_FUNCTION = 6;
const float EV_POINTER = 7;
const float EV_INTEGER = 8;
const float EV_UINT = 9;
const float EV_INT64 = 10;
const float EV_UINT64 = 11;
const float EV_DOUBLE = 12;
and hash table add behaviors

Code: Select all

const float HASH_REPLACE = 256;	/* Used with hash_add. Attempts to remove the old value instead of adding two values for a single key. */
const float HASH_ADD = 512;	/* Used with hash_add. The new entry will be inserted in addition to the existing entry. */

Hash tables are like dictionaries in python, key value pairs in java, or,
sql tables where every ID is a string, and there is only 1 other column for data.

Hash tables are useful for storing and retrieving data, think loot tables and inventory.


First we declare the hashtable variable as a field, so that we can have 1 per player.

Code: Select all

.hashtable inventory;
Then we need to call hash_createtab, give it a table size, and tell it what type of values it should accept

Code: Select all

self.inventory = hash_createtab(16, EV_ENTITY);
next, we want to actually store something in the table, so lets find the a healthbox and shove it in our inventory.

we are storing the h_box in our self.inventory, under the key "healthbox", and if we already have something stored under "healthbox", then our item will be replaced.

Code: Select all

entity h_box = find(world, classname, "item_health");

if(h_box != world)
{
	hash_add(self.inventory, "healthbox", h_box, HASH_REPLACE);
}
now that we have a healthbox in our inventory, we will want to be able to access it.
so we call hash_get on our self.inventory. and we look up "healthbox", letting hash_get know that we are expecting an EV_ENTITY in return.

Code: Select all

entity default = world;
entity hash_get(self.inventory, "healthbox", optional __variant deflt, EV_ENTITY);

also of interest here, are the arguments deflt and index.

deflt tells hash_add what value we would like returned in the event that the item is NOT found.
for entities, world is the obvious choice, it indicates an error, but say you wanted to grab a weapon, but if that weapon wasnt available you still wanted to equip SOMETHING.
well you could use the deflt field here to say, if you cant find the weapon you are looking for, then just grab me the trusty axe.

Index is a special case, because it isnt useful at all if we are using HASH_REPLACE.
If instead we used HASH_ADD, then we would have multiple entries for the same value, and we could tell it that we wanted the first, second, third, fifth one found in the list

The problem i found with the index value and HASH_ADD, is that while you can add multiple entries to the same key, and retrieve them using the index,
you cant *remove* the items from the hash table using an index. its all or nothing. remove every entry with that key, or leave it be.
This limitation made me decide to not actually use hash tables for inventory for my specific use case, but your case may be a better fit.

now that we have a table, it has something in it, and we know how to retrieve items, its time to wrap things up.

Code: Select all

hash_delete(self.inventory, "healthbox");
will remove the item stored as "healthbox" from your table.

Code: Select all

hash_destroytab(self.inventory);
and hash_destroytab will delete the table entirely when you are done with it.


the last function of note is hash_getkey;

Code: Select all

string(self.inventory, 0);

I find this function less useful because theres no function to get the length of your table, or the number of entries stored under the same name, so its a bit tricky to use.
but given a hard index into your table, it will give you the key name. in our example that would have been "healthbox" before we deleted the entry and the table entirely.

Re: how to use <insert here> in CSQC

Posted: Mon May 08, 2023 4:04 am
by gnounc
Quick overview of the methods in this post. next post for usage walkthrough.
Json is easy to parse if you know the field types beforehand, and a bit of an annoyance if you don't.

Functions to create and destroy a json node.

Code: Select all

jsonnode(string) json_parse = #0:json_parse;
void(jsonnode) json_free = #0:json_free;

Value types that can be pulled from a json object.

Code: Select all

enum json_type_e : int
{
	JSON_TYPE_STRING,
	JSON_TYPE_NUMBER,
	JSON_TYPE_OBJECT,
	JSON_TYPE_ARRAY,
	JSON_TYPE_TRUE,
	JSON_TYPE_FALSE,
	JSON_TYPE_NULL
};

Function to find out which of those value types a given node is.

Code: Select all

json_type_e(jsonnode node) json_get_value_type = #0:json_get_value_type;

Functions to actually pull those values out of the json and pack them into a standard quakec data type.

Code: Select all

int(jsonnode node) json_get_integer = #0:json_get_integer;
float(jsonnode node) json_get_float = #0:json_get_float;
string(jsonnode node) json_get_string = #0:json_get_string;
Function to get the name of the key/value pair.

Code: Select all

string(jsonnode node) json_get_name = #0:json_get_name;
Functions to search for and iterate over json nodes

Code: Select all

jsonnode(jsonnode node, string) json_find_object_child = #0:json_find_object_child;
jsonnode(jsonnode node, int childindex) json_get_child_at_index = #0:json_get_child_at_index;
And lastly, a function to see how many times you should iterate over a json node.

Code: Select all

int(jsonnode node) json_get_length = #0:json_get_length;