FTE ragdoll system

Discuss CSQC related programming.
toneddu2000
Posts: 1395
Joined: Tue Feb 24, 2009 4:39 pm
Location: Italy

FTE ragdoll system

Post by toneddu2000 »

Hi guys, I've started the read of csqc_for_idiots.txt by Spike (I suggest everyone to read it) and, surprisingly, at the end there's a mini chapter:
Ragdoll (FTE_QC_RAGDOLL / FTE_QC_RAGDOLL_WIP):
Ragdoll support is dependant upon ODE, which requires cvar("physics_ode_enable") to be set in order to work properly.
Once you have a skeletal object, you can add in ragdoll by calling skel_ragupdate(self, "doll foo.doll", 0); at the end of your predraw function (which will cause the ragdoll to update and move based upon an animated model, and then call skel_ragupdate(self, "animate 0", 0); once your entity dies and goes limp (typically, this is done part-way through the death animation, so the ragdoll inherits a certain amount of velocity from said animation).
The .doll file describes the bodies and joints within the model. If a body is set to animate, it will use the bone data your code specifies. If a body is not set to animate, it will be ragdolled even when the player is still alive. This can be used for things like ponytails, cloth, etc.
But in the entire FTE engine I can't find the function skel_ragupdate nor any clue related to ragdoll. Is it just a stub? It would be great to know a little more about it!
Meadow Fun!! - my first commercial game, made with FTEQW game engine
Spike
Posts: 2914
Joined: Fri Nov 05, 2004 3:12 am
Location: UK
Contact:

Re: FTE ragdoll system

Post by Spike »

random unedited code splurge.

csqc defs/builtins:
http://triptohell.info/moodles/fteqcc/fteextensions.qc

csqc code. make some console command to invoke test2 or something.

Code: Select all

//keep the ragdoll object updated.
static void() rag_predraw =
{
#if 1
	if (!self.animobject)
	{
		self.animobject = skel_create(self.modelindex);
		skel_build(self.animobject, self, self.modelindex, 0, 0, 0, 1);
	}
#else
	skel_build(self.skeletonindex, self, self.modelindex, 0, 0, 0, 1);
#endif
	skel_ragupdate(self, "", self.animobject);
};

//spawn a ragdoll
void() test2 =
{
	local entity e;
	e = spawn();
	e.movetype = MOVETYPE_NONE;
	e.solid = SOLID_NOT;
	e.owner = world;
	e.angles = [0, 0, 0];
	e.angles_x *= -1;
	e.mass = 10;
	e.drawmask = MASK_NORMAL;
	e.predraw = rag_predraw;
	e.alpha = 0.9;

	setmodel(e, "models/player/erebus.iqm");
	setsize(self, VEC_HULL_MIN, VEC_HULL_MAX);
	setorigin(e, vieworg);

	e.skeletonindex = skel_create(e.modelindex);
	skel_build(e.skeletonindex, e, e.modelindex, 0, 0, 0, 1);
	skel_ragupdate(e, "doll test.doll", e.animobject);
	skel_ragupdate(e, "animate 0", e.animobject);
};
models/player/erebus.iqm is from xonotic. extract it yourself.

models/player/erebus.doll

Code: Select all

//see skeletal.txt (http://fteqw.svn.sourceforge.net/viewvc/fteqw/trunk/specs/skeletal.txt) for info on this file format.

updatejoint default
	type hinge
	draw 0
	offset 0 0 0

	lostop -0.1
	histop 0.1
	axis  0 
	erp  0.2
	cfm  0.001

	lostop2 -0.5
	histop2 0.5
	axis2 
	erp2 0.2
	cfm2 0.001
updatebody default
	shape box
	draw 0
	animate 1
	mass 1



//main body... ish.
body torso		master
	shape sphere
	mass 2

//glue the torso in place.
//joint glue torso
//	type hinge

body head	"head"
	shape sphere
joint head	head  torso
	pivot "neck"
	type universal
	lostop -0.5
	histop 0.5
	lostop2 -2
	histop2 2

//joint glue head
//	type point


//let the legs bend backwards a little
updatejoint default
	lostop -2
	histop 0.1

//left leg
body uleg.l		upperleg_L
//	orient
body lleg.l		lowerleg_L
//	orient
body foot.l		foot_L
//	orient
	size 2

//right leg
body uleg.r		upperleg_R
//	orient
body lleg.r		lowerleg_R
//	orient
body foot.r		foot_R
//	orient
	size 2

joint hip.l torso uleg.l
	type universal
	lostop -1
	histop 1
joint knee.l uleg.l lleg.l
joint ankle.l lleg.l foot.l
	lostop -0.5
	histop 0.5

joint hip.r torso uleg.r
	type universal
	lostop -1
	histop 1
joint knee.r uleg.r lleg.r
joint ankle.r lleg.r foot.r
	lostop -0.5
	histop 0.5


//arms tend to bend forwards...
updatejoint default
	type hinge
	lostop -0.5
	histop 2

//left arm
body uarm.l		upperarm_L
	animate 0
//	orient
body larm.l		forearm_L
	animate 0
//	orient
body hand.l		hand_L
	animate 0
//	orient

//right arm
body uarm.r		upperarm_R
	animate 0
//	orient torso.r
body larm.r		forearm_R
	animate 0
//	orient urarm.r
body hand.r		hand_R
	animate 0
//	orient larm.r

joint shoulder.l torso uarm.l
	type point
	lostop2 -1.6
	histop2 0.1
joint elbow.l uarm.l larm.l
	type point
	lostop -1
	histop 1
	draw 1
joint wrist.l larm.l hand.l
	type point
	lostop -1
	histop 1
//	pivot BONE -32 0 0

joint shoulder.r torso  uarm.r
	type point
	lostop2 -0.1
	histop2 1.6
joint elbow.r uarm.r larm.r
	type point
joint wrist.r larm.r hand.r
	type point
//	pivot BONE 32 0 0

body toe.r toe_R
body toe.l toe_L
updatejoint default
	type hinge
	lostop -0.2
	histop 0.1
	lostop2 -0.2
	histop2 0.1
joint hackfix.l toe.l foot.l
joint hackfix.r toe.r foot.r
if done properly, your console command should make an entity spawn and ragdoll down to the floor.

basically, create a skeletal object, tells it to use a doll file. keep poking your skeletal object each frame to keep it updated (it will will do physics if not poked, but won't update the bone info).
note that the code actually uses two skeletal objects. one contains animation data, the other contains physics+final data. the data in the animation data can be used to tell the 'animated' bodies where to move to (so it can kick things etc), and for the bones without bodies to have some position that looks right. If you don't use a second skeletal object, it'll just use the base pose or something for bones-without-bodies.

it probably doesn't work too well with vid_restart. you should probably spam skel_ragupdate with the doll command rather than the empty command. this ensures the entity resets if the doll became invalid due to being flushed for whatever reason. on the plus side, it does make editing the doll file easier.

its still a Work In Progress extension. I documented it in that document for two reasons:
1) gives people something fun to aim for.
2) needs a bit more use+examples+feedback+etc before it can be more than wip.
its been a while since I touched it...
feel free to suggest lots of fixes/tweaks to make it more robust/versatile/easier.

