Racing Ghosts (As this unfolds)

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

Racing Ghosts (As this unfolds)

Post by Baker »

qbism wrote:'Demo rewind' , 'record demo any time' , host_timescale, and video capture start/stop work together for a video package that can save a lot of production time. (I need to go back and look at pause_demo also!) I tried capture_start in the middle of demo playback, and the video matched the rewinds and host_timescale frame rate changes made during playback.

Racing ghosts would be fun ...
Racing ghosts is the idea of a fake player appearing that you can race against. Like let's say you want to improve on beating a level. This should work with just about any mod (I'm not going to think about cutscenes ... at least I don't think I am? Although if I am right those are cl.intermission = 3 or something).

[Who knows, this might also be useful in single player as some sort of "I'm stuck and don't know what to do".]

I peeked at the engine and I am rather confident I can make this happen and hopefully fast.

Elements:

1. Will need to be able to create a ghost file from a demo. timedemo is the best candidate to copy because it tries to run through everything fast. So we will create a ghostdemo function that essentially is a timedemo except that it won't render or make sound. It will record everything to a fix-sized record with a time stamp if the player moves or the player angles change. We will do this in CL_RelinkEntities.

2. The file created will be kind of like this:
No file header or identifying data because the intent is that this will be made FROM a demo.
Map change: Type bit (0) ... MAX_QPATH (64) --- err lets use 31 instead --- of newmap. [32 chars] // Occurs at beginning of file and upon map change. If map name exceeds 31 chars, that's just too bad (how many map names are 27 or more characters long? I know of none. 26 = "maps/whatever" so we don't count "maps/" 5 chars).
Location data: Type bit (1) ... frame number (2 bytes) ... AND as unsigned int (x 100) --->cl.time, player x, y, z, pitch, yaw, roll [32 bytes] (this could be improved by adding the model index, but, eyes.mdl are hard to see and I don't want to break 32 bytes and this doesn't need to be perfect the first time).

Location data checks against player x, y, z and angles from previous frame. If no change, doesn't bother to write any data. Consider the idea of limiting writes to, say, 5 times per second. [You have no way of knowing what FPS a demo was recorded at, someone doing 600 fps ... well, the file could get needlessly large].

Not going to worry about effects or skins or other stuff. Since this file doesn't need to be compatible with anything, that can just be changed in the future if need be the case
3. Play back. We are supporting multimap demos here. Scan through the ghost file and locate the current map. The time read will be "1" (Quake time starts at 1) [we will store the offset time]. Read another record every time cl.time exceeds the current ghost time [if end of file or the map changed in the ghost file, the ghost will forever remain stationary]. Maybe interpolate the location and angles of the ghost.

4. Drawing the ghost ... Probably right before R_DrawViewModel. Maybe make a memcpy of the player entity and overwrite the origin, angles, frame and do R_DrawAliasModel.

In theory ...
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 ..
Spirit
Posts: 1065
Joined: Sat Nov 20, 2004 9:00 pm
Contact:

Re: Racing Ghosts (As this unfolds)

Post by Spirit »

While you are at that, a general demo parser -> human readable output would be ace. QuakeWorld has mvdparser for that http://qw-dev.net/svn/mvdparser/ but I am not aware of any such tool to process quake/netquake demos.

It could be interesting for analysing sp demos, eg where do players go, where do they die etc.
Improve Quaddicted, send me a pull request: https://github.com/SpiritQuaddicted/Quaddicted-reviews
Baker
Posts: 3666
Joined: Tue Mar 14, 2006 5:15 am

Re: Racing Ghosts (As this unfolds)

Post by Baker »

Spirit wrote:While you are at that, a general demo parser -> human readable output would be ace. QuakeWorld has mvdparser for that http://qw-dev.net/svn/mvdparser/ but I am not aware of any such tool to process quake/netquake demos.

It could be interesting for analysing sp demos, eg where do players go, where do they die etc.
That is very possible because essentially in the last 30 minutes I've modified the client to be able to defacto parse demos. Although other than making the heat maps of where players die using MVD, I'm not sure what else Quakeworld does with that ...

