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.)
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: 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.
3) About 40x more work writing down how exactly to make this work perfect ... what a clustermess of 10 clocks and endless minutia.