Sidenote: you can get the same thing working via ssqc by providing a model.iqm.doll file and setting self.frame|=65535. the flag-in-frame thing sets all the dolls 'animate' settings to 0, causing it to go limp and collapse. set that half way through your death animation and it should inherit momentum from the animation. that's the theory anyway.
its still worth using csqc for this stuff, so you can do animation blending properly as per the previous chapter in csqc-for-idiots for when the entity is still alive.
Spike
Posts: 2914
Joined: Fri Nov 05, 2004 3:12 am
Location: UK
Contact:

Re: FTE ragdoll system

Post by Spike »

here's some more code that might be useful.
it just dumps the bone heirachy, frames, skins of a model.
the output might be handy for determining which bones you need to attach your various bodies to, if you don't have the model in an editor or whatever. I dunno.

you can use frameforname to get a frame number for a frame name on a given model(index). hurrah for animation code not caring if the frames get randomly reordered.

Code: Select all

static void(float skel, float numbones, float start, float depth) boneheirachy =
{
	float i, parent;
	//don't print anything for bone 0, because its not real and always exists anyway.
	if (start)
	{
		for (i = 0; i < depth; i++)
			print("  ");
		print(sprintf("%s  (%g)\n", skel_get_bonename(skel, start), start));
	}
	depth+=1;

	for (i = 1; i <= numbones; i++)
	{
		parent = skel_get_boneparent(skel, i);
		if (parent == start)
		{
			boneheirachy(skel, numbones, i, depth);
		}
	}
};

