1
0
mirror of https://git.ngram.ca/OpenJam/rc-servers synced 2026-08-23 23:08:52 +00:00

Add basic server-only player harness

This commit is contained in:
NG (Graham)
2025-08-16 21:32:14 -04:00
parent 94d247f8f4
commit 05bf4a097a
20 changed files with 304 additions and 21 deletions

View File

@@ -19,6 +19,7 @@ byteserde = "0.6.2"
chrono.workspace = true
atomic_float = "1.1"
num-quaternion.workspace = true
rand.workspace = true
#literustlib_server = { version = "0.1", path = "../../LiteRustLib/server" }
#literustlib = { version = "0.1", path = "../../LiteRustLib" }

View File

@@ -3,6 +3,7 @@ pub struct GameMatches {
routing: std::collections::HashMap<i32, String>, // user id to game guid
mode_configs: oj_rc_core::data::game_mode::GameModeConfigs,
map_configs: std::collections::HashMap<String, oj_rc_core::persist::config::MapConfig>,
fake_players: Vec<oj_rc_core::persist::config::FakePlayer>,
}
impl GameMatches {
@@ -15,6 +16,7 @@ impl GameMatches {
.into_iter()
.map(|(map, conf)| (oj_rc_core::data::game_mode::GameMap::from_persist(map).as_str().to_owned(), conf))
.collect(),
fake_players: <oj_rc_core::persist::config::ConfigImpl as oj_rc_core::ConfigProvider<()>>::fake_players(conf),
}
}
@@ -24,6 +26,26 @@ impl GameMatches {
tx
}
fn build_player_emulator(&self, emu: oj_rc_core::persist::config::ClientEmulator) -> Box<dyn super::fake::FakeUser> {
match emu {
oj_rc_core::persist::config::ClientEmulator::Experiment => Box::new(super::fake::ExperimentalPlayer::new()),
}
}
fn build_fake_players(&self, players: &[oj_rc_core::persist::user::PlayerDescriptor]) -> std::collections::HashMap<u8, Box<dyn super::fake::FakeUser>> {
let mut fake_player_i = 0;
let mut fakes = std::collections::HashMap::with_capacity(self.fake_players.len());
for player in players.iter() {
if fake_player_i >= self.fake_players.len() { break; }
if player.user_id.is_none() {
let fake = self.build_player_emulator(self.fake_players[fake_player_i].implementation);
fakes.insert(player.player_id, fake);
fake_player_i += 1;
}
}
fakes
}
async fn start_new_match_engine(&self, user: &Box<dyn oj_rc_core::persist::user::MultiplayerUser + Send + Sync + 'static>, guid: &str) -> Result<tokio::sync::mpsc::Sender<super::GameMessage>, oj_rc_core::persist::user::MultiplayerError> {
let game_info = user.game_info(guid).await?
.ok_or_else(|| oj_rc_core::persist::user::MultiplayerError {
@@ -41,8 +63,10 @@ impl GameMatches {
});
let players = user.game_players(guid).await?;
if players.is_empty() {
log::warn!("No players found to game {}, loading may not work correctly", guid);
log::warn!("No players found for game {}, loading may not work correctly", guid);
}
let fakes = self.build_fake_players(&players);
let fakes_handler = super::fake::Handler::start(fakes, players.clone()).await;
match game_info.mode {
oj_rc_core::data::game_mode::GameMode::SuddenDeath => {
let inner = super::modes::EliminationLogic::new(&self.mode_configs.elimination, &map_config);
@@ -51,6 +75,7 @@ impl GameMatches {
map_config,
players,
inner,
fakes_handler,
);
Ok(engine.spawn())
}

View File

@@ -0,0 +1,99 @@
use rand::Rng;
use byteserde::ser_heap::ByteSerializeHeap;
pub struct ExperimentalPlayer {
me: tokio::sync::RwLock<Option<oj_rc_core::persist::user::PlayerDescriptor>>,
is_complete: std::sync::Arc<std::sync::atomic::AtomicBool>,
}
impl ExperimentalPlayer {
pub fn new() -> Self {
Self {
me: tokio::sync::RwLock::new(None),
is_complete: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
}
}
}
#[async_trait::async_trait]
impl super::FakeUser for ExperimentalPlayer {
async fn on_init(&self, descriptors: &Vec<oj_rc_core::persist::user::PlayerDescriptor>, player_id: u8) {
if let Some(my_desc) = descriptors.iter().filter(|x| x.player_id == player_id).next() {
*self.me.write().await = Some(my_desc.to_owned());
}
}
async fn on_ready(&self, real_players: &std::collections::HashMap<u8, crate::matches::generic::UserSender>) {
let movement_rx = real_players.values().map(|x| x.to_owned()).collect();
let is_complete = self.is_complete.clone();
let player_id = self.me.read().await.as_ref().unwrap().player_id;
tokio::task::spawn(erratic_behaviour(movement_rx, is_complete, player_id));
}
async fn on_end(&self) {
self.is_complete.store(true, std::sync::atomic::Ordering::Relaxed);
}
}
fn random_map_coord(rng: &mut rand::rngs::ThreadRng) -> f32 {
const CONVERSION_FACTOR: f32 = 300.0 / (i16::MAX as f32);
let int = rng.random::<i16>();
(int as f32) * CONVERSION_FACTOR
}
fn random_height_coord(rng: &mut rand::rngs::ThreadRng) -> f32 {
const CONVERSION_FACTOR: f32 = 50.0 / (i16::MAX as f32);
let int = rng.random::<i16>();
((int as f32) * CONVERSION_FACTOR) + 10.0
}
async fn erratic_behaviour(send_to: Vec<crate::matches::generic::UserSender>, is_complete: std::sync::Arc<std::sync::atomic::AtomicBool>, player_id: u8) {
const SLEEP_PERIOD: std::time::Duration = std::time::Duration::from_secs(1);
let mut fake_timestamp = 42.0;
while !is_complete.load(std::sync::atomic::Ordering::Relaxed) {
let (pos_x, pos_y, pos_z) = {
let mut rng = rand::rng();
let pos_x: f32 = random_map_coord(&mut rng);
let pos_y: f32 = random_height_coord(&mut rng);
let pos_z: f32 = random_map_coord(&mut rng);
(pos_x, pos_y, pos_z)
};
let motion = rlnl::machine_motion::MachineMotion {
last_sent_seconds_a: 1.0,
last_sent_seconds_b: 2.0,
timestamp: fake_timestamp,
player_id,
target_point: rlnl::types::CompressedVec3::from((50.0, 50.0, 50.0)),
rb_state: rlnl::machine_motion::RigidBodyState {
rb_pos_rot: rlnl::types::PosQuatPair {
pos: rlnl::types::CompressedVec3::from((pos_x, pos_y, pos_z)),
rot: rlnl::types::CompressedQuat { x: 0, y: 0, z: 0 },
},
angular_velocity: rlnl::types::CompressedVec3::from((0.0, 0.0, 0.0)),
center_of_mass: rlnl::types::CompressedVec3::from((1.0, 1.0, 1.0)),
},
};
let mut ser = byteserde::ser_heap::ByteSerializerHeap::default();
if let Err(e) = motion.byte_serialize_heap(&mut ser) {
log::error!("Failed to serialize motion data from experimental fake player: {}", e);
} else {
let motion_data = bytes::Bytes::copy_from_slice(ser.as_slice());
for conn in send_to.iter() {
send_motion_data_to(conn, motion_data.clone()).await;
}
log::info!("Moved experimental bot to ({}, {}, {})", pos_x, pos_y, pos_z);
}
fake_timestamp += 1.0;
tokio::time::sleep(SLEEP_PERIOD).await;
}
is_complete.store(false, std::sync::atomic::Ordering::Relaxed);
}
async fn send_motion_data_to(to: &crate::matches::generic::UserSender, motion: bytes::Bytes) {
crate::events::log_lnl_send_failure(to.sender.send_data(crate::handler::EventData {
message_ty: crate::data::MessageType::RobotMotion,
variant: 0,
data_size: motion.len() as _,
data: motion,
}, literustlib::packet::Property::Unreliable, &to.connection).await);
}

View File

@@ -0,0 +1,63 @@
enum Message {
Ready {
real_players: std::collections::HashMap<u8, crate::matches::generic::UserSender>,
},
Stop,
}
pub struct Handler {
tx: tokio::sync::mpsc::UnboundedSender<Message>,
}
impl Handler {
pub async fn start(fakes: std::collections::HashMap<u8, Box<dyn super::FakeUser + 'static>>, descriptors: Vec<oj_rc_core::persist::user::PlayerDescriptor>) -> Self {
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
tokio::task::spawn(handler_loop(rx, fakes, descriptors));
Self {
tx,
}
}
pub fn on_ready(&self, real_players: std::collections::HashMap<u8, crate::matches::generic::UserSender>) {
Self::log_failure(self.tx.send(Message::Ready { real_players }));
}
pub fn stop(&self) {
Self::log_failure(self.tx.send(Message::Stop));
}
#[inline]
fn log_failure<T>(res: Result<(), tokio::sync::mpsc::error::SendError<T>>) {
if let Err(_e) = res {
log::warn!("Failed to send message to fake user handler thread");
}
}
}
async fn handler_loop(mut rx: tokio::sync::mpsc::UnboundedReceiver<Message>, players: std::collections::HashMap<u8, Box<dyn super::FakeUser + 'static>>, descriptors: Vec<oj_rc_core::persist::user::PlayerDescriptor>) {
for player in descriptors.iter() {
if player.user_id.is_none() {
let player_id = player.player_id;
if let Some(fake_player) = players.get(&player_id) {
fake_player.on_init(&descriptors, player_id).await;
} else {
log::warn!("No fake user implementation for player {}", player_id);
}
}
}
while let Some(msg) = rx.recv().await {
match msg {
Message::Ready { real_players } => {
for fake in players.values() {
fake.on_ready(&real_players).await;
}
},
Message::Stop => {
for fake in players.values() {
fake.on_end().await;
}
break;
},
}
}
}

View File

@@ -0,0 +1,8 @@
mod handler;
pub use handler::Handler;
mod traits;
pub use traits::FakeUser;
mod experimental;
pub use experimental::ExperimentalPlayer;

View File

@@ -0,0 +1,7 @@
#[async_trait::async_trait]
pub trait FakeUser: Send + Sync {
async fn on_init(&self, descriptors: &Vec<oj_rc_core::persist::user::PlayerDescriptor>, player_id: u8);
async fn on_ready(&self, real_players: &std::collections::HashMap<u8, crate::matches::generic::UserSender>);
//fn on_damage(&self, data: &rlnl::events::ingame::DestroyCubesFull);
async fn on_end(&self);
}

View File

@@ -194,6 +194,7 @@ pub(super) struct GenericGamemodeEngine<L: super::CustomGameLogic> {
pub players_info: std::sync::Arc<Vec<oj_rc_core::persist::user::PlayerDescriptor>>,
pub custom_logic_handler: L,
pub fake_users: std::collections::HashMap<u8, FakeUser>,
pub fakes_handler: super::fake::Handler,
}
impl <L: super::CustomGameLogic> GenericGamemodeEngine<L> {
@@ -204,7 +205,8 @@ impl <L: super::CustomGameLogic> GenericGamemodeEngine<L> {
game: oj_rc_core::persist::user::GameDescriptor,
map: oj_rc_core::persist::config::MapConfig,
players: Vec<oj_rc_core::persist::user::PlayerDescriptor>,
custom: L
custom: L,
fakes_handler: super::fake::Handler,
) -> Self {
let fake_users = players.iter()
@@ -221,6 +223,7 @@ impl <L: super::CustomGameLogic> GenericGamemodeEngine<L> {
players_info: std::sync::Arc::new(players),
custom_logic_handler: custom,
fake_users,
fakes_handler,
}
}
@@ -470,7 +473,7 @@ impl <L: super::CustomGameLogic> GenericGamemodeEngine<L> {
}
for fake in self.fake_users.values() {
let event = rlnl::events::loading::LoadingProgress {
user_name: rlnl::types::BinaryWriterString("FakeUser".to_owned()),
user_name: rlnl::types::BinaryWriterString(fake.descriptor.public_id.clone()),
progress: (fake.state.progress.load(std::sync::atomic::Ordering::Relaxed) as f32) / 100.0,
};
crate::events::log_lnl_send_failure(sender.send_data(
@@ -555,6 +558,11 @@ impl <L: super::CustomGameLogic> GenericGamemodeEngine<L> {
let player_count = self.players_info.iter().filter(|x| x.user_id.is_some()).count();
log::info!("All players ({}) are ready for game {}", player_count, self.game_guid());
tokio::time::sleep(Self::END_OF_SYNC_DELAY).await;
self.fakes_handler.on_ready(
self.users.read().await.iter()
.map(|(id, real_player)| (*id, real_player.connection.clone()))
.collect()
);
let game_start = chrono::Utc::now() + Self::COUNTDOWN_DURATION;
if self.custom_logic_handler.on_countdown_start(&self, game_start).await {
let mut senders = Vec::new();
@@ -812,6 +820,7 @@ impl <L: super::CustomGameLogic> GenericGamemodeEngine<L> {
}
}
}
self.fakes_handler.stop();
log::info!("Game {} has exited", self.game_guid());
}

View File

@@ -16,4 +16,6 @@ pub mod modes;
mod timer;
pub(self) mod fake;
pub const CHANNEL_BOUND: usize = 16;