feat: add replaymod compatible recorder

This commit is contained in:
2025-03-09 03:40:22 -04:00
parent 1a2af8b7aa
commit caec5fa7f8
7 changed files with 430 additions and 7 deletions

88
src/replay/mod.rs Normal file
View File

@@ -0,0 +1,88 @@
pub mod plugin;
use crate::build_info;
use anyhow::Result;
use azalea::{
buf::AzaleaWriteVar,
prelude::Resource,
protocol::packets::{PROTOCOL_VERSION, ProtocolPacket, VERSION_NAME},
};
use serde_json::json;
use std::{
fs::File,
io::Write,
time::{SystemTime, UNIX_EPOCH},
};
use zip::{ZipWriter, write::SimpleFileOptions};
#[derive(Resource)]
pub struct Recorder {
zip_writer: ZipWriter<File>,
start_time: u128,
server: String,
ignore_compression: bool,
}
impl Recorder {
pub fn new(path: String, server: String, ignore_compression: bool) -> Result<Self> {
let mut zip_writer = ZipWriter::new(
File::options()
.write(true)
.create(true)
.truncate(true)
.open(path)?,
);
zip_writer.start_file("recording.tmcpr", SimpleFileOptions::default())?;
Ok(Self {
zip_writer,
start_time: SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis(),
server,
ignore_compression,
})
}
pub fn finish(mut self) -> Result<()> {
self.zip_writer
.start_file("metaData.json", SimpleFileOptions::default())?;
self.zip_writer.write_all(
json!({
"singleplayer": false,
"serverName": self.server,
"duration": SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() - self.start_time,
"date": self.start_time,
"mcversion": VERSION_NAME,
"fileFormat": "MCPR",
"fileFormatVersion": 14,
"protocol": PROTOCOL_VERSION,
"generator": build_info::version_formatted(),
})
.to_string()
.as_bytes(),
)?;
self.zip_writer.finish()?;
Ok(())
}
fn get_timestamp(&self) -> Result<[u8; 4]> {
Ok(TryInto::<u32>::try_into(
SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() - self.start_time,
)?
.to_be_bytes())
}
fn save_raw_packet(&mut self, raw_packet: &[u8]) -> Result<()> {
let mut data = Vec::from(self.get_timestamp()?);
data.extend(TryInto::<u32>::try_into(raw_packet.len())?.to_be_bytes());
data.extend(raw_packet);
self.zip_writer.write_all(&data)?;
Ok(())
}
fn save_packet<T: ProtocolPacket>(&mut self, packet: &T) -> Result<()> {
let mut raw_packet = Vec::new();
packet.id().azalea_write_var(&mut raw_packet)?;
packet.write(&mut raw_packet)?;
self.save_raw_packet(&raw_packet)
}
}

76
src/replay/plugin.rs Normal file
View File

@@ -0,0 +1,76 @@
use super::Recorder;
use azalea::{
ecs::{event::EventReader, system::Query},
packet_handling::{
configuration::ConfigurationEvent,
game::send_packet_events,
login::{LoginPacketEvent, process_packet_events},
},
protocol::packets::login::ClientboundLoginPacket,
raw_connection::RawConnection,
};
use bevy_app::{First, Plugin};
use bevy_ecs::{schedule::IntoSystemConfigs, system::ResMut};
use log::error;
use parking_lot::Mutex;
use std::sync::Arc;
pub struct RecordPlugin {
pub recorder: Arc<Mutex<Option<Recorder>>>,
}
impl Plugin for RecordPlugin {
fn build(&self, app: &mut bevy_app::App) {
if let Some(recorder) = self.recorder.lock().take() {
app.insert_resource(recorder);
}
app.add_systems(First, record_login_packets.before(process_packet_events))
.add_systems(First, record_configuration_packets)
.add_systems(First, record_game_packets.before(send_packet_events));
}
}
fn record_login_packets(
recorder: Option<ResMut<Recorder>>,
mut events: EventReader<LoginPacketEvent>,
) {
if let Some(mut recorder) = recorder {
for event in events.read() {
if recorder.ignore_compression
&& let ClientboundLoginPacket::LoginCompression(_) = *event.packet
{
continue;
}
if let Err(error) = recorder.save_packet(event.packet.as_ref()) {
error!("failed to record login packet: {error:?}");
}
}
}
}
fn record_configuration_packets(
recorder: Option<ResMut<Recorder>>,
mut events: EventReader<ConfigurationEvent>,
) {
if let Some(mut recorder) = recorder {
for event in events.read() {
if let Err(error) = recorder.save_packet(&event.packet) {
error!("failed to record configuration packet: {error:?}");
}
}
}
}
fn record_game_packets(recorder: Option<ResMut<Recorder>>, query: Query<&RawConnection>) {
if let Some(mut recorder) = recorder
&& let Ok(raw_conn) = query.get_single()
{
let queue = raw_conn.incoming_packet_queue();
for raw_packet in queue.lock().iter() {
if let Err(error) = recorder.save_raw_packet(raw_packet) {
error!("failed to record game packet: {error:?}");
}
}
}
}