//print out lots of info about a model
void(string fname) showmodelinfo =
{
	local float modelidx;
	float i;
	float numbones;
	float skel;
	string n;
	entity e;
	precache_model(fname);
	e = spawn();
	setmodel(e, fname);
	modelidx = e.modelindex;
	remove(e);
	skel = skel_create(modelidx);
	numbones = skel_get_numbones(skel);
	if (!modelidx)
	{
		print("no model\n");
		return;
	}

	print(sprintf("bone info for %s:\n", modelnameforindex(modelidx)));
	boneheirachy(skel, numbones, 0, 0);
	skel_delete(skel);

	print(sprintf("frame(group) info for %s:\n", modelnameforindex(modelidx)));
	for (i = 0; ; i+=1)
	{
		//DP - frametoname does not exist.
		n = frametoname(modelidx, i);
		if not (n)
			break;
		print(sprintf("framegroup %g: %s  %g secs\n", i, n, frameduration(modelidx, i)));
	}

	print(sprintf("skin info for %s:\n", modelnameforindex(modelidx)));
	for (i = 0; ; i+=1)
	{
		n = skintoname(modelidx, i);
		if not (n)
			break;
		print(sprintf("skin %g: %s\n", i, n));
	}
};
toneddu2000
Posts: 1395
Joined: Tue Feb 24, 2009 4:39 pm
Location: Italy

Re: FTE ragdoll system

Post by toneddu2000 »

Wow Spike thanks a lot I'll try imediately!
Meadow Fun!! - my first commercial game, made with FTEQW game engine
toneddu2000
Posts: 1395
Joined: Tue Feb 24, 2009 4:39 pm
Location: Italy

Re: FTE ragdoll system

Post by toneddu2000 »

Nope. The model remains at middle air.
I tried PHYSICS_ODE_ENABLE 1
i changed the line MOVETYPE_NONE TO MOVETYPE_PHYSICS
i recreated the doll file with SKEL_GENERATERAGDOLL (it creates the file erebus.iqm.doll)
i changed the doll to skel_ragupdate(e, "doll erebus.iqm.doll", e.animobject);
fteqcc yells that rag_predraw should return a float not a void but if a change to float the model disappears
I had to include some costants like MASK_NORMAL because are not present in fteextensions
I compiled ODE realese double dll and I put double_dll in the folder of fte exe (I use the experimental 32 and 64bit because the stable 32bit remains searching "connecting to internal server...")
I tried even with compiled ode_single.dll
Nothing, the model is hanged over there. There is some already-made code to use?
here my files
progs.src

Code: Select all

#pragma TARGET FTE
#pragma progs_dat "../csprogs.dat"
#define CSQC //for the following line
#include "fteextensions.qc" //include the various system fields, globals, constants, and builtins.
#include "ragdoll.qc"

void CSQC_UpdateView (float width, float height, float menushown) 
{
	clearscene(); 		// CSQC builtin to clear the scene of all entities / reset our view properties
	setproperty(VF_DRAWWORLD, 1); 		// we want to draw our world!
	setproperty(VF_DRAWCROSSHAIR, 1);		 // we want to draw our crosshair!
	addentities(MASK_NORMAL | MASK_ENGINE | MASK_ENGINEVIEWMODELS); 		// add entities with these rendermask field var's to our view
	renderscene(); 		// render that puppy to our screen, 
}

float(float evtype, float scanx, float chary, float devid) CSQC_InputEvent =
{
   if (evtype == IE_KEYDOWN)
   {
      test2();
   }
   else
      return FALSE;//not recognised, let the engine do its thing.
};
ragdoll.qc

Code: Select all

.float animobject;
vector ragdoll_org;

const float MASK_ENGINE			= 1;
const float MASK_ENGINEVIEWMODELS	= 2;
const float MASK_NORMAL			= 4;
vector	VEC_HULL_MIN = '-16 -16 -24';
vector	VEC_HULL_MAX = '16 16 32';

//keep the ragdoll object updated.
static void() rag_predraw =
{
#if 1
   if (!self.animobject)
   {
      self.animobject = skel_create(self.modelindex);
      skel_build(self.animobject, self, self.modelindex, 0, 0, 0, 1);
   }
#else
   skel_build(self.skeletonindex, self, self.modelindex, 0, 0, 0, 1);
#endif
   skel_ragupdate(self, "", self.animobject);
};

