/* Relay -- a tool to record and play Quake2 demos Copyright (C) 2000 Conor Davis This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. Conor Davis cedavis@planetquake.com */ #ifdef _WIN32 #include // for _mkdir() #else #include #include #include #include #endif #include #include #include "r_local.h" void SpawnEntities (char *mapname, char *entities, char *spawnpoint) { //int i; //edict_t *ent; char filename[MAX_QPATH]; char gamedir[MAX_OSPATH]; char path[MAX_OSPATH]; cvar_t *demofile; char *c, t[3] = "% "; time_t now; struct tm *ltime; char format[256][32]; DM2_Init(&dm2out); memset(areaportals, 0, sizeof(areaportals)); memset(format, 0, sizeof(format)); level_frame = 0; dm2out.current_frame = 0; dm2out.delta_frame = BASELINES_FRAME; dm2out.svd.version = 34; dm2out.svd.key = 0; dm2out.svd.isdemo = RECORD_RELAY; strncpy(dm2out.svd.game, gi.cvar("game", "", CVAR_LATCH|CVAR_SERVERINFO)->string, MAX_QPATH-1); dm2out.svd.player = 0; globals.SpawnEntities(mapname, entities, spawnpoint); strncpy(dm2out.svd.mapname, dm2out.configstrings[CS_NAME], MAX_QPATH-1); if (dm2out.svd.isdemo == RECORD_NETWORK || dm2out.svd.isdemo == RECORD_CLIENT) dm2out.maxclients = 1; else dm2out.maxclients = (int)maxclients->value; dm2out.players = Z_Malloc(dm2out.maxclients*sizeof(player_t)); // some configstrings are not supplied by the game dll sprintf(dm2out.configstrings[CS_MODELS+1], "maps/%s.bsp", mapname); strcpy(dm2out.configstrings[CS_AIRACCEL], gi.cvar("sv_airaccelerate", "0", 0)->string); demofile = gi.cvar("demofile", "", 0); if (demofile->string[0]) { time(&now); ltime = localtime(&now); // get common escape codes from strftime() for (c = "AaBbdHIjMmSWwYy"; *c; c++) { t[1] = *c; if (strftime(format[(int)*c], 32, t, ltime) == 0) format[(int)*c][0] = 0; } // add our own escape codes here strncpy(format['F'], dm2out.configstrings[CS_NAME], 31); strncpy(format['f'], mapname, 31); ExpandString(filename, sizeof(filename)-4, demofile->string, format); COM_DefaultExtension(filename, ".rla"); GamePath(gamedir, gi.cvar("basedir", ".", 0)->string, gi.cvar("game", "", 0)->string); // create the demos/ directory in case it doesn't exist sprintf(path, "%s/demos", gamedir); #ifdef _WIN32 _mkdir(path); #else mkdir(path, 0777); #endif AddPackDir(gamedir, PACK_FILES); sprintf(path, "%s/demos/%s", gamedir, filename); gi.dprintf("RELAY: Writing demo file: %s\n", path); outfile = pfopen(path, "wb"); if (!outfile) gi.dprintf("RELAY: Unable to open demo file for writing\n"); } else outfile = NULL; } void ClientThink (edict_t *ent, usercmd_t *cmd) { globals.ClientThink(ent, cmd); } qboolean ClientConnect (edict_t *ent, char *userinfo) { return globals.ClientConnect(ent, userinfo); } void ClientUserinfoChanged (edict_t *ent, char *userinfo) { globals.ClientUserinfoChanged(ent, userinfo); } void ClientDisconnect (edict_t *ent) { globals.ClientDisconnect(ent); } void ClientBegin (edict_t *ent) { globals.ClientBegin(ent); } void ClientCommand (edict_t *ent) { globals.ClientCommand(ent); } void WriteGame (char *filename, qboolean autosave) { globals.WriteGame(filename, autosave); } void ReadGame (char *filename) { globals.ReadGame(filename); } void WriteLevel (char *filename) { globals.WriteLevel(filename); } void ReadLevel (char *filename) { globals.ReadLevel(filename); } void InitGame (void) { globals.Init(); BlockInit(&tempblock, tempblock_buffer, sizeof(tempblock_buffer)); BlockInit(&reliable, reliable_buffer, sizeof(reliable_buffer)); BlockInit(&unreliable, unreliable_buffer, sizeof(unreliable_buffer)); maxclients = gi.cvar("maxclients", "1", CVAR_SERVERINFO|CVAR_LATCH); } void G_RunFrame (void) { state_t *to, *from; edict_t *ent; block_t out; char out_buffer[MAX_SVSLEN]; player_t *recipient; int i, current_index, delta_index; int area_count, connected_count; static player_state_t null_ps; BlockInit(&out, out_buffer, sizeof(out_buffer)); memset(&null_ps, 0, sizeof(null_ps)); globals.RunFrame(); level_frame++; // quake2 doesn't start recording until frame three (I think), // so neither shall we (I can rhyme!) if (level_frame < 3) return; if (dm2out.current_frame == 0) { // write baseline information for (i = 1; i < MAX_EDICTS; i++) { ent = NUM2EDICT(i); if (!ent->inuse) continue; if (!ent->s.modelindex && !ent->s.effects && !ent->s.sound && !ent->s.event) continue; dm2out.baselines.entities[i] = ent->s; dm2out.baselines.entities[i].number = i; } DM2_FillConfigstrings(dm2out.configstrings); DM2_WritePreFrame(&dm2out.svd, NULL, dm2out.configstrings, &dm2out.baselines, outfile); } dm2out.current_frame++; current_index = dm2out.current_frame & UPDATE_MASK; delta_index = dm2out.delta_frame & UPDATE_MASK; to = &dm2out.states[current_index]; if (dm2out.delta_frame == BASELINES_FRAME) from = &dm2out.baselines; else from = &dm2out.states[delta_index]; to->frame = dm2out.current_frame; for (i = 1; i < MAX_EDICTS; i++) { ent = NUM2EDICT(i); // determine if entity is visible // FIXME: determine whether ent has been gi.linked? if (!ent->inuse || ent->svflags & SVF_NOCLIENT || (!ent->s.modelindex && !ent->s.effects && !ent->s.sound && !ent->s.event)) { SETBIT(to->active, i, false); } else { SETBIT(to->active, i, true); to->entities[i] = ent->s; } to->entities[i].number = i; } if (dm2out.svd.isdemo == RECORD_RELAY) { memset(to->connected, 0, sizeof(to->connected)); for (i = 0; i < dm2out.maxclients; i++) { ent = NUM2EDICT(i+1); if (ent->inuse && ent->client) SETBIT(to->connected, i, true); } memcpy(to->areas, areaportals, sizeof(areaportals)); } BlockWrite(&out, reliable.buffer, reliable.writeoffset); // write SVC_FRAME message area_count = 0; connected_count = 0; if (dm2out.svd.isdemo == RECORD_RELAY) { for (i = 0; i < MAX_MAP_AREAPORTALS/8; i++) { if (to->areas[i] != from->areas[i]) area_count = i + 1; } for (i = 0; i < (dm2out.maxclients+7)/8; i++) { if (to->connected[i] != from->connected[i]) connected_count = i + 1; } } WriteByte(&out, SVC_FRAME); DM2_WriteFrame(&out, &dm2out.svd, to->frame, from->frame, area_count, to->areas, connected_count, to->connected); if (dm2out.svd.isdemo == RECORD_RELAY) { for (i = 0; i < dm2out.maxclients; i++) { ent = NUM2EDICT(i+1); if (!ent->inuse || !ent->client) continue; recipient = &dm2out.players[i]; recipient->ps[current_index] = ent->client->ps; WriteByte(&out, SVC_PLAYERINFO | MSG_UNICAST); WriteByte(&out, (byte)i); if (dm2out.delta_frame == BASELINES_FRAME) DM2_WritePS(&out, &recipient->ps[current_index], &null_ps); else DM2_WritePS(&out, &recipient->ps[current_index], &recipient->ps[delta_index]); } } // client demo stuff goes here... // write SVC_PACKETENTITIES message WriteByte(&out, SVC_PACKETENTITIES); DM2_WritePacketEntities(&out, to, from, &dm2out.baselines); BlockWrite(&out, unreliable.buffer, unreliable.writeoffset); if (outfile) DM2_WriteBlock(&out, outfile); BlockRewind(&reliable); BlockRewind(&unreliable); dm2out.delta_frame = dm2out.current_frame; } void ShutdownGame (void) { int len; globals.Shutdown(); UnloadGameModule(&proxydata); if (dm2out.players) { Z_Free(dm2out.players); dm2out.players = NULL; } if (outfile) { gi.dprintf("RELAY: Stopped recording\n"); len = LittleLong(0xffffffff); pfwrite(&len, 4, 1, outfile); pfclose(outfile); outfile = NULL; } Z_FreeAll(); } void ServerCommand (void) { globals.ServerCommand(); }