use crate::actor_state::ActorStateModeler;
use crate::{SubtrActorError, SubtrActorErrorVariant, SubtrActorResult};
use serde::{Deserialize, Serialize};
pub const CLIP_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ClipProvenance {
pub source_first_real_frame: usize,
pub source_last_real_frame: usize,
pub lead_in_frames: usize,
pub synthetic_frame_count: usize,
}
impl ClipProvenance {
pub fn clip_index_of(&self, source_frame: usize) -> Option<usize> {
if source_frame < self.source_first_real_frame || source_frame > self.source_last_real_frame
{
return None;
}
Some(self.synthetic_frame_count + (source_frame - self.source_first_real_frame))
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ReplayClip {
pub clip_version: u32,
pub net_version: Option<i32>,
pub major_version: i32,
pub minor_version: i32,
pub game_type: String,
pub objects: Vec<String>,
pub names: Vec<String>,
pub frames: Vec<boxcars::Frame>,
pub provenance: ClipProvenance,
}
impl ReplayClip {
pub fn to_replay(&self) -> boxcars::Replay {
boxcars::Replay {
header_size: 0,
header_crc: 0,
major_version: self.major_version,
minor_version: self.minor_version,
net_version: self.net_version,
game_type: self.game_type.clone(),
properties: Vec::new(),
content_size: 0,
content_crc: 0,
network_frames: Some(boxcars::NetworkFrames {
frames: self.frames.clone(),
}),
levels: Vec::new(),
keyframes: Vec::new(),
debug_info: Vec::new(),
tick_marks: Vec::new(),
packages: Vec::new(),
objects: self.objects.clone(),
names: self.names.clone(),
class_indices: Vec::new(),
net_cache: Vec::new(),
}
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
pub fn from_json(s: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(s)
}
}
fn attribute_is_transient_event(attribute: &boxcars::Attribute) -> bool {
matches!(
attribute,
boxcars::Attribute::Pickup(_)
| boxcars::Attribute::PickupNew(_)
| boxcars::Attribute::Demolish(_)
| boxcars::Attribute::DemolishFx(_)
| boxcars::Attribute::DemolishExtended(_)
| boxcars::Attribute::Explosion(_)
| boxcars::Attribute::ExtendedExplosion(_)
| boxcars::Attribute::StatEvent(_)
)
}
fn synthesize_keyframe(modeler: &ActorStateModeler, time: f32) -> boxcars::Frame {
let mut new_actors: Vec<boxcars::NewActor> = Vec::with_capacity(modeler.actor_states.len());
let mut updated_actors: Vec<boxcars::UpdatedAttribute> = Vec::new();
let mut actor_ids: Vec<&boxcars::ActorId> = modeler.actor_states.keys().collect();
actor_ids.sort_by_key(|id| id.0);
for actor_id in actor_ids {
let state = &modeler.actor_states[actor_id];
new_actors.push(boxcars::NewActor {
actor_id: *actor_id,
name_id: state.name_id,
object_id: state.object_id,
initial_trajectory: boxcars::Trajectory {
location: None,
rotation: None,
},
});
let mut object_ids: Vec<&boxcars::ObjectId> = state.attributes.keys().collect();
object_ids.sort_by_key(|id| id.0);
for object_id in object_ids {
let (attribute, _source_frame) = &state.attributes[object_id];
if attribute_is_transient_event(attribute) {
continue;
}
updated_actors.push(boxcars::UpdatedAttribute {
actor_id: *actor_id,
stream_id: boxcars::StreamId(object_id.0),
object_id: *object_id,
attribute: attribute.clone(),
});
}
}
boxcars::Frame {
time,
delta: 0.0,
new_actors,
deleted_actors: Vec::new(),
updated_actors,
}
}
pub fn clip_replay_range(
replay: &boxcars::Replay,
real_start: usize,
real_end: usize,
lead_in_frames: usize,
) -> SubtrActorResult<ReplayClip> {
let source_frames = &replay
.network_frames
.as_ref()
.ok_or_else(|| SubtrActorError::new(SubtrActorErrorVariant::NoNetworkFrames))?
.frames;
if real_start > real_end || real_end >= source_frames.len() {
return SubtrActorError::new_result(SubtrActorErrorVariant::FrameIndexOutOfBounds);
}
let mut modeler = ActorStateModeler::new();
for (index, frame) in source_frames.iter().enumerate().take(real_start) {
modeler.process_frame(frame, index)?;
}
let mut frames = Vec::with_capacity(real_end - real_start + 2);
let synthetic_frame_count = if real_start > 0 {
let keyframe_time = source_frames[real_start - 1].time;
frames.push(synthesize_keyframe(&modeler, keyframe_time));
1
} else {
0
};
frames.extend(source_frames[real_start..=real_end].iter().cloned());
Ok(ReplayClip {
clip_version: CLIP_VERSION,
net_version: replay.net_version,
major_version: replay.major_version,
minor_version: replay.minor_version,
game_type: replay.game_type.clone(),
objects: replay.objects.clone(),
names: replay.names.clone(),
frames,
provenance: ClipProvenance {
source_first_real_frame: real_start,
source_last_real_frame: real_end,
lead_in_frames,
synthetic_frame_count,
},
})
}
pub fn clip_replay_around(
replay: &boxcars::Replay,
region_start: usize,
region_end: usize,
lead_in: usize,
tail: usize,
) -> SubtrActorResult<ReplayClip> {
let frame_count = replay
.network_frames
.as_ref()
.ok_or_else(|| SubtrActorError::new(SubtrActorErrorVariant::NoNetworkFrames))?
.frames
.len();
let real_start = region_start.saturating_sub(lead_in);
let real_end = region_end
.saturating_add(tail)
.min(frame_count.saturating_sub(1));
let actual_lead_in = region_start - real_start;
clip_replay_range(replay, real_start, real_end, actual_lead_in)
}
pub fn frame_index_at_time(replay: &boxcars::Replay, time: f32) -> SubtrActorResult<usize> {
let frames = &replay
.network_frames
.as_ref()
.ok_or_else(|| SubtrActorError::new(SubtrActorErrorVariant::NoNetworkFrames))?
.frames;
Ok(frames
.iter()
.position(|frame| frame.time >= time)
.unwrap_or(frames.len().saturating_sub(1)))
}
pub fn clip_replay_around_times(
replay: &boxcars::Replay,
region_start_time: f32,
region_end_time: f32,
lead_in: usize,
tail: usize,
) -> SubtrActorResult<ReplayClip> {
let region_start = frame_index_at_time(replay, region_start_time)?;
let region_end = frame_index_at_time(replay, region_end_time)?;
clip_replay_around(replay, region_start, region_end, lead_in, tail)
}
#[cfg(test)]
#[path = "clip_tests.rs"]
mod tests;