[Although one problem is getting single player guys to record demos ... and really some mechanism to easily share them.]
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 ..
qbism
Posts: 1236
Joined: Thu Nov 04, 2004 5:51 am
Contact:

Re: Racing Ghosts (As this unfolds)

Post by qbism »

Baker wrote:[Although one problem is getting single player guys to record demos ... and really some mechanism to easily share them.]
func_msg will usually have several play-through demos posted after new map releases. Most are FQ or DP protocol demos. Maybe speedruns? http://speeddemosarchive.com/quake/
Baker
Posts: 3666
Joined: Tue Mar 14, 2006 5:15 am

Re: Racing Ghosts (As this unfolds)

Post by Baker »

This is close to done. :D

+/- deciding if I think I need to get beer to wade through the final steps here of using the data ...
... I'm supporting multimap demos, involves some headaches...
... on the plus side, I think CL_Traceline can be used to keep the "ghost" from going out of sight (in the future)
..... basically pause the ghost if is be going to go out of visibility and keep him paused until he comes back into visibility.
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: Racing Ghosts (As this unfolds)

Post by Baker »

Debugging phase ...

(Getting ghost progression right, etc.)
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: Racing Ghosts (As this unfolds)

Post by Baker »

Hah ... working out the timing of this is soup-of-clocks. The pausing the ghosts BEFORE they go somewhere you can't see if perfect.

But there are 3 or 4 different timing scenarios that can go on here to do this right:

1. Someone else's demo may have a high fps or a low fps.
2. A demo may not start at cl.time 1
3. The data collection process that runs through a demo (at 30,000 - 100,000 fps = essentially instant :D ) --- which is like timedemo without the rendering or sound --- I don't have it waste space by writing if the player didn't move or look around or change frame (but it does record the cl.time obviously).
4. I need to allow for not only the client to be paused (opening the console in single player, for instance) but to stop the ghost so the player can catch up.

I've almost got this last part cracked. I have to have a virtual time (ghost time converted to client time) and if everything is paused then update the virtual time baseline and keep track of a "future time" which is essentially when we read again after cl.time exceeds that.

[But in reality, it is just slightly more of a mess than CL_RelinkEntities mtime[0]/ mtime[1] frac stuff. Unfortunately, like that, ghosts have to figure out if they teleported to avoid improper movement interpolation.]

/Note: Do you think Quake draws fast with the console up? I discovered that with a lot of text on-screen, it is actually very slow to render. This shouldn't be a surprise I guess, but I noticed my command to run through a demo hit only 400 fps with a lot of console text on the screen and 1100 fps with a clear console. When I had it stop refreshing the rendering entirely while the command was running, it hit several tens of thousands fps. The console is still on screen, it just isn't needless re-drawing it so it isn't like the screen is black, no it looks quite normal when the command is running.
Last edited by Baker on Thu Aug 02, 2012 9:47 am, edited 1 time in total.
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 ..
Spirit
Posts: 1065
Joined: Sat Nov 20, 2004 9:00 pm
Contact:

Re: Racing Ghosts (As this unfolds)

Post by Spirit »

How can a user change the frame rate of a recorded demo? I always assumed that was a fixed number.
Improve Quaddicted, send me a pull request: https://github.com/SpiritQuaddicted/Quaddicted-reviews
Baker
Posts: 3666
Joined: Tue Mar 14, 2006 5:15 am

Re: Racing Ghosts (As this unfolds)

Post by Baker »

Spirit wrote:How can a user change the frame rate of a recorded demo? I always assumed that was a fixed number.
Well, first, I wish they couldn't. But say you are using FitzQuake, do host_maxfps 999 and record a demo as an example (make sure vid_vsync is off). 10 seconds of demo should be a LOT bigger than a 72 fps demo. [90% sure of this, haven't actually tried it.]

JoeQuake *forces* 72 fps max when recording a demo.

