mirror of
https://git.ngram.ca/OpenJam/rc-servers
synced 2026-08-23 23:08:52 +00:00
Add basic surrendering for #32
This commit is contained in:
@@ -248,7 +248,7 @@ fn default_game_modes() -> GameModes {
|
|||||||
respawn_heal_duration: 10.0,
|
respawn_heal_duration: 10.0,
|
||||||
respawn_full_heal_duration: 0.5,
|
respawn_full_heal_duration: 0.5,
|
||||||
kill_limit: 0,
|
kill_limit: 0,
|
||||||
game_time_m: 5,
|
game_time_m: 20,
|
||||||
},
|
},
|
||||||
elimination: GameMode {
|
elimination: GameMode {
|
||||||
respawn_heal_duration: 10.0,
|
respawn_heal_duration: 10.0,
|
||||||
|
|||||||
@@ -187,6 +187,11 @@ pub async fn handler(init_ctx: &crate::InitConfig) -> crate::handler::LnlEventHa
|
|||||||
{literustlib::packet::Property::ReliableOrdered as u8},
|
{literustlib::packet::Property::ReliableOrdered as u8},
|
||||||
rlnl::events::ingame::InitiateSurrender,
|
rlnl::events::ingame::InitiateSurrender,
|
||||||
>::handler(init_ctx))
|
>::handler(init_ctx))
|
||||||
|
.add(crate::handlers::GamemodeSpecific::<
|
||||||
|
{rlnl::event_code::NetworkEvent::SurrenderVoteCast as i16},
|
||||||
|
{literustlib::packet::Property::ReliableOrdered as u8},
|
||||||
|
rlnl::events::ingame::SurrenderVoteCast,
|
||||||
|
>::handler(init_ctx))
|
||||||
.add(crate::handlers::GamemodeSpecific::<
|
.add(crate::handlers::GamemodeSpecific::<
|
||||||
{rlnl::event_code::NetworkEvent::AwardTeamBaseProtoniumDestroyedRequest as i16},
|
{rlnl::event_code::NetworkEvent::AwardTeamBaseProtoniumDestroyedRequest as i16},
|
||||||
{literustlib::packet::Property::ReliableOrdered as u8},
|
{literustlib::packet::Property::ReliableOrdered as u8},
|
||||||
@@ -231,6 +236,7 @@ mod _broadcast_impls {
|
|||||||
impl Broadcastable for rlnl::events::ingame::MapPing {}
|
impl Broadcastable for rlnl::events::ingame::MapPing {}
|
||||||
impl Broadcastable for rlnl::events::ingame::DamagedByEnemyShield {}
|
impl Broadcastable for rlnl::events::ingame::DamagedByEnemyShield {}
|
||||||
impl Broadcastable for rlnl::events::ingame::InitiateSurrender {}
|
impl Broadcastable for rlnl::events::ingame::InitiateSurrender {}
|
||||||
|
impl Broadcastable for rlnl::events::ingame::SurrenderVoteCast {}
|
||||||
impl Broadcastable for rlnl::events::ingame::AwardProtoniumDestroyedCubes {}
|
impl Broadcastable for rlnl::events::ingame::AwardProtoniumDestroyedCubes {}
|
||||||
|
|
||||||
impl Broadcastable for rlnl::events::sync::UpdateGameModeSettings {}
|
impl Broadcastable for rlnl::events::sync::UpdateGameModeSettings {}
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ impl GameMatches {
|
|||||||
let engine = super::GenericGamemodeEngine::new(
|
let engine = super::GenericGamemodeEngine::new(
|
||||||
game_info,
|
game_info,
|
||||||
map_config,
|
map_config,
|
||||||
|
&self.mode_configs.elimination,
|
||||||
players,
|
players,
|
||||||
inner,
|
inner,
|
||||||
fakes_handler,
|
fakes_handler,
|
||||||
@@ -101,6 +102,7 @@ impl GameMatches {
|
|||||||
let engine = super::GenericGamemodeEngine::new(
|
let engine = super::GenericGamemodeEngine::new(
|
||||||
game_info,
|
game_info,
|
||||||
map_config,
|
map_config,
|
||||||
|
&self.mode_configs.battle_arena,
|
||||||
players,
|
players,
|
||||||
inner,
|
inner,
|
||||||
fakes_handler,
|
fakes_handler,
|
||||||
|
|||||||
@@ -197,6 +197,7 @@ pub(super) struct GenericGamemodeEngine<L: super::CustomGameLogic> {
|
|||||||
pub game_start: std::sync::atomic::AtomicI64,
|
pub game_start: std::sync::atomic::AtomicI64,
|
||||||
pub map_config: std::sync::Arc<oj_rc_core::persist::config::MapConfig>,
|
pub map_config: std::sync::Arc<oj_rc_core::persist::config::MapConfig>,
|
||||||
pub game_descriptor: oj_rc_core::persist::user::GameDescriptor,
|
pub game_descriptor: oj_rc_core::persist::user::GameDescriptor,
|
||||||
|
pub game_duration: std::time::Duration,
|
||||||
pub players_info: std::sync::Arc<Vec<oj_rc_core::persist::user::PlayerDescriptor>>,
|
pub players_info: std::sync::Arc<Vec<oj_rc_core::persist::user::PlayerDescriptor>>,
|
||||||
pub custom_logic_handler: L,
|
pub custom_logic_handler: L,
|
||||||
pub fake_users: std::collections::HashMap<u8, FakeUser>,
|
pub fake_users: std::collections::HashMap<u8, FakeUser>,
|
||||||
@@ -210,6 +211,7 @@ impl <L: super::CustomGameLogic> GenericGamemodeEngine<L> {
|
|||||||
pub fn new(
|
pub fn new(
|
||||||
game: oj_rc_core::persist::user::GameDescriptor,
|
game: oj_rc_core::persist::user::GameDescriptor,
|
||||||
map: oj_rc_core::persist::config::MapConfig,
|
map: oj_rc_core::persist::config::MapConfig,
|
||||||
|
config: &oj_rc_core::data::game_mode::GameModeConfig,
|
||||||
players: Vec<oj_rc_core::persist::user::PlayerDescriptor>,
|
players: Vec<oj_rc_core::persist::user::PlayerDescriptor>,
|
||||||
custom: L,
|
custom: L,
|
||||||
fakes_handler: super::fake::Handler,
|
fakes_handler: super::fake::Handler,
|
||||||
@@ -226,6 +228,7 @@ impl <L: super::CustomGameLogic> GenericGamemodeEngine<L> {
|
|||||||
game_start: std::sync::atomic::AtomicI64::new(-1),
|
game_start: std::sync::atomic::AtomicI64::new(-1),
|
||||||
map_config: std::sync::Arc::new(map),
|
map_config: std::sync::Arc::new(map),
|
||||||
game_descriptor: game,
|
game_descriptor: game,
|
||||||
|
game_duration: std::time::Duration::from_secs((config.game_time_minutes as u64) * 60),
|
||||||
players_info: std::sync::Arc::new(players),
|
players_info: std::sync::Arc::new(players),
|
||||||
custom_logic_handler: custom,
|
custom_logic_handler: custom,
|
||||||
fake_users,
|
fake_users,
|
||||||
@@ -306,6 +309,14 @@ impl <L: super::CustomGameLogic> GenericGamemodeEngine<L> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn elapsed_game_time(&self) -> f32 {
|
||||||
|
let game_start = self.game_start.load(std::sync::atomic::Ordering::Relaxed);
|
||||||
|
//let game_end = (game_start as u64) + self.game_duration.as_secs();
|
||||||
|
let duration = self.game_duration.as_secs();
|
||||||
|
let now = chrono::Utc::now().timestamp();
|
||||||
|
((now - game_start) as f32) / duration as f32
|
||||||
|
}
|
||||||
|
|
||||||
/*pub(super) async fn send_to_player<T: byteserde::ser_heap::ByteSerializeHeap + ?Sized>(&self, player_id: u8, code: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property, data: &T) {
|
/*pub(super) async fn send_to_player<T: byteserde::ser_heap::ByteSerializeHeap + ?Sized>(&self, player_id: u8, code: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property, data: &T) {
|
||||||
if let Some(player) = self.users.read().await.get(&player_id) {
|
if let Some(player) = self.users.read().await.get(&player_id) {
|
||||||
crate::events::log_lnl_send_failure(player.connection.rlnl().send_data(
|
crate::events::log_lnl_send_failure(player.connection.rlnl().send_data(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::matches::CustomGameLogic;
|
use crate::matches::{modes::trackers::SurrenderGameTracker, CustomGameLogic};
|
||||||
|
|
||||||
struct PlayerTracker {
|
struct PlayerTracker {
|
||||||
connected: tokio::sync::Mutex<std::collections::HashMap<u8, std::collections::HashSet<u8>>>, // team -> set of player_id
|
connected: tokio::sync::Mutex<std::collections::HashMap<u8, std::collections::HashSet<u8>>>, // team -> set of player_id
|
||||||
@@ -69,6 +69,14 @@ impl PlayerTracker {
|
|||||||
}
|
}
|
||||||
first_remaining_team
|
first_remaining_team
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn players_on_team(&self, team: u8) -> Vec<u8> {
|
||||||
|
if let Some(members) = self.connected.lock().await.get(&team) {
|
||||||
|
members.iter().map(|x| *x).collect()
|
||||||
|
} else {
|
||||||
|
Vec::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PointInfo {
|
struct PointInfo {
|
||||||
@@ -517,10 +525,10 @@ enum WinMode {
|
|||||||
BaseFull,
|
BaseFull,
|
||||||
OutOfTime,
|
OutOfTime,
|
||||||
OutOfPlayers,
|
OutOfPlayers,
|
||||||
|
Surrender,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct BattleArenaLogic {
|
pub struct BattleArenaLogic {
|
||||||
game_duration: std::time::Duration,
|
|
||||||
game_end: std::sync::atomic::AtomicI64,
|
game_end: std::sync::atomic::AtomicI64,
|
||||||
respawn_full_heal_duration: f32,
|
respawn_full_heal_duration: f32,
|
||||||
respawn_heal_duration: f32,
|
respawn_heal_duration: f32,
|
||||||
@@ -528,6 +536,7 @@ pub struct BattleArenaLogic {
|
|||||||
player_tracking: PlayerTracker,
|
player_tracking: PlayerTracker,
|
||||||
capture_tracking: PointTracker,
|
capture_tracking: PointTracker,
|
||||||
base_tracking: BaseTracker,
|
base_tracking: BaseTracker,
|
||||||
|
surrender_tracking: super::trackers::SurrenderGameTracker,
|
||||||
//cube_parser: std::sync::Arc<oj_rc_core::cubes::CubeLocationsParser>,
|
//cube_parser: std::sync::Arc<oj_rc_core::cubes::CubeLocationsParser>,
|
||||||
crystals: Vec<oj_rc_core::cubes::CubeLocationInfo>,
|
crystals: Vec<oj_rc_core::cubes::CubeLocationInfo>,
|
||||||
config: oj_rc_core::data::battle_arena_config::BattleArenaData,
|
config: oj_rc_core::data::battle_arena_config::BattleArenaData,
|
||||||
@@ -543,13 +552,13 @@ impl BattleArenaLogic {
|
|||||||
let cube_parser = parsers.locations_of();
|
let cube_parser = parsers.locations_of();
|
||||||
let crystals = cube_parser.locations_of_by_distance_to_first(&mut std::io::Cursor::new(&ba_config.base_machine_map), Self::CRYSTAL_ID, Self::CLASP_ID);
|
let crystals = cube_parser.locations_of_by_distance_to_first(&mut std::io::Cursor::new(&ba_config.base_machine_map), Self::CRYSTAL_ID, Self::CLASP_ID);
|
||||||
Self {
|
Self {
|
||||||
game_duration: dur,
|
|
||||||
respawn_full_heal_duration: config.respawn_full_heal_duration,
|
respawn_full_heal_duration: config.respawn_full_heal_duration,
|
||||||
respawn_heal_duration: config.respawn_heal_duration,
|
respawn_heal_duration: config.respawn_heal_duration,
|
||||||
game_end: std::sync::atomic::AtomicI64::new(fake_end),
|
game_end: std::sync::atomic::AtomicI64::new(fake_end),
|
||||||
timer_task: tokio::sync::Mutex::new(None),
|
timer_task: tokio::sync::Mutex::new(None),
|
||||||
player_tracking: PlayerTracker::new(),
|
player_tracking: PlayerTracker::new(),
|
||||||
capture_tracking: PointTracker::new(map.capture_points.iter().map(|(_, speed)| *speed)),
|
capture_tracking: PointTracker::new(map.capture_points.iter().map(|(_, speed)| *speed)),
|
||||||
|
surrender_tracking: super::trackers::SurrenderGameTracker::new(),
|
||||||
base_tracking: BaseTracker::new(map.bases.keys(), &crystals),
|
base_tracking: BaseTracker::new(map.bases.keys(), &crystals),
|
||||||
//cube_parser,
|
//cube_parser,
|
||||||
//ba_base: teambase,
|
//ba_base: teambase,
|
||||||
@@ -622,8 +631,9 @@ impl BattleArenaLogic {
|
|||||||
async fn do_win(&self, winning_team: u8, ty: WinMode, generic: &crate::matches::GenericGamemodeEngine<Self>) {
|
async fn do_win(&self, winning_team: u8, ty: WinMode, generic: &crate::matches::GenericGamemodeEngine<Self>) {
|
||||||
generic.game_done();
|
generic.game_done();
|
||||||
let end_reason = match ty {
|
let end_reason = match ty {
|
||||||
WinMode::BaseFull
|
WinMode::BaseFull => rlnl::types::GameEndReason::BaseDestroyed,
|
||||||
| WinMode::OutOfPlayers => rlnl::types::GameEndReason::BaseDestroyed,
|
WinMode::OutOfPlayers => rlnl::types::GameEndReason::OneTeamRemaining,
|
||||||
|
WinMode::Surrender => rlnl::types::GameEndReason::Surrendered,
|
||||||
WinMode::OutOfTime => rlnl::types::GameEndReason::TimeOut,
|
WinMode::OutOfTime => rlnl::types::GameEndReason::TimeOut,
|
||||||
};
|
};
|
||||||
let payload = rlnl::events::ingame::GameLoseWin {
|
let payload = rlnl::events::ingame::GameLoseWin {
|
||||||
@@ -634,7 +644,8 @@ impl BattleArenaLogic {
|
|||||||
let is_winner = player.descriptor.team == winning_team as i32;
|
let is_winner = player.descriptor.team == winning_team as i32;
|
||||||
let net_event = match ty {
|
let net_event = match ty {
|
||||||
WinMode::BaseFull
|
WinMode::BaseFull
|
||||||
| WinMode::OutOfPlayers => {
|
| WinMode::OutOfPlayers
|
||||||
|
| WinMode::Surrender => {
|
||||||
if is_winner { rlnl::event_code::NetworkEvent::GameWonBaseDestroyed } else { rlnl::event_code::NetworkEvent::GameLostBaseDestroyed }
|
if is_winner { rlnl::event_code::NetworkEvent::GameWonBaseDestroyed } else { rlnl::event_code::NetworkEvent::GameLostBaseDestroyed }
|
||||||
},
|
},
|
||||||
WinMode::OutOfTime => {
|
WinMode::OutOfTime => {
|
||||||
@@ -902,7 +913,7 @@ impl CustomGameLogic for BattleArenaLogic {
|
|||||||
Some(crate::matches::RlnlPacket {
|
Some(crate::matches::RlnlPacket {
|
||||||
event: rlnl::event_code::NetworkEvent::CurrentGameTime,
|
event: rlnl::event_code::NetworkEvent::CurrentGameTime,
|
||||||
property: literustlib::packet::Property::ReliableOrdered,
|
property: literustlib::packet::Property::ReliableOrdered,
|
||||||
data: Box::new(rlnl::events::GameTime(self.game_duration.as_millis() as f32 / 1000.0)),
|
data: Box::new(rlnl::events::GameTime(generic.game_duration.as_millis() as f32 / 1000.0)),
|
||||||
}),
|
}),
|
||||||
// SyncTeamBaseCubes
|
// SyncTeamBaseCubes
|
||||||
// TODO ???
|
// TODO ???
|
||||||
@@ -978,7 +989,7 @@ impl CustomGameLogic for BattleArenaLogic {
|
|||||||
senders.push((conn.connection.clone(), conn.state.clone()));
|
senders.push((conn.connection.clone(), conn.state.clone()));
|
||||||
}
|
}
|
||||||
drop(read_lock);
|
drop(read_lock);
|
||||||
let game_end = game_start + self.game_duration;
|
let game_end = game_start + generic.game_duration;
|
||||||
let extra_packets = Vec::default();
|
let extra_packets = Vec::default();
|
||||||
let new_timer_task = crate::matches::timer::match_time_syncer(senders, game_start, game_end, extra_packets, Vec::default());
|
let new_timer_task = crate::matches::timer::match_time_syncer(senders, game_start, game_end, extra_packets, Vec::default());
|
||||||
let mut timer_lock = self.timer_task.lock().await;
|
let mut timer_lock = self.timer_task.lock().await;
|
||||||
@@ -988,6 +999,12 @@ impl CustomGameLogic for BattleArenaLogic {
|
|||||||
}
|
}
|
||||||
*timer_lock = Some(new_timer_task);
|
*timer_lock = Some(new_timer_task);
|
||||||
self.game_end.store(game_end.timestamp(), std::sync::atomic::Ordering::Relaxed);
|
self.game_end.store(game_end.timestamp(), std::sync::atomic::Ordering::Relaxed);
|
||||||
|
generic.broadcast(
|
||||||
|
rlnl::event_code::NetworkEvent::SetSurrenderTimes,
|
||||||
|
literustlib::packet::Property::ReliableOrdered,
|
||||||
|
&SurrenderGameTracker::surrender_times(),
|
||||||
|
false,
|
||||||
|
).await;
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1013,21 +1030,6 @@ impl CustomGameLogic for BattleArenaLogic {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
/*(rlnl::event_code::NetworkEvent::DamageCubeEffectOnly, Some(data)) => {
|
|
||||||
let maybe_cube_dmg = <dyn core::any::Any>::downcast_ref::<rlnl::events::ingame::DestroyCubeEffectOnly>(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
|
_ => true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1099,7 +1101,7 @@ impl CustomGameLogic for BattleArenaLogic {
|
|||||||
if let Some(owned_points) = tick_info.owned.get(base_id) {
|
if let Some(owned_points) = tick_info.owned.get(base_id) {
|
||||||
if let Some(tracked_base) = self.base_tracking.bases.get(base_id) {
|
if let Some(tracked_base) = self.base_tracking.bases.get(base_id) {
|
||||||
let one_tick = (self.crystals.len() as f32)
|
let one_tick = (self.crystals.len() as f32)
|
||||||
* ((PointTracker::TICK_MS as f32) / (self.game_duration.as_millis() as f32))
|
* ((PointTracker::TICK_MS as f32) / (generic.game_duration.as_millis() as f32))
|
||||||
* ((self.base_tracking.bases.len() as f32) / (self.capture_tracking.points.len() as f32));
|
* ((self.base_tracking.bases.len() as f32) / (self.capture_tracking.points.len() as f32));
|
||||||
let increment = tick_info.delta as f32 * (*owned_points as f32) * one_tick;
|
let increment = tick_info.delta as f32 * (*owned_points as f32) * one_tick;
|
||||||
let old_float_index = tracked_base.cube_index.fetch_add(increment, std::sync::atomic::Ordering::SeqCst);
|
let old_float_index = tracked_base.cube_index.fetch_add(increment, std::sync::atomic::Ordering::SeqCst);
|
||||||
@@ -1164,11 +1166,14 @@ impl CustomGameLogic for BattleArenaLogic {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handle surrender vote tick
|
||||||
|
self.surrender_tracking.tick(generic).await;
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn on_custom(&self, generic: &crate::matches::GenericGamemodeEngine<Self>, _user_id: i32, event: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property, data: Box<dyn crate::Broadcastable>) {
|
async fn on_custom(&self, generic: &crate::matches::GenericGamemodeEngine<Self>, user_id: i32, event: rlnl::event_code::NetworkEvent, property: literustlib::packet::Property, data: Box<dyn crate::Broadcastable>) {
|
||||||
match (event, property) {
|
match (event, property) {
|
||||||
(rlnl::event_code::NetworkEvent::SendDamagedByEnemyShield, literustlib::packet::Property::ReliableOrdered) => {
|
(rlnl::event_code::NetworkEvent::SendDamagedByEnemyShield, literustlib::packet::Property::ReliableOrdered) => {
|
||||||
generic.broadcast(
|
generic.broadcast(
|
||||||
@@ -1180,8 +1185,35 @@ impl CustomGameLogic for BattleArenaLogic {
|
|||||||
},
|
},
|
||||||
(rlnl::event_code::NetworkEvent::SurrenderRequest, literustlib::packet::Property::ReliableOrdered) => {
|
(rlnl::event_code::NetworkEvent::SurrenderRequest, literustlib::packet::Property::ReliableOrdered) => {
|
||||||
// TODO
|
// TODO
|
||||||
log::warn!("Ignoring SurrenderRequest because it's not implemented (yet)");
|
let maybe_init_surr = <dyn core::any::Any>::downcast_ref::<rlnl::events::ingame::InitiateSurrender>(data.as_ref());
|
||||||
}
|
if let Some(init_surr) = maybe_init_surr {
|
||||||
|
if let Some(team) = self.player_tracking.team(init_surr.player_id).await {
|
||||||
|
let team_members = self.player_tracking.players_on_team(team).await;
|
||||||
|
let result = self.surrender_tracking.request_new(team, init_surr.player_id, team_members.into_iter(), generic).await;
|
||||||
|
if matches!(result, super::trackers::SurrenderVoteResult::Succeeded) {
|
||||||
|
let winning_team = if team == 0 { 1 } else { 0 };
|
||||||
|
self.do_win(winning_team, WinMode::Surrender, generic).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::warn!("Bad SurrenderRequest data");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(rlnl::event_code::NetworkEvent::SurrenderVoteCast, literustlib::packet::Property::ReliableOrdered) => {
|
||||||
|
if let Some(player_id) = generic.user_key_by_user_id(user_id).await {
|
||||||
|
if let Some(team) = self.player_tracking.team(player_id).await {
|
||||||
|
let maybe_vote = <dyn core::any::Any>::downcast_ref::<rlnl::events::ingame::SurrenderVoteCast>(data.as_ref());
|
||||||
|
if let Some(vote) = maybe_vote {
|
||||||
|
let result = self.surrender_tracking.vote(team, player_id, vote.surrender != 0, generic).await;
|
||||||
|
if matches!(result, super::trackers::SurrenderVoteResult::Succeeded) {
|
||||||
|
let winning_team = if team == 0 { 1 } else { 0 };
|
||||||
|
self.do_win(winning_team, WinMode::Surrender, generic).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
(rlnl::event_code::NetworkEvent::AwardTeamBaseProtoniumDestroyedRequest, literustlib::packet::Property::ReliableOrdered) => {
|
(rlnl::event_code::NetworkEvent::AwardTeamBaseProtoniumDestroyedRequest, literustlib::packet::Property::ReliableOrdered) => {
|
||||||
let maybe_crystal_bonus = <dyn core::any::Any>::downcast_ref::<rlnl::events::ingame::AwardProtoniumDestroyedCubes>(data.as_ref());
|
let maybe_crystal_bonus = <dyn core::any::Any>::downcast_ref::<rlnl::events::ingame::AwardProtoniumDestroyedCubes>(data.as_ref());
|
||||||
if let Some(crystal_destroyed) = maybe_crystal_bonus {
|
if let Some(crystal_destroyed) = maybe_crystal_bonus {
|
||||||
|
|||||||
@@ -339,7 +339,6 @@ impl BaseTracker {
|
|||||||
pub struct EliminationLogic {
|
pub struct EliminationLogic {
|
||||||
tracked: PlayerTracker,
|
tracked: PlayerTracker,
|
||||||
bases: BaseTracker,
|
bases: BaseTracker,
|
||||||
game_duration: std::time::Duration,
|
|
||||||
respawn_full_heal_duration: f32,
|
respawn_full_heal_duration: f32,
|
||||||
respawn_heal_duration: f32,
|
respawn_heal_duration: f32,
|
||||||
game_end: std::sync::atomic::AtomicI64,
|
game_end: std::sync::atomic::AtomicI64,
|
||||||
@@ -360,7 +359,6 @@ impl EliminationLogic {
|
|||||||
last_tick: std::sync::atomic::AtomicI64::new(i64::MIN),
|
last_tick: std::sync::atomic::AtomicI64::new(i64::MIN),
|
||||||
is_baseless: map.bases.is_empty(),
|
is_baseless: map.bases.is_empty(),
|
||||||
},
|
},
|
||||||
game_duration: dur,
|
|
||||||
respawn_full_heal_duration: config.respawn_full_heal_duration,
|
respawn_full_heal_duration: config.respawn_full_heal_duration,
|
||||||
respawn_heal_duration: config.respawn_heal_duration,
|
respawn_heal_duration: config.respawn_heal_duration,
|
||||||
game_end: std::sync::atomic::AtomicI64::new(fake_end),
|
game_end: std::sync::atomic::AtomicI64::new(fake_end),
|
||||||
@@ -478,7 +476,7 @@ impl CustomGameLogic for EliminationLogic {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn extra_sync_events(&self, _generic: &crate::matches::GenericGamemodeEngine<Self>, _player: &crate::matches::generic::UserConnection) -> Vec<crate::matches::RlnlPacket> {
|
async fn extra_sync_events(&self, generic: &crate::matches::GenericGamemodeEngine<Self>, _player: &crate::matches::generic::UserConnection) -> Vec<crate::matches::RlnlPacket> {
|
||||||
vec![
|
vec![
|
||||||
crate::matches::RlnlPacket {
|
crate::matches::RlnlPacket {
|
||||||
event: rlnl::event_code::NetworkEvent::GameModeSettings,
|
event: rlnl::event_code::NetworkEvent::GameModeSettings,
|
||||||
@@ -491,7 +489,7 @@ impl CustomGameLogic for EliminationLogic {
|
|||||||
crate::matches::RlnlPacket {
|
crate::matches::RlnlPacket {
|
||||||
event: rlnl::event_code::NetworkEvent::CurrentGameTime,
|
event: rlnl::event_code::NetworkEvent::CurrentGameTime,
|
||||||
property: literustlib::packet::Property::ReliableOrdered,
|
property: literustlib::packet::Property::ReliableOrdered,
|
||||||
data: Box::new(rlnl::events::GameTime(self.game_duration.as_millis() as f32 / 1000.0)),
|
data: Box::new(rlnl::events::GameTime(generic.game_duration.as_millis() as f32 / 1000.0)),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -503,7 +501,7 @@ impl CustomGameLogic for EliminationLogic {
|
|||||||
senders.push((conn.connection.clone(), conn.state.clone()));
|
senders.push((conn.connection.clone(), conn.state.clone()));
|
||||||
}
|
}
|
||||||
drop(read_lock);
|
drop(read_lock);
|
||||||
let game_end = game_start + self.game_duration;
|
let game_end = game_start + generic.game_duration;
|
||||||
let teams = self.bases.teams();
|
let teams = self.bases.teams();
|
||||||
let extra_packets = teams.iter().map(|team| crate::matches::RlnlPacket {
|
let extra_packets = teams.iter().map(|team| crate::matches::RlnlPacket {
|
||||||
event: rlnl::event_code::NetworkEvent::TeamBaseInitialise,
|
event: rlnl::event_code::NetworkEvent::TeamBaseInitialise,
|
||||||
|
|||||||
@@ -7,3 +7,5 @@ pub use elimination::EliminationLogic;
|
|||||||
|
|
||||||
mod battle_arena;
|
mod battle_arena;
|
||||||
pub use battle_arena::BattleArenaLogic;
|
pub use battle_arena::BattleArenaLogic;
|
||||||
|
|
||||||
|
mod trackers;
|
||||||
|
|||||||
2
rc_multiplayer/src/matches/modes/trackers/mod.rs
Normal file
2
rc_multiplayer/src/matches/modes/trackers/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
mod surrender;
|
||||||
|
pub use surrender::{SurrenderGameTracker, SurrenderVoteResult};
|
||||||
228
rc_multiplayer/src/matches/modes/trackers/surrender.rs
Normal file
228
rc_multiplayer/src/matches/modes/trackers/surrender.rs
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
#[repr(u8)]
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
enum VoteStatus {
|
||||||
|
NoEntry = 0,
|
||||||
|
Yes = 1,
|
||||||
|
No = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VoteStatus {
|
||||||
|
fn from_u8(num: u8) -> Self {
|
||||||
|
match num {
|
||||||
|
0 => Self::NoEntry,
|
||||||
|
1 => Self::Yes,
|
||||||
|
2 => Self::No,
|
||||||
|
_ => panic!("Invalid vote status variant {}", num),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn to_u8(self) -> u8 {
|
||||||
|
self as u8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CurrentVoteInfo {
|
||||||
|
current_votes: rlnl::events::ingame::CurrentSurrenderVotes,
|
||||||
|
declined: Option<rlnl::events::ingame::SurrenderDeclined>,
|
||||||
|
succeeded: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SurrenderTracker {
|
||||||
|
ends_at: chrono::DateTime<chrono::Utc>,
|
||||||
|
players: std::collections::HashMap<u8, std::sync::atomic::AtomicU8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SurrenderTracker {
|
||||||
|
const DURATION: std::time::Duration = std::time::Duration::from_secs(30);
|
||||||
|
|
||||||
|
fn new(players: impl std::iter::Iterator<Item=u8>, initiator: u8) -> Self {
|
||||||
|
let now = chrono::Utc::now();
|
||||||
|
Self {
|
||||||
|
ends_at: now + Self::DURATION,
|
||||||
|
players: players
|
||||||
|
.map(|player_id| (player_id, if initiator == player_id {
|
||||||
|
std::sync::atomic::AtomicU8::new(VoteStatus::Yes.to_u8())
|
||||||
|
} else {
|
||||||
|
std::sync::atomic::AtomicU8::new(VoteStatus::NoEntry.to_u8())
|
||||||
|
}))
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_votes(&self, player_id: Option<u8>, game_time_elapsed: f32) -> CurrentVoteInfo {
|
||||||
|
let mut votes = Vec::with_capacity(self.players.len());
|
||||||
|
let mut total_no_votes = 0;
|
||||||
|
let mut total_yes_votes = 0;
|
||||||
|
for voter in self.players.values() {
|
||||||
|
let vote = VoteStatus::from_u8(voter.load(std::sync::atomic::Ordering::Relaxed));
|
||||||
|
match vote {
|
||||||
|
VoteStatus::NoEntry => {},
|
||||||
|
VoteStatus::Yes => {
|
||||||
|
total_yes_votes += 1;
|
||||||
|
votes.push(1);
|
||||||
|
},
|
||||||
|
VoteStatus::No => {
|
||||||
|
total_no_votes += 1;
|
||||||
|
votes.push(0);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let threshold = self.players.len() / 2;
|
||||||
|
let declined = if total_no_votes > threshold {
|
||||||
|
Some(rlnl::events::ingame::SurrenderDeclined {
|
||||||
|
surrendering_player_id: player_id.or_else(|| self.players.keys().next().map(|x| *x)).unwrap_or(0) as i32,
|
||||||
|
game_time_elapsed,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
CurrentVoteInfo {
|
||||||
|
current_votes: rlnl::events::ingame::CurrentSurrenderVotes {
|
||||||
|
players_on_team: self.players.len() as i32,
|
||||||
|
num_votes: votes.len() as _,
|
||||||
|
votes,
|
||||||
|
},
|
||||||
|
declined,
|
||||||
|
succeeded: total_yes_votes > threshold,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vote(&self, player_id: u8, is_surrender: bool, game_time_elapsed: f32) -> Option<CurrentVoteInfo> {
|
||||||
|
if let Some(voter) = self.players.get(&player_id) {
|
||||||
|
let vote = if is_surrender { VoteStatus::Yes } else { VoteStatus::No };
|
||||||
|
voter.store(vote.to_u8(), std::sync::atomic::Ordering::Relaxed);
|
||||||
|
Some(self.current_votes(Some(player_id), game_time_elapsed))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tick(&self, game_time_elapsed: f32) -> Option<rlnl::events::ingame::SurrenderDeclined> {
|
||||||
|
let now = chrono::Utc::now();
|
||||||
|
if now > self.ends_at {
|
||||||
|
Some(rlnl::events::ingame::SurrenderDeclined {
|
||||||
|
surrendering_player_id: 0,
|
||||||
|
game_time_elapsed,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SurrenderGameTracker {
|
||||||
|
surrenders: tokio::sync::RwLock<std::collections::HashMap<u8, SurrenderTracker>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum SurrenderVoteResult {
|
||||||
|
Succeeded,
|
||||||
|
Declined,
|
||||||
|
NoChange,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SurrenderGameTracker {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
surrenders: tokio::sync::RwLock::new(std::collections::HashMap::with_capacity(2)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn vote<L: crate::matches::CustomGameLogic>(&self, team: u8, player_id: u8, is_surrender: bool, generic: &crate::matches::GenericGamemodeEngine<L>) -> SurrenderVoteResult {
|
||||||
|
let mut result = SurrenderVoteResult::NoChange;
|
||||||
|
if let Some(tracker) = self.surrenders.read().await.get(&team) {
|
||||||
|
if let Some(vote_info) = tracker.vote(player_id, is_surrender, generic.elapsed_game_time()) {
|
||||||
|
if vote_info.succeeded {
|
||||||
|
result = SurrenderVoteResult::Succeeded;
|
||||||
|
} else if vote_info.declined.is_some() {
|
||||||
|
result = SurrenderVoteResult::Declined;
|
||||||
|
}
|
||||||
|
Self::handle_vote_info(tracker, generic, vote_info, false).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match result {
|
||||||
|
SurrenderVoteResult::Succeeded | SurrenderVoteResult::Declined => {
|
||||||
|
self.surrenders.write().await.remove(&team);
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn request_new<L: crate::matches::CustomGameLogic>(&self, team: u8, initiator: u8, players: impl std::iter::Iterator<Item=u8>, generic: &crate::matches::GenericGamemodeEngine<L>) -> SurrenderVoteResult {
|
||||||
|
log::info!("Surrender vote for team {} initiated by player {} in game {}", team, initiator, generic.game_guid());
|
||||||
|
let new_tracker = SurrenderTracker::new(players, initiator);
|
||||||
|
let vote_info = new_tracker.current_votes(None, generic.elapsed_game_time());
|
||||||
|
let result = if vote_info.succeeded {
|
||||||
|
SurrenderVoteResult::Succeeded
|
||||||
|
} else {
|
||||||
|
SurrenderVoteResult::NoChange
|
||||||
|
};
|
||||||
|
Self::handle_vote_info(&new_tracker, generic, vote_info, true).await;
|
||||||
|
self.surrenders.write().await.insert(team, new_tracker);
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn surrender_times() -> rlnl::events::ingame::SurrenderTimes {
|
||||||
|
// TODO make this configurable
|
||||||
|
rlnl::events::ingame::SurrenderTimes {
|
||||||
|
player_cooldown_seconds: 30,
|
||||||
|
team_cooldown_seconds: 30,
|
||||||
|
surrender_timeout_seconds: 10,
|
||||||
|
initial_surrender_timeout_seconds: 60,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn tick<L: crate::matches::CustomGameLogic>(&self, generic: &crate::matches::GenericGamemodeEngine<L>) {
|
||||||
|
let mut keys_to_remove = Vec::default();
|
||||||
|
let generic_user_lock = generic.users.read().await;
|
||||||
|
for (team, tracker) in self.surrenders.read().await.iter() {
|
||||||
|
if let Some(declined) = tracker.tick(generic.elapsed_game_time()) {
|
||||||
|
log::info!("Surrender vote for team {} expired in game {}", team, generic.game_guid());
|
||||||
|
for team_member in tracker.players.keys() {
|
||||||
|
if let Some(conn) = generic_user_lock.get(team_member) {
|
||||||
|
crate::events::log_lnl_send_failure(conn.connection.rlnl().send_data(
|
||||||
|
&declined,
|
||||||
|
rlnl::event_code::NetworkEvent::SurrenderDeclined,
|
||||||
|
literustlib::packet::Property::ReliableOrdered,
|
||||||
|
&conn.connection.connection,
|
||||||
|
).await);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keys_to_remove.push(*team);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !keys_to_remove.is_empty() {
|
||||||
|
let mut tracker_write_lock = self.surrenders.write().await;
|
||||||
|
for to_remove in keys_to_remove {
|
||||||
|
tracker_write_lock.remove(&to_remove);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_vote_info<L: crate::matches::CustomGameLogic>(tracker: &SurrenderTracker, generic: &crate::matches::GenericGamemodeEngine<L>, vote_info: CurrentVoteInfo, is_new: bool) {
|
||||||
|
let generic_user_lock = generic.users.read().await;
|
||||||
|
for team_member in tracker.players.keys() {
|
||||||
|
if let Some(conn) = generic_user_lock.get(team_member) {
|
||||||
|
crate::events::log_lnl_send_failure(conn.connection.rlnl().send_data(
|
||||||
|
&vote_info.current_votes,
|
||||||
|
if is_new { rlnl::event_code::NetworkEvent::SurrenderVoteStarted } else { rlnl::event_code::NetworkEvent::CurrentSurrenderVotes },
|
||||||
|
literustlib::packet::Property::ReliableOrdered,
|
||||||
|
&conn.connection.connection,
|
||||||
|
).await);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(declined) = vote_info.declined {
|
||||||
|
for team_member in tracker.players.keys() {
|
||||||
|
if let Some(conn) = generic_user_lock.get(team_member) {
|
||||||
|
crate::events::log_lnl_send_failure(conn.connection.rlnl().send_data(
|
||||||
|
&declined,
|
||||||
|
rlnl::event_code::NetworkEvent::SurrenderDeclined,
|
||||||
|
literustlib::packet::Property::ReliableOrdered,
|
||||||
|
&conn.connection.connection,
|
||||||
|
).await);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user