//spawn a ragdoll
void() test2 =
{
   ragdoll_org = [220,550,160];
   local entity e;
   e = spawn();
   e.movetype = MOVETYPE_PHYSICS;
   e.solid = SOLID_NOT;
   e.owner = world;
   e.angles = [0, 0, 0];
   e.angles_x *= -1;
   e.mass = 10;
   e.drawmask = MASK_NORMAL;
   e.predraw = rag_predraw;
   e.alpha = 0.9;

   setmodel(e, "models/player/erebus.iqm");
   setsize(self, VEC_HULL_MIN, VEC_HULL_MAX);
   setorigin(e, ragdoll_org);

   e.skeletonindex = skel_create(e.modelindex);
   skel_build(e.skeletonindex, e, e.modelindex, 0, 0, 0, 1);
   skel_ragupdate(e, "doll erebus.iqm.doll", e.animobject);
   skel_ragupdate(e, "animate 0", e.animobject);
};
Thanks again

PS: if I invoke SKEL_INFO it appears a list DOLL:0, DOLL:1,...why?
Meadow Fun!! - my first commercial game, made with FTEQW game engine
Spike
Posts: 2914
Joined: Fri Nov 05, 2004 3:12 am
Location: UK
Contact:

Re: FTE ragdoll system

Post by Spike »

physics_ode_enable must be 1. the engine will force+lock it to 0 if the dll can't be loaded.
the 'doll' command must be given a full (quake) path. my lame example just used 'test.doll' in a crazy location.
the 'skel_info' command shows that you don't have the doll instanciated properly.
toneddu2000
Posts: 1395
Joined: Tue Feb 24, 2009 4:39 pm
Location: Italy

Re: FTE ragdoll system

Post by toneddu2000 »

physics_ode_enable must be 1. the engine will force+lock it to 0 if the dll can't be loaded.
There's at least a method to understand if the ode dlls are not correctly loaded? A console command for example?
because the dlls are there. I checked at the FTE engine src and I found that(in the World_Physics_Init() function):

Code: Select all

#ifdef ODE_DYNAMIC
	const char* dllname =
	{
# if defined(WIN64)
		"libode1_64.dll"
# elif defined(WIN32)
		"ode_double"
# elif defined(MACOSX)
		"libode.1.dylib"
# else
		"libode.so.1"
# endif
	};
#endif
I'm ignorant in programming, really, but the second string doesn't miss ".dll" suffix at the end?
And, by the way, I couldn't find an ode solution at 64bit. If I search the net for libode1_64.dl I can't find anything
Meadow Fun!! - my first commercial game, made with FTEQW game engine
toneddu2000
Posts: 1395
Joined: Tue Feb 24, 2009 4:39 pm
Location: Italy

Re: FTE ragdoll system

Post by toneddu2000 »

Hurray!!Real Ragdolls on Quake!
Image
Too much tired tonight to make other tests because it's almost 26 hours that I try! :D
I post only temp code. Tomorrow I'll make a detailed guide because it's not so intuitive to setup the environment