If connected to a server, I *think* essentially your demo frames per second is capped to the effective frame rate you are receiving which from a dedicated server would probably always be 72 fps or lower (sys_ticrate is the main factor, but I haven't thought about it much.).

/I'm somewhat confident everything above is correct. But there might be something outstanding I didn't think of.
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: Racing Ghosts (As this unfolds)

Post by Baker »

This is completed.

(I would upload the binary without the source. Except someone like Spirit might nag me. But I need to clean up the source since I had an unknown tough problem that took me a little while to figure out and I have some "trial code commented out here and there" and otherwise it's .... well the source needs cleaned up.)

Here is how it works:

type in console: ghostdemo demo1; map e1m3

It runs through demo1 instantly (you don't even know it happened) which creates a ghostfile. When e1m3 loads up, you are playing E1M3 with the ghost of John Romero running around the map attacking and such. The ghost will always immediately stop if it goes out of sight including a door or a plat (collision finesee ++++ I really had to put some work into that).

Code looks like this:

Code: Select all

typedef struct // Used to write ghost data
{
// WRITTEN PER MAP CHANGE
	char		player_map[31];
// WRITTEN WHEN PLAYER DATA CHANGES
	short		player_frame;
	int			player_time;
	int			player_origin[3];
	int			player_angles[3];
} ghostw_t;

typedef struct
{
	double		ghost_time;			// Named so we know this isn't a cl.time (have to convert to get "virtual cl.time"
	short		frame;
	vec3_t		origin;
	vec3_t		angles;
} mghost_t; // To some extent this must above.

typedef struct
{
// CALCULATED EVERY FRAME

//	qboolean	waiting_current;	// Are we in a waiting state this frame?
//	float		frac_time;			// Current REAL fraction between old time and new time
//	double		ghost_time;			// Derived ghost time of NOW.  The formula NEVER changes (but virtual_base_time advances if ghost is waiting so the conversion can change)

//	double		cl_time;
	double		ghost_time;			// Mostly to detect end of time.
	float		frac_final;			// fraction between last frame and future frame.  If EOF, frac is 1.
									// This must be kept so we can interpolate between frames
	short		framepast;
	short		framefuture;
	vec3_t		origin;
	vec3_t		angles;
	
// ^^ Keep that, it doesn't depend on any rendering information

// If frac ghost isn't visible ... this accumulates.  If ghost is 
	

} ghostfme_t;

typedef struct
{
	mghost_t	past;
	mghost_t	future;			
	ghostfme_t	current;			// The "present" we CAN render.
	ghostfme_t	current_failed;		// The "present" we CAN'T render.  Until this can be rendered, we accumulate waiting and do not move time forward

// RE-CALCULATED UNDER CERTAIN CIRCUMSTANCES: These 2 help us convert between ghost time and cl.time
	double		virtual_base_time;// Calculated ONCE per map.  Never changes.
	
// These can only be changed by a committed simulated present
	double		waiting_accumulator;	// If ghost is waiting, this accumulates "ghost paused" time which holds back the ghost clock from really advancing.
	qboolean	end_of_time;			// At end of file and past future time
	
	// No future until this is cleared.  If we can't be seen now, we can't progress to future.
	qboolean	out_of_sight;			// Retry previous "current" as-is.
	double		out_of_sight_cltime;	// Only way to store this, since we can't use ghost_times to accumulate wait inactivity		

	double		last_cl_time;
	//	qboolean	rvisible;				// Rendering: Is ghost visible.  Only matters for caption.
// ^^ Kill!  Even a dead ghost needs that.
} ghost_t;
And this ...

Code: Select all

/*
====================
CL_GhostDemo_f

ghostdemo [demoname]
====================
*/
void CL_GhostDemo_f (void)
{
	if (cmd_source != src_command)
		return;

	if (Cmd_Argc() != 2)
	{
		Con_Printf ("%s <demoname> : generates a ghostfile\n", Cmd_Argv(0));
		return;
	}

#if 0
	// Baker: This is a performance benchmark.  No reason to have console up.
	if (key_dest != key_game)
		key_dest = key_game;
#endif

	CL_Clear_Demos_Queue (); // ghostdemo is a very intentional action

	cls.ghostdemo = true;
	CL_PlayDemo_f ();
	cls.ghostdemo = false;

	// don't trigger ghostdemo mode if playdemo fails
	if (!cls.demofile) return;

//	cls.ghost_player_x = cls.ghost_player_y = cls.ghost_player_z =
//	cls.ghost_player_pitch = cls.ghost_player_yaw = cls.ghost_player_roll = 0 ;

//	cls.ghost_player_map[0] = 0;
// Erase ghost write history
	memset (&cls.ghost_write, 0, sizeof(cls.ghost_write) );

	sprintf (cls.ghostname, "%s/%s", com_gamedir, Cmd_Argv(1));
	COM_DefaultExtension (cls.ghostname, ".ghost");
//	Con_Printf ("recording to %s.\n", name);
	cls.ghostfile = fopen (cls.ghostname, "wb");
	if (!cls.ghostfile)
	{
		cls.ghostname[0] = 0;
		Con_Printf ("ERROR: couldn't open.\n");
		return;
	}


// cls.td_starttime will be grabbed at the second frame of the demo, so
// all the loading time doesn't get counted

	cls.ghostdemo = cls.timedemo = true;
	cls.td_startframe = host_framecount;
	cls.td_lastframe = -1;		// get a new message this frame

#if 1
	Cmd_Wait_f (); // Stop execution of command buffer until we are done
#endif
}

// Returns true is the point in the cube
qboolean PointInCube(vec3_t point, vec3_t cube_mins, vec3_t cube_maxs)
{
	if (cube_mins[0] <= point[0] && point[0] <= cube_maxs[0])
		if (cube_mins[1] <= point[1] && point[1] <= cube_maxs[1])
			if (cube_mins[2] <= point[2] && point[2] <= cube_maxs[2])
				return true;

	return false;
}


// Baker: This is sort of a cl_main.c kind of function
qboolean CL_GhostVisible (vec3_t ghost_origin)
{
	vec3_t real_player = {cl_entities[cl.viewentity].origin[0], cl_entities[cl.viewentity].origin[1], cl_entities[cl.viewentity].origin[2] /*+ cl.viewheight*/};
	vec3_t ghost_center = {ghost_origin[0], ghost_origin[1], ghost_origin[2]};
	vec3_t ghost_head = {ghost_origin[0], ghost_origin[1], ghost_origin[2] + 32 - 0.1};
	vec3_t ghost_feet = {ghost_origin[0], ghost_origin[1], ghost_origin[2] - 24 + 0.1};
	vec3_t corner;
	//model_t *player_model = Mod_ForName("progs/player.mdl", false);
	entity_t *ent;
	int i, j;

	// Check the center against the static unchanging world
	if (TraceLine (real_player, ghost_center, corner) == false)
		if (TraceLine (real_player, ghost_head, corner) == false)
			if (TraceLine (real_player, ghost_feet, corner) == false)
				return false; // Can't see center, head or feet

	// We can see the center of the player
	// Yet if the player is inside ANY of the world submodels
	// Stop him
	// So check all 8 corners of the player against every
	// Visible submodel.  Keep in mind that this should be ok
	// Since a submodel that blocks the player from being seen
	// should be visible if the player is visible

	for (i = 0 ; i < cl_numvisedicts ; i++)
	{
		vec3_t mins, maxs;
		ent = cl_visedicts[i];

		if (!ent->model)
			continue;   // no model for ent

		if (ent->model->type != mod_brush)
			continue; // Only want brush models

		if (!(ent->model->surfaces == cl.worldmodel->surfaces))
			continue; // This is a health box or something ...

		// Do we need to adjust?  I don't think so
		// But if we do, we'll find out I guess
		if (ent->angles[0] || ent->angles[2]) //pitch or roll
		{
			VectorAdd (ent->origin, ent->model->rmins, mins);
			VectorAdd (ent->origin, ent->model->rmaxs, maxs);
		}
		else if (ent->angles[1]) //yaw
		{
			VectorAdd (ent->origin, ent->model->ymins, mins);
			VectorAdd (ent->origin, ent->model->ymaxs, maxs);
		}
		else //no rotation
		{
			VectorAdd (ent->origin, ent->model->mins, mins);
			VectorAdd (ent->origin, ent->model->maxs, maxs);
		}

//	VectorSet (hull->clip_mins, -16, -16, -24);
//	VectorSet (hull->clip_maxs,  16,  16,  32);
		
		for (j = 0; j < 8; j++)
		{
			corner[0] = ghost_origin[0] + ((j&1)? -16 + 0.1: +16 - 0.1);
			corner[1] = ghost_origin[1] + ((j&2)? -16 + 0.1: +16 - 0.1);
			corner[2] = ghost_origin[2] + ((j&4)? -24 + 0.1: +32 - 0.1);

			// This assumes a tiny brush isn't entirely inside the ghost player
			// I'm not going to worry about that.
			if (PointInCube(corner, mins, maxs) == true) 
			{
//				Con_Printf ("Player is in submodel\n");
				return false; // some part of the player is in the brush
			}
		}		
	}

	return true;
}


void CL_GhostReadRecord (mghost_t *read_data, int newrec, int *out_current_record)
{
	#define FIXEDPOINT_READ(var) fread(&read_int, 1, sizeof(read_int), cls.ghostfileread); var = read_int / 1000.0;
	byte rectype;
	int read_int;
	short frame_in;

	fseek (cls.ghostfileread, newrec * 32, SEEK_SET);
	fread (&rectype, 1, sizeof(rectype), cls.ghostfileread);
	fread (&frame_in, 1, sizeof(frame_in), cls.ghostfileread); // Make sure this is 2!
	read_data->frame = frame_in; // Yay
	FIXEDPOINT_READ (read_data->ghost_time);
	FIXEDPOINT_READ (read_data->origin[0]);
	FIXEDPOINT_READ (read_data->origin[1]);
	FIXEDPOINT_READ (read_data->origin[2]);
	FIXEDPOINT_READ (read_data->angles[0]);
	FIXEDPOINT_READ (read_data->angles[1]);
	FIXEDPOINT_READ (read_data->angles[2]);

	*out_current_record = newrec;
}

#define CL_TIME_FOR(x) (x - cls.ghost_read.virtual_base_time + cls.ghost_read.waiting_accumulator)
#define GHOST_TIME_FOR(x) (x + cls.ghost_read.virtual_base_time - cls.ghost_read.waiting_accumulator)
static qboolean sCL_Ghost_Init_Think (void)
{
	
	if (cls.ghost_record_map_current != 0)
		return false;
	
	// Erase
	memset (&cls.ghost_read, 0, sizeof(cls.ghost_read));

	// READ AND INITIALIZE
	CL_GhostReadRecord (&cls.ghost_read.past, cls.ghost_record_map_begin, &cls.ghost_record_map_current);
	CL_GhostReadRecord (&cls.ghost_read.future, cls.ghost_record_map_begin + 1, &cls.ghost_record_map_current);

	// Establish time base
	// Example ... ghost time starts at 29.5 and cl.time is 1
	// virtual_base_time = 28.5 = 29.5 - 1
	cls.ghost_read.virtual_base_time = cls.ghost_read.past.ghost_time - cl.time;		
		
	// Construct the present.  Important because
	// this might otherwise get rejected.

	cls.ghost_read.current.ghost_time = GHOST_TIME_FOR(cl.time);
//	cls.ghost_read.current.cl_time = cl.time; // Is this necessary?
	cls.ghost_read.current.frac_final = 0; // Use past as-is
	cls.ghost_read.current.framepast = cls.ghost_read.past.frame;
	cls.ghost_read.current.framefuture = cls.ghost_read.future.frame;
	VectorCopy (cls.ghost_read.past.origin, cls.ghost_read.current.origin);
	VectorCopy (cls.ghost_read.past.angles, cls.ghost_read.current.angles);
	cls.ghost_read.last_cl_time = cl.time;

	if (CL_GhostVisible(cls.ghost_read.current.origin) == false)
	{
		// Frozen from start scenario
		memcpy (&cls.ghost_read.current_failed, &cls.ghost_read.current, sizeof(cls.ghost_read.current) );
		cls.ghost_read.out_of_sight = true;
		cls.ghost_read.out_of_sight_cltime = cl.time;

		// Accumulate waiting time and do not progress until this can be cleared.
		// Which means that current_failed must be rendered.

		// The reason we accumulate the wait in the following frame
		// Is our theoretical current_failed ate this timeslice
		// It is the NEXT timeslice that isn't going to happen

	}
	
	// We should be read to go.

	return true;
}





// Determine the current location and interpolation of the ghost
// Possibly pause the ghost
void sCL_Ghost_Current_Think (void)
{
	// Determine if we are paused?
	// How can we do that?
	// Try to update things and if it doesn't work out
	// Then don't and add a time slice into the accumulator

	// Simulate theoretical current state
	// If this state breaks our rules, restore everything and
	// Add some accumated time

	ghostfme_t	simcurrent;
	mghost_t	simpast;
	mghost_t	simfuture;
	double		temp_ghost_time; // Needs to become REAL
	double		temp_time_sliceg;
	double		temp_time_slicec;
	qboolean	temp_end_of_time = false;
	float		temp_frac;
	vec3_t		angle_delta;
	int			sim_record_map_current, i;

	// Check and see if the ghost is "dead".  If so, get out
	if (cls.ghost_read.end_of_time)
		return; // No theoretical future for the ghost ... he's done

	// Check for blocked future (a frame that would be rendered out-of-sight
	// Try to unblock.  If we can't, we must accumulate time.
	// If we can render it, still accumulate the wait, but then
	// Unblock.
	if (cls.ghost_read.out_of_sight)
	{
		// Accumulate the wait no matter what!
		cls.ghost_read.waiting_accumulator += cl.time - cls.ghost_read.out_of_sight_cltime;
		cls.ghost_read.last_cl_time = cl.time;
		// Can we see it now?
		if (CL_GhostVisible(cls.ghost_read.current_failed.origin) == true)
		{
			// We can see it!   Copy to current and remove block.
			cls.ghost_read.out_of_sight = false;
			cls.ghost_read.out_of_sight_cltime = 0;
			// We have a current frame and put the time slice into
			// The wait accumulator.  So get out!
			memcpy (&cls.ghost_read.current, &cls.ghost_read.current_failed, sizeof(cls.ghost_read.current_failed) );

//			Let a potential end of time resolve itself next frame.

			return;
		}
		
		// And ... we still can't see it ... so get out.
		// Current view didn't change at all.
		cls.ghost_read.out_of_sight_cltime = cl.time;
		cls.ghost_read.out_of_sight = true; // For clarity
		return;
	}

	// CONSTRUCT POSSIBLE GHOST CURRENT SCENARIO
	// We discard this IF player can't see it
	// If player can see it, we keep it.

	memset (&simcurrent, 0, sizeof(simcurrent));
	memcpy (&simpast, &cls.ghost_read.past, sizeof(simpast));
	memcpy (&simfuture, &cls.ghost_read.future, sizeof(simfuture));
	sim_record_map_current = cls.ghost_record_map_current;

	temp_ghost_time = GHOST_TIME_FOR(cl.time);
	
//	temp_time_slicec = cl.time - cls.ghost_read.last_cl_time;
//	temp_time_sliceg = GHOST_TIME_FOR(cl.time) - cls.ghost_read.last_ghost_time;
//	cls.ghost_read.last_ghost_time = temp_ghost_time;


	cls.ghost_read.last_cl_time = cl.time;

	// We need to eat the time slice.  How do we do that?
	// Progress as if everything is normal.
	
	// If we our "future" is stale, keep cycling through new futures
	// until we find one or must declare "end of time"
	while (temp_ghost_time > simfuture.ghost_time)
	{
		if (sim_record_map_current >= cls.ghost_record_map_end)
		{
			temp_end_of_time = true; // Set frac to 1
			break; // Can't move into the future any further
		}
		
		// Read a record.  Move future to past.  Read future in from file.
		// Move into the future
		memcpy (&simpast, &simfuture, sizeof(simpast));
		CL_GhostReadRecord (&simfuture, sim_record_map_current + 1, &sim_record_map_current);

//		break;
		// Repeat loop to make sure this "future" is ahead of
		// our ghost_time
	}

	// We have our "past" and "future" set.  Calculate "present"
	// Calculate "true frac time"
	 
	if (temp_end_of_time)
		temp_frac = 1.0f;
	else
	{
		qboolean isteleport = true;

		// TODO: VectorLength would be less messy
		// Only reason didn't use VectorLength is that cl_main.c doesn't use to test for teleport
		// If all 3 deltas are under 100, it isn't a teleport
		if ( (simpast.origin[0] - simfuture.origin[0]) < 100)
			if ( (simpast.origin[1] - simfuture.origin[1]) < 100)
				if ( (simpast.origin[2] - simfuture.origin[2]) < 100)
					isteleport = false;

		if (isteleport)
			temp_frac = 1.0f;
		else
		{
			double range = simfuture.ghost_time - simpast.ghost_time;
			double amount_into_range = temp_ghost_time - simpast.ghost_time;
		
			if (range < 0)
				range = range;
			temp_frac = amount_into_range / range;
			if (temp_frac > 1)
				temp_frac = 1;
		}
	}
	
	// Apply the frac = the past plus some percent of the different into the future.

	VectorSubtract (simfuture.origin, simpast.origin, angle_delta);

	simcurrent.origin[0] = simpast.origin[0] + angle_delta[0] * simcurrent.frac_final;
	simcurrent.origin[1] = simpast.origin[1] + angle_delta[1] * simcurrent.frac_final;
	simcurrent.origin[2] = simpast.origin[2] + angle_delta[2] * simcurrent.frac_final;

	VectorSubtract (simfuture.angles, simpast.angles, angle_delta);

	// always interpolate along the shortest path
	for (i = 0 ; i < 3 ; i++)
	{
		if (angle_delta[i] > 180)
			angle_delta[i] -= 360;
		else if (angle_delta[i] < -180)
			angle_delta[i] += 360;
	}
	
	
	simcurrent.angles[0] = simpast.angles[0] + angle_delta[0] * simcurrent.frac_final;
	simcurrent.angles[1] = simpast.angles[1] + angle_delta[1] * simcurrent.frac_final;
	simcurrent.angles[2] = simpast.angles[2] + angle_delta[2] * simcurrent.frac_final;
	simcurrent.framepast = simpast.frame;
	simcurrent.framefuture=simfuture.frame;
	simcurrent.frac_final= temp_frac;
	
	// Can we see this new "present"?  If so, all is ok.
	// Determine if this is a real or failed future ...
	if (CL_GhostVisible(simcurrent.origin) == false)
	{
		// A discarded future.
		// Preserve it in current_failed
		memcpy (&cls.ghost_read.current_failed, &simcurrent, sizeof(simcurrent));
		cls.ghost_read.out_of_sight_cltime = cl.time;
		cls.ghost_read.out_of_sight = true; // For clarity
		
		// Nothing else that occurred here was real ....
		// We have a stalled ghost with a wannabee future
		// This wannabee future ate the time.
		// And maybe it can used next frame
		return;
	}

	// If we are here, everything that happened is real.

//	Con_Printf ("frac final is %f with elapsed %f\n", (float)temp_frac, (float)(temp_ghost_time - simpast.ghost_time));

	memcpy (&cls.ghost_read.current, &simcurrent, sizeof(simcurrent));
	memcpy (&cls.ghost_read.past, &simpast, sizeof(simpast));
	memcpy (&cls.ghost_read.future, &simfuture, sizeof(simfuture));
	cls.ghost_record_map_current = sim_record_map_current;
	cls.ghost_read.end_of_time = temp_end_of_time;
}

void CL_Ghost_Think (void)
{
	if (!cls.using_ghost)
		return;

	if (!cl.time)
		return; // Wait until we have a time

 	// Initialize/Reset if everything is new
	if (sCL_Ghost_Init_Think ())
		return; // We got initialized.  No more pre-rendering calcs needed

	// If paused, nothing should need to be updated
	// Because cl.time doesn't change when paused.
	// Maybe sv.frozen should be looked at too
	if (cl.paused)
		return;

	// These things happen in a normal frame with elapsed time
	sCL_Ghost_Current_Think (); // Determine state of current frame

}

#endif // Baker change 
BTW ... this was actually a major in the arse to do. Far more complicated than I had anticipated.

It's also

1) Awesome as hell :D
2) Works with multimap demos just like I said :mrgreen:
3) About 40x more work writing down how exactly to make this work perfect ... what a clustermess of 10 clocks and endless minutia.
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: Racing Ghosts (As this unfolds)

