diff --git a/rc_multiplayer/src/handlers/gamemode_specific.rs b/rc_multiplayer/src/handlers/gamemode_specific.rs index a0c3522..7c65dd4 100644 --- a/rc_multiplayer/src/handlers/gamemode_specific.rs +++ b/rc_multiplayer/src/handlers/gamemode_specific.rs @@ -28,12 +28,11 @@ impl >, user: &crate::UserData, _sender: &std::sync::Arc>) { if let Some(user_info) = user.user().await { - crate::events::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::RebroadcastRlnl { - skip_user_id: user_info.user_id(), + crate::events::log_channel_send_failure(self.msg_router.send(crate::matches::GameMessage::CustomLogicRlnl { + user_id: user_info.user_id(), event: self.event, - event_in: Self::CODE, property: self.property, - data: Some(Box::new(data)), + data: Box::new(data), }).await); } else { log::error!("Failed to send gamemode specifc event {:?} for user (no auth!)", self.event); diff --git a/rc_multiplayer/src/matches/generic.rs b/rc_multiplayer/src/matches/generic.rs index 4812914..183e4d1 100644 --- a/rc_multiplayer/src/matches/generic.rs +++ b/rc_multiplayer/src/matches/generic.rs @@ -107,6 +107,8 @@ pub(super) struct UserData { pub received_healed: std::sync::atomic::AtomicU32, pub cubes: std::sync::atomic::AtomicU32, pub received_cubes: std::sync::atomic::AtomicU32, // damage taken + //pub segments_captured: std::sync::atomic::AtomicU32, // TODO + pub crystals: std::sync::atomic::AtomicU32, // crystals destroyed } impl UserData { @@ -119,6 +121,8 @@ impl UserData { received_healed: std::sync::atomic::AtomicU32::new(0), cubes: std::sync::atomic::AtomicU32::new(0), received_cubes: std::sync::atomic::AtomicU32::new(0), + //segments_captured: std::sync::atomic::AtomicU32::new(0), + crystals: std::sync::atomic::AtomicU32::new(0), } } @@ -127,6 +131,7 @@ impl UserData { + self.assists.load(std::sync::atomic::Ordering::Relaxed) * 100 + self.healed.load(std::sync::atomic::Ordering::Relaxed) + self.cubes.load(std::sync::atomic::Ordering::Relaxed) + + self.crystals.load(std::sync::atomic::Ordering::Relaxed) * 25 } pub(super) fn get_generic_packet(&self, player_id: u8, stat: rlnl::types::IngameStatId, delta: Option) -> rlnl::events::ingame::UpdateGameStats { @@ -138,6 +143,7 @@ impl UserData { rlnl::types::IngameStatId::KillAssist => (self.assists.load(std::sync::atomic::Ordering::Relaxed), 100), rlnl::types::IngameStatId::HealCubes => (self.assists.load(std::sync::atomic::Ordering::SeqCst), 1), rlnl::types::IngameStatId::RobotDestroyed => (self.deaths.load(std::sync::atomic::Ordering::Relaxed), 0), + rlnl::types::IngameStatId::DestroyedProtoniumCubes => (self.deaths.load(std::sync::atomic::Ordering::Relaxed), 25), s => panic!("Cannot generate game stat {:?}", s) }; rlnl::events::ingame::UpdateGameStats { diff --git a/rc_multiplayer/src/matches/modes/battle_arena.rs b/rc_multiplayer/src/matches/modes/battle_arena.rs index eab913b..74b8ef6 100644 --- a/rc_multiplayer/src/matches/modes/battle_arena.rs +++ b/rc_multiplayer/src/matches/modes/battle_arena.rs @@ -435,6 +435,11 @@ impl BaseInfo { * (max_health as f32)).ceil() as u32 } + #[inline] + fn max_index(&self) -> usize { + self.cube_index.load(std::sync::atomic::Ordering::SeqCst).ceil() as usize + } + fn first_damaged(&self, old_index: usize, max_health: u32) -> Option { for i in 0..old_index { let health = self.calculate_crystal_health(i, max_health); @@ -444,6 +449,68 @@ impl BaseInfo { } None } + + /// returns whether crystal exists + fn damage_crystal_at_pos(&self, pos: rlnl::types::Byte3, damage: i32, crystals: &[oj_rc_core::cubes::CubeLocationInfo], max_health: u32) -> bool { + if let Some((index, _)) = crystals.iter().enumerate().find(|(_i, crystal)| crystal.x == pos.x && crystal.y == pos.y && crystal.z == pos.z) { + let max_index = self.max_index(); + if index > max_index { return false; } + self.damage_crystal(index, damage, max_health); + true + } else { + false + } + } + + /// returns whether crystal was destroyed + fn damage_crystal(&self, i: usize, damage: i32, max_health: u32) -> bool { + let damage_byte = (((damage as f32) / (max_health as f32)) * (u8::MAX as f32)).ceil() as u8; + let old_health = self.crystals_healths[i].fetch_sub(damage_byte, std::sync::atomic::Ordering::Relaxed); + log::debug!("Crystal {} damaged (was {}, now {}, delta {})", i, old_health, old_health.saturating_sub(damage_byte), damage_byte); + if old_health < damage_byte { + // guarantee underflow behaviour + self.crystals_healths[i].store(0, std::sync::atomic::Ordering::Relaxed); + } + old_health != 0 && old_health >= damage_byte + } + + /// returns whether crystal exists + fn destroy_crystal_at_pos(&self, pos: rlnl::types::Byte3, crystals: &[oj_rc_core::cubes::CubeLocationInfo]) -> bool { + if let Some((index, _)) = crystals.iter().enumerate().find(|(_i, crystal)| crystal.x == pos.x && crystal.y == pos.y && crystal.z == pos.z) { + let max_index = self.max_index(); + if index > max_index { return false; } + self.destroy_crystal(index); + true + } else { + false + } + } + + fn destroy_crystal(&self, i: usize) { + self.crystals_healths[i].store(0, std::sync::atomic::Ordering::Relaxed); + } + + fn generate_full_base_heal(&self, base_id: u8, max_health: u32, crystals: &[oj_rc_core::cubes::CubeLocationInfo]) -> rlnl::events::HealedCubes { + let max_index = self.max_index(); + let target_crystals: Vec = (0..crystals.len()) + .take_while(|&i| i <= max_index) + .filter(|&i| self.calculate_crystal_health(i, max_health) != 0) + .map(|i| { + let target_loc = &crystals[i]; + rlnl::types::HitCubeInfo { + pos: rlnl::types::Byte3 { x: target_loc.x, y: target_loc.y, z: target_loc.z, }, + damage: max_health as i32, + } + }) + .collect(); + rlnl::events::HealedCubes { + healed_machine: base_id as u16, + type_performing_healing: rlnl::types::TargetType::TeamBase, + target_type: rlnl::types::TargetType::TeamBase, + num_healed_cubes: target_crystals.len() as _, + hit_cubes: target_crystals, + } + } } enum WinMode { @@ -659,7 +726,62 @@ impl BattleArenaLogic { &player.connection, ).await); } + } + async fn do_team_base_stealing(&self, cube_damage: &rlnl::events::ingame::DestroyCubeNoEffect, generic: &crate::matches::GenericGamemodeEngine) { + let base_id = cube_damage.hit_machine_id as u8; + let mut total_destroyed = 0; + let mut valid_cubes = Vec::with_capacity(cube_damage.hit_cubes.len()); + if let Some(base) = self.base_tracking.bases.get(&base_id) { + for hit_cube in cube_damage.hit_cubes.iter() { + if let Some(damage) = hit_cube.status.damage { + if base.damage_crystal_at_pos(hit_cube.loc, damage, &self.crystals, self.config.protonium_health as u32) { + valid_cubes.push(hit_cube.to_owned()); + } + } else if matches!(hit_cube.status.ty, rlnl::types::CubeHistoryEventType::Destroy) { + if base.destroy_crystal_at_pos(hit_cube.loc, &self.crystals) { + valid_cubes.push(hit_cube.to_owned()); + total_destroyed += 1; + } else { + log::warn!("Did not destroy cube with destroy status"); + } + } + } + generic.broadcast( + rlnl::event_code::NetworkEvent::DestroyCubeNoEffect, + literustlib::packet::Property::ReliableOrdered, + &rlnl::events::ingame::DestroyCubeNoEffect { + shooting_machine_id: cube_damage.shooting_machine_id, + hit_machine_id: cube_damage.hit_machine_id, + target_type: rlnl::types::TargetType::TeamBase, + num_hits: valid_cubes.len() as _, + hit_cubes: valid_cubes, + }, + true, + ).await; + generic.broadcast( + rlnl::event_code::NetworkEvent::SyncTeamBaseCubes, + literustlib::packet::Property::ReliableOrdered, + &base.generate_full_base_heal(base_id, self.config.protonium_health as u32, &self.crystals), + true, + ).await; + } + if total_destroyed != 0 { + if let Some(team_id) = self.player_tracking.team(cube_damage.shooting_machine_id as u8).await { + if let Some(base) = self.base_tracking.bases.get(&team_id) { + //log::info!("Player {} stole {} crystals", cube_damage.shooting_machine_id, total_destroyed); + base.cube_index.fetch_add(total_destroyed as f32, std::sync::atomic::Ordering::SeqCst); + generic.broadcast( + rlnl::event_code::NetworkEvent::SyncTeamBaseCubes, + literustlib::packet::Property::ReliableOrdered, + &base.generate_full_base_heal(team_id, self.config.protonium_health as u32, &self.crystals), + true, + ).await; + } + } else { + log::warn!("Machine {} has no team", cube_damage.shooting_machine_id); + } + } } } @@ -874,8 +996,40 @@ impl CustomGameLogic for BattleArenaLogic { true } - async fn on_broadcast(&self, _generic: &crate::matches::GenericGamemodeEngine, _user_id: i32, _event_out: rlnl::event_code::NetworkEvent, _event_in: rlnl::event_code::NetworkEvent, _property: literustlib::packet::Property, _data: &Option>, _skip_user: bool) -> bool { - true + async fn on_broadcast(&self, generic: &crate::matches::GenericGamemodeEngine, _user_id: i32, _event_out: rlnl::event_code::NetworkEvent, event_in: rlnl::event_code::NetworkEvent, _property: literustlib::packet::Property, data: &Option>, _skip_user: bool) -> bool { + match (event_in, data) { + (rlnl::event_code::NetworkEvent::DamageCubeNoEffect, Some(data)) => { + let maybe_cube_dmg = ::downcast_ref::(data.as_ref()); + if let Some(cube_damage) = maybe_cube_dmg { + if matches!(cube_damage.target_type, rlnl::types::TargetType::TeamBase) { + self.do_team_base_stealing(cube_damage, generic).await; + false + } else { + //log::info!("Got non-base DamageCubeNoEffect {:?}", cube_damage.target_type); + true + } + } else { + log::warn!("Got DamageCubeNoEffect event with bad serialization type"); + true + } + }, + /*(rlnl::event_code::NetworkEvent::DamageCubeEffectOnly, Some(data)) => { + let maybe_cube_dmg = ::downcast_ref::(data.as_ref()); + if let Some(cube_damage) = maybe_cube_dmg { + if matches!(cube_damage.target_type, rlnl::types::TargetType::TeamBase) { + log::info!("Got team base DamageCubeEffectOnly for pos ({}, {}, {})", cube_damage.hit_cube.x, cube_damage.hit_cube.y, cube_damage.hit_cube.z); + false + } else { + log::info!("Got non-base DamageCubeEffectOnly {:?}", cube_damage.target_type); + true + } + } else { + log::warn!("Got DamageCubeEffectOnly event with bad serialization type"); + true + } + },*/ + _ => true + } } async fn on_motion(&self, generic: &crate::matches::GenericGamemodeEngine, motion: &rlnl::machine_motion::MachineMotion, location: (f32, f32, f32)) -> bool { @@ -1029,8 +1183,19 @@ impl CustomGameLogic for BattleArenaLogic { log::warn!("Ignoring SurrenderRequest because it's not implemented (yet)"); } (rlnl::event_code::NetworkEvent::AwardTeamBaseProtoniumDestroyedRequest, literustlib::packet::Property::ReliableOrdered) => { - // TODO - log::warn!("Ignoring AwardTeamBaseProtoniumDestroyedRequest because it's not implemented (yet)"); + let maybe_crystal_bonus = ::downcast_ref::(data.as_ref()); + if let Some(crystal_destroyed) = maybe_crystal_bonus { + if let Some(generic_player) = generic.users.read().await.get(&crystal_destroyed.player_id) { + generic_player.counters.crystals.fetch_add(crystal_destroyed.destroyed_cubes as u32, std::sync::atomic::Ordering::Relaxed); + let data = generic_player.counters.get_generic_packet(crystal_destroyed.player_id, rlnl::types::IngameStatId::DestroyedProtoniumCubes, Some(crystal_destroyed.destroyed_cubes as _)); + generic.broadcast( + rlnl::event_code::NetworkEvent::UpdateGameStats, + literustlib::packet::Property::ReliableOrdered, + &data, + true, + ).await; + } + } } _ => {} }