Ive given another thought about the alternative hybrid approach, and I think it's possible to overcome some of the limitations:
1)
- make a replay facility server-side, by feeding real clients some fake packets containing the messages of a demo. This can easily be done by just reading a message from a demo and using SV_SendMessageToClient(client, &msg). What is harder is to prevent the server from sending conflicting data to clients (because the server will continue to send them the valid gamestate), without crashing the clients nor the server.
Easy to be done: at replaying, we just have to use SV_SendMessageToClient(client, &msg). Both are already recorded in TheDoctor's patch.
And to avoid the interference of the engine, we simple impose a condition in SV_SendClientSnapshot() to call SV_SendMessageToClient() only if we are not replaying a demo (because if were replaying a demo, we will directly call SV_SendMessageToClient() in our demo loop).
2)
- instead of recording every client's flux separately, we could save them all in the same file and add an event "End of frame" which would allow to replay these packets at the right time (similarly to Amanieu's approach, see my patch).
As I said, TheDoctor's patch already records the messages, you then just have at each end of frame (at the end of SV_Frame(), see my current implementation) to record an "_endFrame" marker. This way, all messages will just be recorded when they are made by the engine, and we just add a marker so that at replaying we can know that we are changing frame.
To replay, we make a simple loop to process and send all demo messages (for the current frame) until we hit the _endFrame marker, then we exit the demo playback loop until next SV_Frame() iteration.
3)
- hook clientCommand and when the client send a "follow" or "follownext" command, the server would send them the flux of the democlient associated with the number. But it seems that FollowCycle (when you click your mouse button) is not catched this way, so it would be needed to edit the gamecode at least to allow the hooking of FollowCycle.
This one is tricky but should be possible and fairly easy to implement if done right.
Cmd_FollowCycle_f is in fact triggered in two way: either with follownext or followprev clientCommands, or either by just clicking the mouse button (which can be seen in the usercmd_t packets).
// game commands
{ "follow", CMD_NOTEAM, Cmd_Follow_f },
{ "follownext", CMD_NOTEAM, Cmd_FollowCycle_f },
{ "followprev", CMD_NOTEAM, Cmd_FollowCycle_f },
/*
=================
SpectatorThink
=================
*/
void SpectatorThink( gentity_t *ent, usercmd_t *ucmd ) {
...
client->oldbuttons = client->buttons;
client->buttons = ucmd->buttons;
if ( ( client->buttons & BUTTON_ATTACK ) && ! ( client->oldbuttons & BUTTON_ATTACK ) ) {
Cmd_FollowCycle_f( ent );
}
...
}
This is a standard behaviour for all ioquake3 based game, so we can safely assume that clicking the button when spectator will always result in changing the viewpoint.
The usercmd_t packets are also available from the engine, and also we can access playerState_t->pm_flags.
Here's the idea is to basically to reproduce the behaviour of SpectatorThink() and Cmd_FollowCycle_f() in the engine instead of the gamecode, by just detecting when BUTTON_ATTACK is pressed while the client is a spectator (and it should always be in a demo playback, so this is optional).
But there will be some challenges:
- SpectatorThink(): detecting if the client want to change the viewpoint and maintain an array that says for all spectators who they are watching. This should be done at the beginning of any frame prior to sending demo messages to assign all spectators to the right democlient in case of a change.
- Cmd_FollowCycle_f(): to find the next democlient in the list, to achieve that we would need to maintain an array of the democlients indexes at any time. A non-empty configstring could be used to check that the democlient was active?
Note that the list would be needed anyway for "follow" to work, because we must make sure that we have a democlients that can be watched with the specified id.
4)
- to allow freeflying, spectators that are known to be freeflying right now would be sent just the entities states and player states. A gamecode hook would also be needed here, because I don't know of a way to know the state of a spectator (and who he is following) completely server-side, but from the gamecode it's easily available.
This one is HARD. I don't know even if it's possible to achieve it. Maybe it will be impossible, or just too easy to believe (because there's a chance that after step 3 is done, freeflying may be totally managed by the gamecode without having to change anything in the engine).
I explain: the problem here is not to allow for freeflying, as I wrote we just have to send the entities delta messages (update messages), and the client will just draw them and will be able to freefly around. The problem is to
detect when the spectator wants to freefly.
To detail further, let me show you the code that allows to switch from following to freeflying (which is a call to StopFollowing()):
/*
=================
SpectatorThink
=================
*/
void SpectatorThink( gentity_t *ent, usercmd_t *ucmd ) {
...
if ( ( client->buttons & BUTTON_USE_HOLDABLE ) && ! ( client->oldbuttons & BUTTON_USE_HOLDABLE ) ) {
if ( ( g_gametype.integer == GT_ELIMINATION || g_gametype.integer == GT_CTF_ELIMINATION) &&
g_elimination_lockspectator.integer>1 &&
ent->client->sess.sessionTeam != TEAM_SPECTATOR ) {
return;
}
StopFollowing(ent);
}
...
}
At first, you could think "well, why can't we use the same trick as in step 3 and detect when BUTTON_USE_HOLDABLE is pressed?"
Answer: because this is not a native behaviour of ioquake3. Ioquake3 does not even have any way to go to freefly once you've followed someone, except from joining the Spectator team again. So this behaviour was introduced in OpenArena.
Also, mods can further modify this behaviour. For example, ExcessivePlus does not go from Following directly to Freefly, but from Following -> Third-person view (Track Cam) -> TV View -> Freeflying.
Anyway, we could still implement this feature this way, by detecting if BUTTON_USE_HOLDABLE is pressed and then just sending him the entities states (and stopping to send him the democlient messages he was previously following), but this is not satisfying if you want to be faithful to the original way mods have made their spectating work. But this is without any doubt the easiest way to do it.
Now let's take a look on another way to do that. Let's see the code of StopFollowing():
/*
=================
StopFollowing
If the client being followed leaves the game, or you just want to drop
to free floating spectator mode
=================
*/
void StopFollowing( gentity_t *ent ) {
ent->client->ps.persistant[ PERS_TEAM ] = TEAM_SPECTATOR;
ent->client->sess.sessionTeam = TEAM_SPECTATOR;
ent->client->sess.spectatorState = SPECTATOR_FREE;
ent->client->ps.pm_flags &= ~PMF_FOLLOW;
ent->r.svFlags &= ~SVF_BOT;
ent->client->ps.clientNum = ent - g_entities;
}
As you can see, there's a playerState_t pm_flags PMF_FOLLOW that gets removed. Also the ps.clientNum gets changed too.
So a good way to do this would maybe be by checking the state of the pm_flags PMF_FOLLOW, but this would require that the gamecode would set it, which is not certain since the democlients do not exist on the server with this implementation. So the PMF_FOLLOW flag may not even be set.
Maybe we can then set PMF_FOLLOW flag by ourselves at demo playback in our functions made at step 3, and then the gamecode would be now able to call StopFollowing()? This is something to be explored...
--------------------------
So I now think this implementation could be pretty easily done. However, it may not be possible to get a satisfyingly working implementation of the Freeflying feature, but it would be posible to allow for follow switching (multiview) without any problem for sure.
I really do think this path should be explored. I currently have no time to dedicate to make a prototype of this implementation, but if someone out there is interested, I can give a hand and lend some of my knowledge.