Post by Baker »

See it in action:

http://www.youtube.com/watch?v=TAod_G2J ... e=youtu.be

/ Because I'm kinda going to sleep now, heh.
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 ..
gnounc
Posts: 428
Joined: Mon Apr 06, 2009 6:26 am

Re: Racing Ghosts (As this unfolds)

Post by gnounc »

Two questions.

One: can you have the ghost render blueishWhite and transparent?
and Two: can you have the ghost NOT wait for you? So you can race him?
and if so, will he walk through doors? (currently it looks like he waits for you to open them, judging by your video).
Baker
Posts: 3666
Joined: Tue Mar 14, 2006 5:15 am

Re: Racing Ghosts (As this unfolds)

Post by Baker »

Well this sleeping thing isn't working out so well ... too much Diet Dew overnight and I can't sleep ...

So ...
gnounc wrote:Two questions.

One: can you have the ghost render blueishWhite and transparent?
I had the player transparent originally. But alias models (q1 .mdl) as far as I know, there is no way to prevent, say, the axe hiding inside the player from showing so it looks a bit stupid.
and Two: can you have the ghost NOT wait for you? So you can race him?
I plan on doing that. But that's easy. Making the player pause at the right times was the hard part. Unless you are SDA, most people wouldn't have much use for that because odds are you are going to lose the ghost real quick.

