#ifndef RT_REPLAY_H #define RT_REPLAY_H /* rt_replay.h * Input replay system for game development. * * This library is C99 compliant and builds without warnings with * (clang) -Wall -Wextra -Wpedantic. * * To integrate it into your codebase, define RT_REPLAY_IMPLEMENTATION * before including it in one source file. * By default, the library depends on stdint.h (for fixed size integers), * and on stdlib.h (for malloc & free). * You can provide replacements for these by defining macros as described * below. * * It's provided under the terms of the MIT license (found below) */ /* The MIT License (MIT) * Copyright © 2023 Kevin Trogant * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the “Software”), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE * USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* Fixed size integers. * * You can replace these by defining the macros below. * HOWEVER: Please note that this might break previous recordings if you * inadvertently end up changing the size of the integers. */ #if !defined(RTR_INT32) || !defined(RTR_UINT32) || !defined(RTR_UINT64) #include #endif #if !defined(RTR_INT32) #define RTR_INT32 int32_t #endif #if !defined(RTR_UINT32) #define RTR_UINT32 uint32_t #endif #if !defined(RTR_UINT64) #define RTR_UINT64 uint64_t #endif typedef RTR_INT32 rtr_int32; typedef RTR_UINT32 rtr_uint32; typedef RTR_UINT64 rtr_uint64; /* Allocators. * * You can replace the allocation functions by defining RTR_MALLOC * and RTR_FREE. Both take a void* context parameter, that is user defined * and can be used by your functions however you need. */ #if !defined(RTR_MALLOC) && !defined(RTR_FREE) #include #define RTR_MALLOC(ctx, sz) malloc(sz) #define RTR_FREE(ctx, ptr) free(ptr) #elif !defined(RTR_MALLOC) || !defined(RTR_FREE) #error "You must define RTR_MALLOC and RTR_FREE or none" #endif /* Memcpy & memset * * You can replace these by defining the macros below. */ #if !defined(RTR_MEMCPY) || !defined(RTR_MEMSET) #include #endif #if !defined(RTR_MEMCPY) #define RTR_MEMCPY(dst, src, n) memcpy((void*)(dst), (const void*)(src), (size_t)(n)) #endif #if !defined(RTR_MEMSET) #define RTR_MEMSET(dst, val, n) memset((void*)(dst), (int)(val), (size_t)(n)) #endif enum { RTR_SUCCESS, RTR_ERROR_OUT_OF_MEMORY, RTR_ERROR_NO_FRAME, RTR_ERROR_INVALID_RECORDING, }; enum { RTR_RECORDING_MODE_RECORD, RTR_RECORDING_MODE_PLAYBACK, }; enum { RTR_EVENT_TYPE_KEY, RTR_EVENT_TYPE_ANALOG, }; typedef struct rtr_record_frame rtr_record_frame; /* A recording keeps a list of record frames, where * each frame contains a timestamp and a number of input events. * * During replay, events are fed back to the game once the timestamp is reached. */ typedef struct { unsigned int mode; double timestamp; rtr_record_frame* frames; rtr_record_frame* last_frame; rtr_uint32 num_frames; void* ctx; } rtr_recording; typedef struct { rtr_uint32 type; union { struct { rtr_uint32 keycode; rtr_uint32 state; } key; struct { rtr_uint32 axis; double value; } analog; } data; } rtr_event; typedef struct { rtr_record_frame* frame; double timestamp; int is_finished; } rtr_playback_cursor; /* * Recording api */ void rtr_begin_recording(void* user_ctx, rtr_recording* recording); int rtr_end_recording(rtr_recording* recording, void** buffer, rtr_uint64* buffer_size); int rtr_frame(rtr_recording* recording, double delta); int rtr_add_key_event(rtr_recording* recording, rtr_uint32 key, rtr_uint32 state); int rtr_add_analog_event(rtr_recording* recording, rtr_uint32 axis, double value); /* * Playback api */ int rtr_open_recording(void* buffer, rtr_uint64 buffer_size, void* user_ctx, rtr_recording* recording, rtr_playback_cursor* cursor); void rtr_close_recording(rtr_recording* recording); rtr_playback_cursor rtr_create_cursor(rtr_recording* recording); void rtr_advance_playback(rtr_playback_cursor* cursor, double delta); int rtr_get_events(rtr_playback_cursor* cursor, unsigned int* num_events, rtr_event* events); #endif #ifdef RT_REPLAY_IMPLEMENTATION #undef RT_REPLAY_IMPLEMENTATION #include #define RTR_UNUSED(x) (void)(sizeof(x)) #define RTR_EVENTS_PER_FRAME 256 struct rtr_record_frame { double timestamp; rtr_record_frame* next; rtr_uint32 num_events; rtr_event events[RTR_EVENTS_PER_FRAME]; }; #define RTR_MAKE_VERSION(major, minor, patch) (\ (((major) & 0xfff) << 20) | \ (((minor) & 0xfff) << 8) | \ ((patch) & 0xff)) #define RTR_VERSION RTR_MAKE_VERSION(0, 1, 0) typedef struct { rtr_uint32 version; rtr_uint32 num_frames; } rtr_buffer_header; typedef struct { double rel_timestamp; rtr_uint32 num_events; rtr_event events[RTR_EVENTS_PER_FRAME]; } rtr_buffer_frame; void rtr_begin_recording(void* user_ctx, rtr_recording* recording) { rtr_recording rec; rec.mode = RTR_RECORDING_MODE_RECORD; rec.timestamp = 0.0; rec.num_frames = 0; rec.frames = NULL; rec.ctx = user_ctx; *recording = rec; } int rtr_end_recording(rtr_recording* recording, void** buffer, rtr_uint64* buffer_size) { if (recording->mode != RTR_RECORDING_MODE_RECORD) return RTR_ERROR_INVALID_RECORDING; /* Count the frames */ rtr_int32 unseen_frames = (rtr_int32)recording->num_frames; for (rtr_record_frame* frame = recording->frames; frame != NULL; frame = frame->next) { --unseen_frames; } if (unseen_frames != 0) return RTR_ERROR_INVALID_RECORDING; rtr_uint64 req_size = sizeof(rtr_buffer_header) + sizeof(rtr_buffer_frame) * recording->num_frames; char* buf = RTR_MALLOC(recording->ctx, req_size); if (!buf) return RTR_ERROR_OUT_OF_MEMORY; rtr_buffer_header* hdr = (rtr_buffer_header*)buf; hdr->version = RTR_VERSION; hdr->num_frames = recording->num_frames; rtr_buffer_frame* buf_frames = (rtr_buffer_frame*)(hdr + 1); int i = 0; rtr_record_frame* frame = recording->frames; while (frame) { buf_frames[i].rel_timestamp = frame->timestamp; buf_frames[i].num_events = frame->num_events; RTR_MEMCPY(buf_frames[i].events, frame->events, sizeof(rtr_event) * frame->num_events); rtr_record_frame* tmp = frame; frame = frame->next; RTR_FREE(recording->ctx, tmp); } RTR_MEMSET(recording, 0, sizeof(recording)); *buffer = (void*)buf; *buffer_size = req_size; return RTR_SUCCESS; } int rtr_frame(rtr_recording* recording, double delta) { rtr_record_frame* frame = (rtr_record_frame*)RTR_MALLOC(recording->ctx, sizeof(rtr_record_frame)); if (!frame) { return RTR_ERROR_OUT_OF_MEMORY; } recording->timestamp += delta; frame->num_events = 0; frame->next = NULL; frame->timestamp = recording->timestamp; if (recording->last_frame) recording->last_frame->next = frame; recording->last_frame = frame; ++recording->num_frames; return RTR_SUCCESS; } static int rtr_add_event(rtr_recording* recording, rtr_event event) { if (!recording->last_frame) return RTR_ERROR_NO_FRAME; rtr_record_frame* frame = recording->last_frame; if (frame->num_events < RTR_EVENTS_PER_FRAME) { frame->events[frame->num_events++] = event; return RTR_SUCCESS; } else { /* add a new "extended" frame with the same timestamp */ rtr_record_frame* ext_frame = (rtr_record_frame*)RTR_MALLOC(recording->ctx, sizeof(rtr_record_frame)); if (!ext_frame) return RTR_ERROR_OUT_OF_MEMORY; ext_frame->timestamp = frame->timestamp; ext_frame->next = NULL; ext_frame->events[0] = event; ext_frame->num_events = 1; frame->next = ext_frame; recording->last_frame = ext_frame; return RTR_SUCCESS; } } int rtr_add_key_event(rtr_recording* recording, rtr_uint32 key, rtr_uint32 state) { rtr_event event; event.type = RTR_EVENT_TYPE_KEY; event.data.key.keycode = key; event.data.key.state = state; return rtr_add_event(recording, event); } int rtr_add_analog_event(rtr_recording* recording, rtr_uint32 axis, double value) { rtr_event event; event.type = RTR_EVENT_TYPE_ANALOG; event.data.analog.axis = axis; event.data.analog.value = value; return rtr_add_event(recording, event); } int rtr_open_recording(void* buffer, rtr_uint64 buffer_size, void* user_ctx, rtr_recording* recording, rtr_playback_cursor* cursor) { rtr_buffer_header* hdr = (rtr_buffer_header*)buffer; if (hdr->version != RTR_VERSION) { /* because this is the only valid version, this check suffices. * in the future, we would check if the buffer contains a version * we recognize */ return RTR_ERROR_INVALID_RECORDING; } if (((hdr->num_frames * sizeof(rtr_buffer_frame)) + sizeof(hdr)) > buffer_size) { /* something went wrong */ return RTR_ERROR_INVALID_RECORDING; } rtr_buffer_frame* buf_frames = (rtr_buffer_frame*)(hdr + 1); rtr_record_frame* frames = RTR_MALLOC(user_ctx, sizeof(rtr_record_frame) * hdr->num_frames); for (unsigned int i = 0; i < hdr->num_frames; ++i) { frames[i].next = (i < hdr->num_frames - 1) ? &(frames[i + 1]) : NULL; frames[i].num_events = buf_frames[i].num_events; frames[i].timestamp = buf_frames[i].rel_timestamp; RTR_MEMCPY(frames[i].events, buf_frames[i].events, buf_frames[i].num_events * sizeof(rtr_event)); } recording->frames = frames; recording->last_frame = &frames[hdr->num_frames - 1]; recording->mode = RTR_RECORDING_MODE_PLAYBACK; recording->num_frames = hdr->num_frames; recording->timestamp = 0.0; recording->ctx = user_ctx; if (cursor) { cursor->frame = recording->frames; cursor->is_finished = 0; cursor->timestamp = 0.0; } return RTR_SUCCESS; } void rtr_close_recording(rtr_recording* recording) { if (recording->mode != RTR_RECORDING_MODE_PLAYBACK) return; RTR_FREE(recording->ctx, recording->frames); RTR_MEMSET(recording, 0, sizeof(recording)); } rtr_playback_cursor rtr_create_cursor(rtr_recording* recording) { rtr_playback_cursor cursor = {NULL, 0.0, 0}; if (recording->mode != RTR_RECORDING_MODE_PLAYBACK) return cursor; cursor.frame = recording->frames; cursor.is_finished = 0; cursor.timestamp = 0.0; return cursor; } void rtr_advance_playback(rtr_playback_cursor* cursor, double delta) { rtr_record_frame* next_frame = cursor->frame->next; /* skip extended frames */ while (next_frame && next_frame->timestamp == cursor->frame->timestamp) next_frame = next_frame->next; if (next_frame) { cursor->timestamp += delta; if (cursor->timestamp >= next_frame->timestamp) cursor->frame = next_frame; } else { cursor->is_finished = 1; } } int rtr_get_events(rtr_playback_cursor* cursor, unsigned int* num_events, rtr_event* events) { /* its possible that multiple frames are concatenated together */ unsigned int n = (unsigned int)cursor->frame->num_events; double ts = cursor->frame->timestamp; rtr_record_frame* ext_frame = cursor->frame->next; while (ext_frame->timestamp == ts) { n += ext_frame->num_events; ext_frame = ext_frame->next; } if (num_events) *num_events = n; if (events) { RTR_MEMCPY(events, cursor->frame->events, cursor->frame->num_events * sizeof(rtr_event)); rtr_uint32 i = cursor->frame->num_events; rtr_record_frame* ext_frame = cursor->frame->next; while (ext_frame->timestamp == ts) { RTR_MEMCPY(&events[i], ext_frame->events, ext_frame->num_events * sizeof(rtr_event)); i += ext_frame->num_events; ext_frame = ext_frame->next; } } return 1; } #endif