#include #include "e_demorec.h" #include "e_memheap.h" #include "e_snapshot.h" #include "e_compression.h" #include "e_network.h" #include "e_engine.h" #include "e_if_other.h" static IOHANDLE record_file = 0; static const unsigned char header_marker[8] = {'T', 'W', 'D', 'E', 'M', 'O', 0, 1}; /* Record */ static int record_lasttickmarker = -1; static int record_lastkeyframe; static unsigned char record_lastsnapshotdata[MAX_SNAPSHOT_SIZE]; int demorec_isrecording() { return record_file != 0; } int demorec_record_start(const char *filename, const char *netversion, const char *map, int crc, const char *type) { DEMOREC_HEADER header; if(record_file) return -1; record_file = engine_openfile(filename, IOFLAG_WRITE); if(!record_file) { dbg_msg("demorec/record", "Unable to open '%s' for recording", filename); return -1; } /* write header */ mem_zero(&header, sizeof(header)); mem_copy(header.marker, header_marker, sizeof(header.marker)); str_copy(header.netversion, netversion, sizeof(header.netversion)); str_copy(header.map, map, sizeof(header.map)); str_copy(header.type, type, sizeof(header.type)); header.crc[0] = (crc>>24)&0xff; header.crc[1] = (crc>>16)&0xff; header.crc[2] = (crc>>8)&0xff; header.crc[3] = (crc)&0xff; io_write(record_file, &header, sizeof(header)); record_lastkeyframe = -1; record_lasttickmarker = -1; dbg_msg("demorec/record", "Recording to '%s'", filename); return 0; } /* Tickmarker 7 = Always set 6 = Keyframe flag 0-5 = Delta tick Normal 7 = Not set 5-6 = Type 0-4 = Size */ enum { CHUNKTYPEFLAG_TICKMARKER = 0x80, CHUNKTICKFLAG_KEYFRAME = 0x40, /* only when tickmarker is set*/ CHUNKMASK_TICK = 0x3f, CHUNKMASK_TYPE = 0x60, CHUNKMASK_SIZE = 0x1f, CHUNKTYPE_SNAPSHOT = 1, CHUNKTYPE_MESSAGE = 2, CHUNKTYPE_DELTA = 3, CHUNKFLAG_BIGSIZE = 0x10 }; static void demorec_record_write_tickmarker(int tick, int keyframe) { if(record_lasttickmarker == -1 || tick-record_lasttickmarker > 63 || keyframe) { unsigned char chunk[5]; chunk[0] = CHUNKTYPEFLAG_TICKMARKER; chunk[1] = (tick>>24)&0xff; chunk[2] = (tick>>16)&0xff; chunk[3] = (tick>>8)&0xff; chunk[4] = (tick)&0xff; if(keyframe) chunk[0] |= CHUNKTICKFLAG_KEYFRAME; io_write(record_file, chunk, sizeof(chunk)); } else { unsigned char chunk[1]; chunk[0] = CHUNKTYPEFLAG_TICKMARKER | (tick-record_lasttickmarker); io_write(record_file, chunk, sizeof(chunk)); } record_lasttickmarker = tick; } static void demorec_record_write(int type, const void *data, int size) { char buffer[64*1024]; char buffer2[64*1024]; unsigned char chunk[3]; if(!record_file) return; /* pad the data with 0 so we get an alignment of 4, else the compression won't work and miss some bytes */ mem_copy(buffer2, data, size); while(size&3) buffer2[size++] = 0; size = intpack_compress(buffer2, size, buffer); /* buffer2 -> buffer */ size = netcommon_compress(buffer, size, buffer2, sizeof(buffer2)); /* buffer -> buffer2 */ chunk[0] = ((type&0x3)<<5); if(size < 30) { chunk[0] |= size; io_write(record_file, chunk, 1); } else { if(size < 256) { chunk[0] |= 30; chunk[1] = size&0xff; io_write(record_file, chunk, 2); } else { chunk[0] |= 31; chunk[1] = size&0xff; chunk[2] = size>>8; io_write(record_file, chunk, 3); } } io_write(record_file, buffer2, size); } void demorec_record_snapshot(int tick, const void *data, int size) { if(record_lastkeyframe == -1 || (tick-record_lastkeyframe) > SERVER_TICK_SPEED*5) { /* write full tickmarker */ demorec_record_write_tickmarker(tick, 1); /* write snapshot */ demorec_record_write(CHUNKTYPE_SNAPSHOT, data, size); record_lastkeyframe = tick; mem_copy(record_lastsnapshotdata, data, size); } else { /* create delta, prepend tick */ char delta_data[MAX_SNAPSHOT_SIZE+sizeof(int)]; int delta_size; /* write tickmarker */ demorec_record_write_tickmarker(tick, 0); delta_size = snapshot_create_delta((SNAPSHOT*)record_lastsnapshotdata, (SNAPSHOT*)data, &delta_data); if(delta_size) { /* record delta */ demorec_record_write(CHUNKTYPE_DELTA, delta_data, delta_size); mem_copy(record_lastsnapshotdata, data, size); } } } void demorec_record_message(const void *data, int size) { demorec_record_write(CHUNKTYPE_MESSAGE, data, size); } int demorec_record_stop() { if(!record_file) return -1; dbg_msg("demorec/record", "Stopped recording"); io_close(record_file); record_file = 0; return 0; } /* Playback */ typedef struct KEYFRAME { long filepos; int tick; } KEYFRAME; typedef struct KEYFRAME_SEARCH { KEYFRAME frame; struct KEYFRAME_SEARCH *next; } KEYFRAME_SEARCH; static IOHANDLE play_file = 0; static DEMOREC_PLAYCALLBACK play_callback_snapshot = 0; static DEMOREC_PLAYCALLBACK play_callback_message = 0; static KEYFRAME *keyframes = 0; static DEMOREC_PLAYBACKINFO playbackinfo; static unsigned char playback_lastsnapshotdata[MAX_SNAPSHOT_SIZE]; static int playback_lastsnapshotdata_size = -1; const DEMOREC_PLAYBACKINFO *demorec_playback_info() { return &playbackinfo; } int demorec_isplaying() { return play_file != 0; } int demorec_playback_registercallbacks(DEMOREC_PLAYCALLBACK snapshot_cb, DEMOREC_PLAYCALLBACK message_cb) { play_callback_snapshot = snapshot_cb; play_callback_message = message_cb; return 0; } static int read_chunk_header(int *type, int *size, int *tick) { unsigned char chunk = 0; *size = 0; *type = 0; if(io_read(play_file, &chunk, sizeof(chunk)) != sizeof(chunk)) return -1; if(chunk&CHUNKTYPEFLAG_TICKMARKER) { /* decode tick marker */ int tickdelta = chunk&(CHUNKMASK_TICK); *type = chunk&(CHUNKTYPEFLAG_TICKMARKER|CHUNKTICKFLAG_KEYFRAME); if(tickdelta == 0) { unsigned char tickdata[4]; if(io_read(play_file, tickdata, sizeof(tickdata)) != sizeof(tickdata)) return -1; *tick = (tickdata[0]<<24) | (tickdata[1]<<16) | (tickdata[2]<<8) | tickdata[3]; } else { *tick += tickdelta; } } else { /* decode normal chunk */ *type = (chunk&CHUNKMASK_TYPE)>>5; *size = chunk&CHUNKMASK_SIZE; if(*size == 30) { unsigned char sizedata[1]; if(io_read(play_file, sizedata, sizeof(sizedata)) != sizeof(sizedata)) return -1; *size = sizedata[0]; } else if(*size == 31) { unsigned char sizedata[2]; if(io_read(play_file, sizedata, sizeof(sizedata)) != sizeof(sizedata)) return -1; *size = (sizedata[1]<<8) | sizedata[0]; } } return 0; } static void scan_file() { long start_pos; HEAP *heap = 0; KEYFRAME_SEARCH *first_key = 0; KEYFRAME_SEARCH *current_key = 0; /*DEMOREC_CHUNK chunk;*/ int chunk_size, chunk_type, chunk_tick = 0; int i; heap = memheap_create(); start_pos = io_tell(play_file); playbackinfo.seekable_points = 0; while(1) { long current_pos = io_tell(play_file); if(read_chunk_header(&chunk_type, &chunk_size, &chunk_tick)) break; /* read the chunk */ if(chunk_type&CHUNKTYPEFLAG_TICKMARKER) { if(chunk_type&CHUNKTICKFLAG_KEYFRAME) { KEYFRAME_SEARCH *key; /* save the position */ key = memheap_allocate(heap, sizeof(KEYFRAME_SEARCH)); key->frame.filepos = current_pos; key->frame.tick = chunk_tick; key->next = 0; if(current_key) current_key->next = key; if(!first_key) first_key = key; current_key = key; playbackinfo.seekable_points++; } if(playbackinfo.first_tick == -1) playbackinfo.first_tick = chunk_tick; playbackinfo.last_tick = chunk_tick; } else if(chunk_size) io_skip(play_file, chunk_size); } /* copy all the frames to an array instead for fast access */ keyframes = (KEYFRAME*)mem_alloc(playbackinfo.seekable_points*sizeof(KEYFRAME), 1); for(current_key = first_key, i = 0; current_key; current_key = current_key->next, i++) keyframes[i] = current_key->frame; /* destroy the temporary heap and seek back to the start */ memheap_destroy(heap); io_seek(play_file, start_pos, IOSEEK_START); } static void do_tick() { static char compresseddata[MAX_SNAPSHOT_SIZE]; static char decompressed[MAX_SNAPSHOT_SIZE]; static char data[MAX_SNAPSHOT_SIZE]; int chunk_size, chunk_type, chunk_tick; int data_size; int got_snapshot = 0; /* update ticks */ playbackinfo.previous_tick = playbackinfo.current_tick; playbackinfo.current_tick = playbackinfo.next_tick; chunk_tick = playbackinfo.current_tick; while(1) { if(read_chunk_header(&chunk_type, &chunk_size, &chunk_tick)) { /* stop on error or eof */ dbg_msg("demorec", "end of file"); demorec_playback_pause(); break; } /* read the chunk */ if(chunk_size) { if(io_read(play_file, compresseddata, chunk_size) != chunk_size) { /* stop on error or eof */ dbg_msg("demorec", "error reading chunk"); demorec_playback_stop(); break; } data_size = netcommon_decompress(compresseddata, chunk_size, decompressed, sizeof(decompressed)); if(data_size < 0) { /* stop on error or eof */ dbg_msg("demorec", "error during network decompression"); demorec_playback_stop(); break; } data_size = intpack_decompress(decompressed, data_size, data); if(data_size < 0) { dbg_msg("demorec", "error during intpack decompression"); demorec_playback_stop(); break; } } if(chunk_type == CHUNKTYPE_DELTA) { /* process delta snapshot */ static char newsnap[MAX_SNAPSHOT_SIZE]; got_snapshot = 1; data_size = snapshot_unpack_delta((SNAPSHOT*)playback_lastsnapshotdata, (SNAPSHOT*)newsnap, data, data_size); if(data_size >= 0) { if(play_callback_snapshot) play_callback_snapshot(newsnap, data_size); playback_lastsnapshotdata_size = data_size; mem_copy(playback_lastsnapshotdata, newsnap, data_size); } else dbg_msg("demorec", "error duing unpacking of delta, err=%d", data_size); } else if(chunk_type == CHUNKTYPE_SNAPSHOT) { /* process full snapshot */ got_snapshot = 1; playback_lastsnapshotdata_size = data_size; mem_copy(playback_lastsnapshotdata, data, data_size); if(play_callback_snapshot) play_callback_snapshot(data, data_size); } else { /* if there were no snapshots in this tick, replay the last one */ if(!got_snapshot && play_callback_snapshot && playback_lastsnapshotdata_size != -1) { got_snapshot = 1; play_callback_snapshot(playback_lastsnapshotdata, playback_lastsnapshotdata_size); } /* check the remaining types */ if(chunk_type&CHUNKTYPEFLAG_TICKMARKER) { playbackinfo.next_tick = chunk_tick; break; } else if(chunk_type == CHUNKTYPE_MESSAGE) { if(play_callback_message) play_callback_message(data, data_size); } } } } void demorec_playback_pause() { playbackinfo.paused = 1; } void demorec_playback_unpause() { if(playbackinfo.paused) { /*playbackinfo.start_tick = playbackinfo.current_tick; playbackinfo.start_time = time_get();*/ playbackinfo.paused = 0; } } int demorec_playback_load(const char *filename) { play_file = engine_openfile(filename, IOFLAG_READ); if(!play_file) { dbg_msg("demorec/playback", "could not open '%s'", filename); return -1; } /* clear the playback info */ mem_zero(&playbackinfo, sizeof(playbackinfo)); playbackinfo.first_tick = -1; playbackinfo.last_tick = -1; /*playbackinfo.start_tick = -1;*/ playbackinfo.next_tick = -1; playbackinfo.current_tick = -1; playbackinfo.previous_tick = -1; playbackinfo.speed = 1; playback_lastsnapshotdata_size = -1; /* read the header */ io_read(play_file, &playbackinfo.header, sizeof(playbackinfo.header)); if(mem_comp(playbackinfo.header.marker, header_marker, sizeof(header_marker)) != 0) { dbg_msg("demorec/playback", "'%s' is not a demo file", filename); io_close(play_file); play_file = 0; return -1; } /* scan the file for interessting points */ scan_file(); /* ready for playback */ return 0; } int demorec_playback_nextframe() { do_tick(); return demorec_isplaying(); } int demorec_playback_play() { /* fill in previous and next tick */ while(playbackinfo.previous_tick == -1 && demorec_isplaying()) do_tick(); /* set start info */ /*playbackinfo.start_tick = playbackinfo.previous_tick; playbackinfo.start_time = time_get();*/ playbackinfo.current_time = playbackinfo.previous_tick*time_freq()/SERVER_TICK_SPEED; playbackinfo.last_update = time_get(); return 0; } int demorec_playback_set(float percent) { int keyframe; int wanted_tick; if(!play_file) return -1; /* -5 because we have to have a current tick and previous tick when we do the playback */ wanted_tick = playbackinfo.first_tick + (int)((playbackinfo.last_tick-playbackinfo.first_tick)*percent) - 5; keyframe = (int)(playbackinfo.seekable_points*percent); if(keyframe < 0 || keyframe >= playbackinfo.seekable_points) return -1; /* get correct key frame */ if(keyframes[keyframe].tick < wanted_tick) while(keyframe < playbackinfo.seekable_points-1 && keyframes[keyframe].tick < wanted_tick) keyframe++; while(keyframe && keyframes[keyframe].tick > wanted_tick) keyframe--; /* seek to the correct keyframe */ io_seek(play_file, keyframes[keyframe].filepos, IOSEEK_START); /*playbackinfo.start_tick = -1;*/ playbackinfo.next_tick = -1; playbackinfo.current_tick = -1; playbackinfo.previous_tick = -1; /* playback everything until we hit our tick */ while(playbackinfo.previous_tick < wanted_tick) do_tick(); demorec_playback_play(); return 0; } void demorec_playback_setspeed(float speed) { playbackinfo.speed = speed; } int demorec_playback_update() { int64 now = time_get(); int64 deltatime = now-playbackinfo.last_update; playbackinfo.last_update = now; if(!demorec_isplaying()) return 0; if(playbackinfo.paused) { } else { int64 freq = time_freq(); playbackinfo.current_time += (int64)(deltatime*(double)playbackinfo.speed); while(1) { int64 curtick_start = (playbackinfo.current_tick)*freq/SERVER_TICK_SPEED; /* break if we are ready */ if(curtick_start > playbackinfo.current_time) break; /* do one more tick */ do_tick(); if(playbackinfo.paused) return 0; } /* update intratick */ { int64 curtick_start = (playbackinfo.current_tick)*freq/SERVER_TICK_SPEED; int64 prevtick_start = (playbackinfo.previous_tick)*freq/SERVER_TICK_SPEED; playbackinfo.intratick = (playbackinfo.current_time - prevtick_start) / (float)(curtick_start-prevtick_start); playbackinfo.ticktime = (playbackinfo.current_time - prevtick_start) / (float)freq; } if(playbackinfo.current_tick == playbackinfo.previous_tick || playbackinfo.current_tick == playbackinfo.next_tick) { dbg_msg("demorec/playback", "tick error prev=%d cur=%d next=%d", playbackinfo.previous_tick, playbackinfo.current_tick, playbackinfo.next_tick); } } return 0; } int demorec_playback_stop(const char *filename) { if(!play_file) return -1; dbg_msg("demorec/playback", "Stopped playback"); io_close(play_file); play_file = 0; mem_free(keyframes); keyframes = 0; return 0; }