What I haven't done is the thing they do in, say, other games where maybe the ghost lead time should be shown above the head when waiting like "+3.143 seconds" or "-5.4 seconds" to show how well you are keeping up. [That's easy too, obviously].
and if so, will he walk through doors? (currently it looks like he waits for you to open them, judging by your video).
If the ghost is going to go out of sight, he stops in his tracks.

My idea is that some of these hard single player maps where you don't know what to do, you do "ghostdemo marcher; map marcher" and have a "trainer" help you solve it --- so let's say you don't know what to do, you follow the ghost and "viola! Red keycard!" or "OMG! That's the secret door!". Or you can compete against the SDA guys (yeah right!).

/ Note: Really I wouldn't have done this qbism didn't start talking about it more. But I was getting bored with whatever code I was working on.
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 ..
r00k
Posts: 1111
Joined: Sat Nov 13, 2004 10:39 pm

Re: Racing Ghosts (As this unfolds)

Post by r00k »

That's pretty cool! This would be real handy to load up dm3 and ghost a match demo and spectate a match from Any angle..
Personally I think the SDA guys use a modified progs or server setting cause they seem to strafe jump faster than normal nq physics to me dunno
Spike
Posts: 2914
Joined: Fri Nov 05, 2004 3:12 am
Location: UK
Contact:

Re: Racing Ghosts (As this unfolds)

Post by Spike »

baker, regarding transparent models - draw the model with glColorMask(0,0,0,0); once, then draw it again with glColorMask(1,1,1,1);glDepthFunc(GL_EQUAL);
first time around you write just the depth (nothing actually visible at all), second time, you actually write colour, but only for the surfaces that actually match the nearest depth value written, resulting in a 'solid transparent' model. That's the theory, anyway.
Post Reply