You need:
Xonotic erebus model (you can find here the blend model)
fteqw src compiled from here (I couldn't compile on Microsoft Visual Studio 2008/2010 so I used Linux, let me know if someone is able to compile fteqw on visual studio)
fteextensions.qc
ragdoll.qc -just make attention to point to .doll file with FULL path (i noticed that, on Linux, it generates doll file in .fte/models/player/, on Windows it generates in users\X\Documents\My Games\FTE QuakeWorld\fte so i placed there models and .doll files)
I was wrong about the MOVETYPE_PHYSICS, it needs a normal MOVETYPE_STEP

Code: Select all

//keep the ragdoll object updated.
void() rag_predraw =
{
#if 1
   if (!self.animobject)
   {
      self.animobject = skel_create(self.modelindex);
      skel_build(self.animobject, self, self.modelindex, 0, 0, 0, 1);
   }
#else
   skel_build(self.skeletonindex, self, self.modelindex, 0, 0, 0, 1);
#endif
   skel_ragupdate(self, "", self.animobject);
};

//spawn a ragdoll
void RagdollSpawn()
{
   local entity e;
   local float numbones;
   e = spawn();
   e.movetype = MOVETYPE_STEP;
   e.owner = world;
   e.angles = [0, 0, 0];
   e.angles_x *= -1;
   e.mass = 10;
   e.drawmask = MASK_NORMAL;
   e.predraw = rag_predraw;
   e.alpha = 0.9;

   setmodel(e, "models/player/erebus.iqm");
   setsize(e, VEC_HULL_MIN, VEC_HULL_MAX);
   setorigin(e, [465, 620, 180]);

   e.skeletonindex = skel_create(e.modelindex);
   skel_build(e.skeletonindex, e, e.modelindex, 0, 0, 0, 1);
   skel_ragupdate(e, "doll models/player/erebus.iqm.doll", e.modelindex);
   skel_ragupdate(e, "animate 0", e.modelindex);
   numbones = skel_get_numbones(e.skeletonindex);
   print("num bones ",ftos(numbones)," animobject ",ftos(e.modelindex),"\n"); //check if engine has correctly loaded bones from the model skeleton
}
main.qc (almost blank)

Code: Select all

void CSQC_Init(float apilevel, string enginename, float engineversion)
{
	RagdollSpawn ();
}

void CSQC_Ent_Update (float isnew)
{
	rag_predraw();
}

void CSQC_UpdateView (float width, float height, float menushown) 
{
	clearscene(); 
	setproperty(VF_DRAWWORLD, 1); 		// we want to draw our world!
	setproperty(VF_DRAWCROSSHAIR, 1);
	addentities(MASK_NORMAL | MASK_ENGINE | MASK_ENGINEVIEWMODELS); 		// add entities with these rendermask field var's to our view
	renderscene(); 
}
VERY IMPORTANT: ragdoll .doll file (incorrect values make the model INACTIVE to ragdoll)

Code: Select all

updatejoint default
   type hinge
   draw 0
   offset 0 0 0

   lostop -0.1
   histop 0.1
   //axis  0 
   erp  0.2
   cfm  0.001

   lostop2 -0.5
   histop2 0.5
   //axis2 
   erp2 0.2
   cfm2 0.001
updatebody default
   shape box
   draw 0
   animate 0
   mass 1



//main body... ish.
body torso      master
   shape sphere
   mass 2

//glue the torso in place.
//joint glue torso
//   type hinge

body head   "head"
   shape sphere
joint head   head  torso
   pivot "neck"
   type universal
   lostop -0.5
   histop 0.5
   lostop2 -2
   histop2 2

//joint glue head
//   type point


//let the legs bend backwards a little
updatejoint default
   lostop -2
   histop 0.1

//left leg
body uleg.l      upperleg_L
//   orient
body lleg.l      lowerleg_L
//   orient
body foot.l      foot_L
//   orient
   size 2

//right leg
body uleg.r      upperleg_R
//   orient
body lleg.r      lowerleg_R
//   orient
body foot.r      foot_R
//   orient
   size 2

joint hip.l torso uleg.l
   type universal
   lostop -1
   histop 1
joint knee.l uleg.l lleg.l
joint ankle.l lleg.l foot.l
   lostop -0.5
   histop 0.5

joint hip.r torso uleg.r
   type universal
   lostop -1
   histop 1
joint knee.r uleg.r lleg.r
joint ankle.r lleg.r foot.r
   lostop -0.5
   histop 0.5


//arms tend to bend forwards...
updatejoint default
   type hinge
   lostop -0.5
   histop 2

//left arm
body uarm.l      upperarm_L
   animate 0
//   orient
body larm.l      forearm_L
   animate 0
//   orient
body hand.l      hand_L
   animate 0
//   orient

//right arm
body uarm.r      upperarm_R
   animate 0
//   orient torso.r
body larm.r      forearm_R
   animate 0
//   orient urarm.r
body hand.r      hand_R
   animate 0
//   orient larm.r

joint shoulder.l torso uarm.l
   type point
   lostop2 -1.6
   histop2 0.1
joint elbow.l uarm.l larm.l
   type point
   lostop -1
   histop 1
   draw 1
joint wrist.l larm.l hand.l
   type point
   lostop -1
   histop 1
//   pivot BONE -32 0 0

joint shoulder.r torso  uarm.r
   type point
   lostop2 -0.1
   histop2 1.6
joint elbow.r uarm.r larm.r
   type point
joint wrist.r larm.r hand.r
   type point
//   pivot BONE 32 0 0

body toe.r toe_R
body toe.l toe_L
updatejoint default
   type hinge
   lostop -0.2
   histop 0.1
   lostop2 -0.2
   histop2 0.1
joint hackfix.l toe.l foot.l
joint hackfix.r toe.r foot.r
progs.src - usual stuff

Code: Select all

#pragma TARGET FTE
#pragma progs_dat "../csprogs.dat"
#define CSQC
#include "fteextensions.qc"
#include "ragdoll.qc"
#include "main.qc"

With these three files you should be able to see your model fall like a.. uhm .. like a rag doll! :D


PS:Note for Spike. I'm still confused with the implementation of the ragdoll in the fteqw engine. Because I searched for skel_ragupdate and it's present in pr_cmds.c and pr_csqc.c files inside the struct BuiltinList which corresponds to function PF_skel_ragedit() but this function is not defined anywhere!This is the only definition I found:

Code: Select all

void QCBUILTIN PF_skel_ragedit(pubprogfuncs_t *prinst, struct globalvars_s *pr_globals);
How is it possible? Thanks again for your kindness
Meadow Fun!! - my first commercial game, made with FTEQW game engine
Spike
Posts: 2914
Joined: Fri Nov 05, 2004 3:12 am
Location: UK
Contact:

Re: FTE ragdoll system

Post by Spike »

its in pr_skelobj.c

I'm glad you got it working. :)
it helps prove that I'm not making stuff up on a whim. :D
ceriux
Posts: 2230
Joined: Sat Sep 06, 2008 3:30 pm
Location: Indiana, USA

Re: FTE ragdoll system

Post by ceriux »

a video would show more than a screenshot when it comes to screenshots, imo... sorry to not be able to contribute more than that.
toneddu2000
Posts: 1395
Joined: Tue Feb 24, 2009 4:39 pm
Location: Italy

Re: FTE ragdoll system

Post by toneddu2000 »

ceriux wrote:a video would show more than a screenshot
Here. :)
This is the first video of a (I hope) long series. I would like to implement ragdolls in csqc in a complete manner. In this video simply it's raining dolls!
Spike wrote:its in pr_skelobj.c
Thanks Spike, I still don't get it how the builtin functions work but I'll learn!
Spike wrote:I'm glad you got it working. :)
it helps prove that I'm not making stuff up on a whim. :D
:lol: No, I never thought it! You're a genius!
Thanks again for your help!

