619 lines
18 KiB
C
619 lines
18 KiB
C
#include "pxl8_replay.h"
|
|
#include "pxl8_log.h"
|
|
#include "pxl8_sys.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
typedef struct pxl8_replay_chunk {
|
|
u8 type;
|
|
u32 size;
|
|
u8* data;
|
|
struct pxl8_replay_chunk* next;
|
|
} pxl8_replay_chunk;
|
|
|
|
typedef struct pxl8_keyframe_entry {
|
|
pxl8_keyframe keyframe;
|
|
pxl8_replay_chunk* input_deltas;
|
|
struct pxl8_keyframe_entry* next;
|
|
struct pxl8_keyframe_entry* prev;
|
|
} pxl8_keyframe_entry;
|
|
|
|
struct pxl8_replay {
|
|
FILE* file;
|
|
pxl8_replay_header header;
|
|
bool recording;
|
|
bool playing;
|
|
u32 current_frame;
|
|
|
|
pxl8_keyframe_entry* keyframes;
|
|
pxl8_keyframe_entry* current_keyframe;
|
|
u32 keyframe_count;
|
|
u32 max_keyframes;
|
|
|
|
pxl8_replay_chunk* pending_inputs;
|
|
pxl8_replay_chunk* pending_inputs_tail;
|
|
|
|
pxl8_replay_chunk* audio_events;
|
|
pxl8_replay_chunk* audio_events_tail;
|
|
};
|
|
|
|
static void pxl8_replay_chunk_free(pxl8_replay_chunk* chunk) {
|
|
while (chunk) {
|
|
pxl8_replay_chunk* next = chunk->next;
|
|
free(chunk->data);
|
|
free(chunk);
|
|
chunk = next;
|
|
}
|
|
}
|
|
|
|
static void pxl8_replay_keyframe_entry_free(pxl8_keyframe_entry* entry) {
|
|
while (entry) {
|
|
pxl8_keyframe_entry* next = entry->next;
|
|
pxl8_replay_chunk_free(entry->input_deltas);
|
|
free(entry);
|
|
entry = next;
|
|
}
|
|
}
|
|
|
|
pxl8_replay* pxl8_replay_create(const char* path, u32 keyframe_interval) {
|
|
FILE* f = fopen(path, "wb");
|
|
if (!f) {
|
|
pxl8_error("Failed to create replay file: %s", path);
|
|
return NULL;
|
|
}
|
|
|
|
pxl8_replay* r = calloc(1, sizeof(pxl8_replay));
|
|
if (!r) {
|
|
fclose(f);
|
|
return NULL;
|
|
}
|
|
|
|
r->file = f;
|
|
r->recording = true;
|
|
r->playing = false;
|
|
r->header.magic = PXL8_REPLAY_MAGIC;
|
|
r->header.version = PXL8_REPLAY_VERSION;
|
|
r->header.keyframe_interval = keyframe_interval;
|
|
|
|
fwrite(&r->header, sizeof(pxl8_replay_header), 1, f);
|
|
fflush(f);
|
|
|
|
pxl8_info("Created replay file: %s (keyframe interval: %u)", path, keyframe_interval);
|
|
return r;
|
|
}
|
|
|
|
pxl8_replay* pxl8_replay_create_buffer(u32 keyframe_interval, u32 max_keyframes) {
|
|
pxl8_replay* r = calloc(1, sizeof(pxl8_replay));
|
|
if (!r) return NULL;
|
|
|
|
r->recording = true;
|
|
r->playing = false;
|
|
r->max_keyframes = max_keyframes;
|
|
r->header.magic = PXL8_REPLAY_MAGIC;
|
|
r->header.version = PXL8_REPLAY_VERSION;
|
|
r->header.keyframe_interval = keyframe_interval;
|
|
|
|
pxl8_debug("Created replay buffer (keyframe interval: %u, max: %u)", keyframe_interval, max_keyframes);
|
|
return r;
|
|
}
|
|
|
|
pxl8_replay* pxl8_replay_open(const char* path) {
|
|
FILE* f = fopen(path, "rb");
|
|
if (!f) {
|
|
pxl8_error("Failed to open replay file: %s", path);
|
|
return NULL;
|
|
}
|
|
|
|
pxl8_replay_header header;
|
|
if (fread(&header, sizeof(header), 1, f) != 1) {
|
|
pxl8_error("Failed to read replay header");
|
|
fclose(f);
|
|
return NULL;
|
|
}
|
|
|
|
if (header.magic != PXL8_REPLAY_MAGIC) {
|
|
pxl8_error("Invalid replay magic: 0x%08X (expected 0x%08X)", header.magic, PXL8_REPLAY_MAGIC);
|
|
fclose(f);
|
|
return NULL;
|
|
}
|
|
|
|
if (header.version > PXL8_REPLAY_VERSION) {
|
|
pxl8_error("Unsupported replay version: %u (max supported: %u)", header.version, PXL8_REPLAY_VERSION);
|
|
fclose(f);
|
|
return NULL;
|
|
}
|
|
|
|
pxl8_replay* r = calloc(1, sizeof(pxl8_replay));
|
|
if (!r) {
|
|
fclose(f);
|
|
return NULL;
|
|
}
|
|
|
|
r->file = f;
|
|
r->header = header;
|
|
r->recording = false;
|
|
r->playing = true;
|
|
|
|
while (!feof(f)) {
|
|
u8 chunk_type;
|
|
if (fread(&chunk_type, 1, 1, f) != 1) break;
|
|
|
|
if (chunk_type == PXL8_REPLAY_CHUNK_END) break;
|
|
|
|
u8 size_bytes[3];
|
|
if (fread(size_bytes, 3, 1, f) != 1) break;
|
|
u32 size = size_bytes[0] | (size_bytes[1] << 8) | (size_bytes[2] << 16);
|
|
|
|
u8* data = malloc(size);
|
|
if (!data || fread(data, size, 1, f) != 1) {
|
|
free(data);
|
|
break;
|
|
}
|
|
|
|
if (chunk_type == PXL8_REPLAY_CHUNK_KEYFRAME) {
|
|
pxl8_keyframe_entry* entry = calloc(1, sizeof(pxl8_keyframe_entry));
|
|
if (entry && size >= sizeof(pxl8_keyframe)) {
|
|
memcpy(&entry->keyframe, data, sizeof(pxl8_keyframe));
|
|
entry->prev = r->current_keyframe;
|
|
if (r->current_keyframe) {
|
|
r->current_keyframe->next = entry;
|
|
}
|
|
r->current_keyframe = entry;
|
|
if (!r->keyframes) {
|
|
r->keyframes = entry;
|
|
}
|
|
r->keyframe_count++;
|
|
}
|
|
free(data);
|
|
} else if (chunk_type == PXL8_REPLAY_CHUNK_INPUT) {
|
|
if (r->current_keyframe) {
|
|
pxl8_replay_chunk* chunk = calloc(1, sizeof(pxl8_replay_chunk));
|
|
if (chunk) {
|
|
chunk->type = chunk_type;
|
|
chunk->size = size;
|
|
chunk->data = data;
|
|
data = NULL;
|
|
|
|
if (!r->current_keyframe->input_deltas) {
|
|
r->current_keyframe->input_deltas = chunk;
|
|
} else {
|
|
pxl8_replay_chunk* tail = r->current_keyframe->input_deltas;
|
|
while (tail->next) tail = tail->next;
|
|
tail->next = chunk;
|
|
}
|
|
}
|
|
}
|
|
free(data);
|
|
} else if (chunk_type == PXL8_REPLAY_CHUNK_AUDIO_EVENT) {
|
|
pxl8_replay_chunk* chunk = calloc(1, sizeof(pxl8_replay_chunk));
|
|
if (chunk) {
|
|
chunk->type = chunk_type;
|
|
chunk->size = size;
|
|
chunk->data = data;
|
|
data = NULL;
|
|
|
|
if (!r->audio_events) {
|
|
r->audio_events = chunk;
|
|
r->audio_events_tail = chunk;
|
|
} else {
|
|
r->audio_events_tail->next = chunk;
|
|
r->audio_events_tail = chunk;
|
|
}
|
|
}
|
|
free(data);
|
|
} else {
|
|
free(data);
|
|
}
|
|
}
|
|
|
|
r->current_keyframe = r->keyframes;
|
|
pxl8_info("Opened replay: %u frames, %u keyframes", r->header.total_frames, r->keyframe_count);
|
|
return r;
|
|
}
|
|
|
|
void pxl8_replay_destroy(pxl8_replay* r) {
|
|
if (!r) return;
|
|
|
|
if (r->file) {
|
|
if (r->recording) {
|
|
u8 end_chunk = PXL8_REPLAY_CHUNK_END;
|
|
fwrite(&end_chunk, 1, 1, r->file);
|
|
|
|
fseek(r->file, 0, SEEK_SET);
|
|
fwrite(&r->header, sizeof(pxl8_replay_header), 1, r->file);
|
|
}
|
|
fclose(r->file);
|
|
}
|
|
|
|
pxl8_replay_keyframe_entry_free(r->keyframes);
|
|
pxl8_replay_chunk_free(r->pending_inputs);
|
|
pxl8_replay_chunk_free(r->audio_events);
|
|
|
|
free(r);
|
|
}
|
|
|
|
bool pxl8_replay_is_recording(pxl8_replay* r) {
|
|
return r && r->recording;
|
|
}
|
|
|
|
bool pxl8_replay_is_playing(pxl8_replay* r) {
|
|
return r && r->playing;
|
|
}
|
|
|
|
u32 pxl8_replay_get_frame(pxl8_replay* r) {
|
|
return r ? r->current_frame : 0;
|
|
}
|
|
|
|
u32 pxl8_replay_get_total_frames(pxl8_replay* r) {
|
|
return r ? r->header.total_frames : 0;
|
|
}
|
|
|
|
static void write_chunk(FILE* f, u8 type, const void* data, u32 size) {
|
|
fwrite(&type, 1, 1, f);
|
|
u8 size_bytes[3] = {
|
|
(u8)(size & 0xFF),
|
|
(u8)((size >> 8) & 0xFF),
|
|
(u8)((size >> 16) & 0xFF)
|
|
};
|
|
fwrite(size_bytes, 3, 1, f);
|
|
fwrite(data, size, 1, f);
|
|
}
|
|
|
|
static void add_chunk_to_buffer(pxl8_replay* r, u8 type, const void* data, u32 size) {
|
|
pxl8_replay_chunk* chunk = calloc(1, sizeof(pxl8_replay_chunk));
|
|
if (!chunk) return;
|
|
|
|
chunk->type = type;
|
|
chunk->size = size;
|
|
chunk->data = malloc(size);
|
|
if (!chunk->data) {
|
|
free(chunk);
|
|
return;
|
|
}
|
|
memcpy(chunk->data, data, size);
|
|
|
|
if (!r->pending_inputs) {
|
|
r->pending_inputs = chunk;
|
|
r->pending_inputs_tail = chunk;
|
|
} else {
|
|
r->pending_inputs_tail->next = chunk;
|
|
r->pending_inputs_tail = chunk;
|
|
}
|
|
}
|
|
|
|
static void pack_keys(const bool* keys, u8* packed) {
|
|
memset(packed, 0, 32);
|
|
for (int i = 0; i < 256; i++) {
|
|
if (keys[i]) {
|
|
packed[i / 8] |= (1 << (i % 8));
|
|
}
|
|
}
|
|
}
|
|
|
|
static void unpack_keys(const u8* packed, bool* keys) {
|
|
for (int i = 0; i < 256; i++) {
|
|
keys[i] = (packed[i / 8] >> (i % 8)) & 1;
|
|
}
|
|
}
|
|
|
|
static u8 pack_mouse_buttons(const bool* buttons) {
|
|
return (buttons[0] ? 1 : 0) | (buttons[1] ? 2 : 0) | (buttons[2] ? 4 : 0);
|
|
}
|
|
|
|
static void unpack_mouse_buttons(u8 packed, bool* buttons) {
|
|
buttons[0] = (packed & 1) != 0;
|
|
buttons[1] = (packed & 2) != 0;
|
|
buttons[2] = (packed & 4) != 0;
|
|
}
|
|
|
|
void pxl8_replay_write_keyframe(pxl8_replay* r, u32 frame, f32 time, pxl8_rng* rng, pxl8_input_state* input) {
|
|
if (!r || !r->recording) return;
|
|
|
|
pxl8_keyframe kf = {0};
|
|
kf.frame_number = frame;
|
|
kf.time = time;
|
|
kf.rng_state = rng ? rng->state : 0;
|
|
|
|
if (input) {
|
|
pack_keys(input->keys_down, kf.keys_down);
|
|
kf.mouse_buttons = pack_mouse_buttons(input->mouse_buttons_down);
|
|
kf.mouse_x = (i16)input->mouse_x;
|
|
kf.mouse_y = (i16)input->mouse_y;
|
|
}
|
|
|
|
if (r->file) {
|
|
write_chunk(r->file, PXL8_REPLAY_CHUNK_KEYFRAME, &kf, sizeof(kf));
|
|
fflush(r->file);
|
|
} else {
|
|
pxl8_keyframe_entry* entry = calloc(1, sizeof(pxl8_keyframe_entry));
|
|
if (!entry) return;
|
|
|
|
entry->keyframe = kf;
|
|
entry->input_deltas = r->pending_inputs;
|
|
r->pending_inputs = NULL;
|
|
r->pending_inputs_tail = NULL;
|
|
|
|
if (r->keyframe_count >= r->max_keyframes && r->keyframes) {
|
|
pxl8_keyframe_entry* oldest = r->keyframes;
|
|
r->keyframes = oldest->next;
|
|
if (r->keyframes) {
|
|
r->keyframes->prev = NULL;
|
|
}
|
|
pxl8_replay_chunk_free(oldest->input_deltas);
|
|
free(oldest);
|
|
r->keyframe_count--;
|
|
}
|
|
|
|
entry->prev = r->current_keyframe;
|
|
if (r->current_keyframe) {
|
|
r->current_keyframe->next = entry;
|
|
}
|
|
r->current_keyframe = entry;
|
|
if (!r->keyframes) {
|
|
r->keyframes = entry;
|
|
}
|
|
r->keyframe_count++;
|
|
}
|
|
|
|
r->current_frame = frame;
|
|
r->header.total_frames = frame;
|
|
}
|
|
|
|
void pxl8_replay_write_input(pxl8_replay* r, u32 frame, pxl8_input_state* prev, pxl8_input_state* curr) {
|
|
if (!r || !r->recording || !prev || !curr) return;
|
|
|
|
u8 buffer[256];
|
|
u32 offset = 0;
|
|
|
|
buffer[offset++] = (u8)(frame & 0xFF);
|
|
buffer[offset++] = (u8)((frame >> 8) & 0xFF);
|
|
buffer[offset++] = (u8)((frame >> 16) & 0xFF);
|
|
buffer[offset++] = (u8)((frame >> 24) & 0xFF);
|
|
|
|
u8 key_event_count = 0;
|
|
u8 key_events[64];
|
|
u32 key_offset = 0;
|
|
|
|
for (int i = 0; i < 256 && key_event_count < 32; i++) {
|
|
if (prev->keys_down[i] != curr->keys_down[i]) {
|
|
key_events[key_offset++] = (u8)i;
|
|
key_events[key_offset++] = curr->keys_down[i] ? 1 : 0;
|
|
key_event_count++;
|
|
}
|
|
}
|
|
|
|
buffer[offset++] = key_event_count;
|
|
memcpy(buffer + offset, key_events, key_offset);
|
|
offset += key_offset;
|
|
|
|
u8 prev_mouse_btns = pack_mouse_buttons(prev->mouse_buttons_down);
|
|
u8 curr_mouse_btns = pack_mouse_buttons(curr->mouse_buttons_down);
|
|
|
|
u8 mouse_flags = 0;
|
|
if (curr->mouse_x != prev->mouse_x || curr->mouse_y != prev->mouse_y) {
|
|
mouse_flags |= 0x01;
|
|
}
|
|
if (curr_mouse_btns != prev_mouse_btns) {
|
|
mouse_flags |= 0x02;
|
|
}
|
|
if (curr->mouse_wheel_x != 0 || curr->mouse_wheel_y != 0) {
|
|
mouse_flags |= 0x04;
|
|
}
|
|
|
|
buffer[offset++] = mouse_flags;
|
|
|
|
if (mouse_flags & 0x01) {
|
|
i16 dx = (i16)(curr->mouse_x - prev->mouse_x);
|
|
i16 dy = (i16)(curr->mouse_y - prev->mouse_y);
|
|
buffer[offset++] = (u8)(dx & 0xFF);
|
|
buffer[offset++] = (u8)((dx >> 8) & 0xFF);
|
|
buffer[offset++] = (u8)(dy & 0xFF);
|
|
buffer[offset++] = (u8)((dy >> 8) & 0xFF);
|
|
}
|
|
|
|
if (mouse_flags & 0x02) {
|
|
buffer[offset++] = curr_mouse_btns;
|
|
}
|
|
|
|
if (mouse_flags & 0x04) {
|
|
buffer[offset++] = (i8)curr->mouse_wheel_x;
|
|
buffer[offset++] = (i8)curr->mouse_wheel_y;
|
|
}
|
|
|
|
if (r->file) {
|
|
write_chunk(r->file, PXL8_REPLAY_CHUNK_INPUT, buffer, offset);
|
|
} else {
|
|
add_chunk_to_buffer(r, PXL8_REPLAY_CHUNK_INPUT, buffer, offset);
|
|
}
|
|
|
|
r->current_frame = frame;
|
|
r->header.total_frames = frame;
|
|
}
|
|
|
|
void pxl8_replay_write_audio_event(pxl8_replay* r, u32 frame, u8 event_type, u8 context_id, u8 note, f32 volume) {
|
|
if (!r || !r->recording) return;
|
|
|
|
pxl8_audio_event evt = {
|
|
.frame_number = frame,
|
|
.event_type = event_type,
|
|
.context_id = context_id,
|
|
.note = note,
|
|
.volume = volume
|
|
};
|
|
|
|
if (r->file) {
|
|
write_chunk(r->file, PXL8_REPLAY_CHUNK_AUDIO_EVENT, &evt, sizeof(evt));
|
|
} else {
|
|
pxl8_replay_chunk* chunk = calloc(1, sizeof(pxl8_replay_chunk));
|
|
if (!chunk) return;
|
|
|
|
chunk->type = PXL8_REPLAY_CHUNK_AUDIO_EVENT;
|
|
chunk->size = sizeof(evt);
|
|
chunk->data = malloc(sizeof(evt));
|
|
if (!chunk->data) {
|
|
free(chunk);
|
|
return;
|
|
}
|
|
memcpy(chunk->data, &evt, sizeof(evt));
|
|
|
|
if (!r->audio_events) {
|
|
r->audio_events = chunk;
|
|
r->audio_events_tail = chunk;
|
|
} else {
|
|
r->audio_events_tail->next = chunk;
|
|
r->audio_events_tail = chunk;
|
|
}
|
|
}
|
|
}
|
|
|
|
void pxl8_replay_close(pxl8_replay* r) {
|
|
pxl8_replay_destroy(r);
|
|
}
|
|
|
|
bool pxl8_replay_seek_frame(pxl8_replay* r, u32 frame) {
|
|
if (!r || !r->playing) return false;
|
|
|
|
pxl8_keyframe_entry* target = NULL;
|
|
for (pxl8_keyframe_entry* e = r->keyframes; e; e = e->next) {
|
|
if (e->keyframe.frame_number <= frame) {
|
|
target = e;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!target) return false;
|
|
|
|
r->current_keyframe = target;
|
|
r->current_frame = target->keyframe.frame_number;
|
|
return true;
|
|
}
|
|
|
|
bool pxl8_replay_read_frame(pxl8_replay* r, pxl8_input_state* input_out) {
|
|
if (!r || !r->playing || !r->current_keyframe) return false;
|
|
|
|
u32 target_frame = r->current_frame + 1;
|
|
|
|
if (target_frame > r->header.total_frames) return false;
|
|
|
|
pxl8_replay_chunk* delta = r->current_keyframe->input_deltas;
|
|
while (delta) {
|
|
if (delta->size >= 5) {
|
|
u32 delta_frame = delta->data[0] | (delta->data[1] << 8) | (delta->data[2] << 16) | (delta->data[3] << 24);
|
|
if (delta_frame == target_frame) {
|
|
u32 offset = 4;
|
|
u8 key_event_count = delta->data[offset++];
|
|
|
|
for (u8 i = 0; i < key_event_count && offset + 1 < delta->size; i++) {
|
|
u8 scancode = delta->data[offset++];
|
|
u8 pressed = delta->data[offset++];
|
|
input_out->keys_down[scancode] = pressed != 0;
|
|
}
|
|
|
|
if (offset < delta->size) {
|
|
u8 mouse_flags = delta->data[offset++];
|
|
|
|
if (mouse_flags & 0x01 && offset + 3 < delta->size) {
|
|
i16 dx = (i16)(delta->data[offset] | (delta->data[offset + 1] << 8));
|
|
i16 dy = (i16)(delta->data[offset + 2] | (delta->data[offset + 3] << 8));
|
|
offset += 4;
|
|
input_out->mouse_x += dx;
|
|
input_out->mouse_y += dy;
|
|
}
|
|
|
|
if (mouse_flags & 0x02 && offset < delta->size) {
|
|
u8 mouse_btns = delta->data[offset++];
|
|
unpack_mouse_buttons(mouse_btns, input_out->mouse_buttons_down);
|
|
}
|
|
|
|
if (mouse_flags & 0x04 && offset + 1 < delta->size) {
|
|
input_out->mouse_wheel_x = (i8)delta->data[offset++];
|
|
input_out->mouse_wheel_y = (i8)delta->data[offset++];
|
|
}
|
|
}
|
|
|
|
r->current_frame = target_frame;
|
|
return true;
|
|
}
|
|
}
|
|
delta = delta->next;
|
|
}
|
|
|
|
if (r->current_keyframe->next && r->current_keyframe->next->keyframe.frame_number <= target_frame) {
|
|
r->current_keyframe = r->current_keyframe->next;
|
|
}
|
|
|
|
r->current_frame = target_frame;
|
|
return true;
|
|
}
|
|
|
|
bool pxl8_replay_get_keyframe(pxl8_replay* r, u32 frame, pxl8_keyframe* out) {
|
|
if (!r || !out) return false;
|
|
|
|
pxl8_keyframe_entry* target = NULL;
|
|
for (pxl8_keyframe_entry* e = r->keyframes; e; e = e->next) {
|
|
if (e->keyframe.frame_number <= frame) {
|
|
target = e;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!target) return false;
|
|
|
|
*out = target->keyframe;
|
|
return true;
|
|
}
|
|
|
|
void pxl8_replay_apply_keyframe(pxl8_replay* r, pxl8_keyframe* kf, pxl8_rng* rng, pxl8_input_state* input) {
|
|
if (!kf) return;
|
|
|
|
if (rng) {
|
|
rng->state = kf->rng_state;
|
|
}
|
|
|
|
if (input) {
|
|
unpack_keys(kf->keys_down, input->keys_down);
|
|
unpack_mouse_buttons(kf->mouse_buttons, input->mouse_buttons_down);
|
|
input->mouse_x = kf->mouse_x;
|
|
input->mouse_y = kf->mouse_y;
|
|
}
|
|
|
|
if (r) {
|
|
r->current_frame = kf->frame_number;
|
|
}
|
|
}
|
|
|
|
bool pxl8_replay_export(pxl8_replay* r, const char* path) {
|
|
if (!r || !r->keyframes) return false;
|
|
|
|
FILE* f = fopen(path, "wb");
|
|
if (!f) {
|
|
pxl8_error("Failed to create export file: %s", path);
|
|
return false;
|
|
}
|
|
|
|
pxl8_replay_header header = r->header;
|
|
fwrite(&header, sizeof(header), 1, f);
|
|
|
|
for (pxl8_keyframe_entry* e = r->keyframes; e; e = e->next) {
|
|
write_chunk(f, PXL8_REPLAY_CHUNK_KEYFRAME, &e->keyframe, sizeof(pxl8_keyframe));
|
|
|
|
for (pxl8_replay_chunk* c = e->input_deltas; c; c = c->next) {
|
|
write_chunk(f, c->type, c->data, c->size);
|
|
}
|
|
}
|
|
|
|
for (pxl8_replay_chunk* c = r->audio_events; c; c = c->next) {
|
|
write_chunk(f, c->type, c->data, c->size);
|
|
}
|
|
|
|
u8 end_chunk = PXL8_REPLAY_CHUNK_END;
|
|
fwrite(&end_chunk, 1, 1, f);
|
|
|
|
fclose(f);
|
|
pxl8_info("Exported replay to: %s (%u frames)", path, header.total_frames);
|
|
return true;
|
|
}
|