PS: Can you please explain which are the devs of FTEQW,beyond you?
And, what is fteqtv?
Meadow Fun!! - my first commercial game, made with FTEQW game engine
Spike
Posts: 2914
Joined: Fri Nov 05, 2004 3:12 am
Location: UK
Contact:

Re: FTE ragdoll system

Post by Spike »

there have not been many direct code contributions for a while, other than from myself.
TimeServ still contributes with server hosting (fteqw.com and triptohell.info are both his).
I do sometimes 'steal' code from DarkPlaces, primarily for compatibility, but also for laziness sometimes. The initial ODE stuff (read: MOVETYPE_PHYSICS) is a relevent example of this.

fteqtv is a proxy system, essentually mvd-over-tcp chains, that can be used to broadcast a live game amoungst a potentially unlimited number of viewers (if there's proxies running on enough hosts).
you can connect to the proxy with either a qw or nq client, and either play on a qw server, or watch a live stream.
ceriux
Posts: 2230
Joined: Sat Sep 06, 2008 3:30 pm
Location: Indiana, USA

Re: FTE ragdoll system

Post by ceriux »

thanks for the video, do the bodies react when shot?
toneddu2000
Posts: 1395
Joined: Tue Feb 24, 2009 4:39 pm
Location: Italy

Re: FTE ragdoll system

Post by toneddu2000 »

Still not, next weekend: that task! Wait for another video! :)
Meadow Fun!! - my first commercial game, made with FTEQW game engine
toneddu2000
Posts: 1395
Joined: Tue Feb 24, 2009 4:39 pm
Location: Italy

Re: FTE ragdoll system

Post by toneddu2000 »

Spike wrote:there have not been many direct code contributions for a while, other than from myself.
I've not seen your reply!
Unbelievable. An enormous work to do all by yourself. Compliments. FTE and DP are now more than a quake engine, are game engine on their own.
Meadow Fun!! - my first commercial game, made with FTEQW game engine
Post Reply