cringe?
This commit is contained in:
@@ -5,7 +5,5 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "*", features = ["full"] }
|
||||
serenity = { version = "*", default-features = false, features = ["client", "gateway", "rustls_backend", "model"] }
|
||||
sqlx = { version = "*", features = ["runtime-tokio-rustls", "sqlite"] }
|
||||
serenity = { version = "*", default-features = false, features = ["client", "gateway", "rustls_backend", "model", "cache", "utils"] }
|
||||
sea-orm = { version = "*", features = ["sqlx-sqlite", "runtime-tokio-rustls", "macros"] }
|
||||
ctrlc = "*"
|
||||
28
run/database/script.sql
Normal file
28
run/database/script.sql
Normal file
@@ -0,0 +1,28 @@
|
||||
CREATE TABLE IF NOT EXISTS players (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
discord_id INTEGER NOT NULL,
|
||||
bn_name TEXT,
|
||||
bn_tag TEXT,
|
||||
tank REAL NOT NULL DEFAULT 2500.0,
|
||||
dps REAL NOT NULL DEFAULT 2500.0,
|
||||
support REAL NOT NULL DEFAULT 2500.0,
|
||||
flex INTEGER NOT NULL DEFAULT true,
|
||||
primary_role INTEGER NOT NULL DEFAULT -1,
|
||||
secondary_role INTEGER NOT NULL DEFAULT -1,
|
||||
tertiary_role INTEGER NOT NULL DEFAULT -1,
|
||||
UNIQUE (discord_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS lobbies (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
guild_id INTEGER NOT NULL,
|
||||
main_voice_id INTEGER NOT NULL,
|
||||
red_team_voice_id INTEGER NOT NULL,
|
||||
blue_team_voice_id INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS players_discord_id_idx ON players (discord_id);
|
||||
CREATE INDEX IF NOT EXISTS lobbies_guild_id_idx ON lobbies (guild_id);
|
||||
CREATE INDEX IF NOT EXISTS lobbies_main_voice_id_idx ON lobbies (main_voice_id);
|
||||
CREATE INDEX IF NOT EXISTS lobbies_red_team_voice_id_idx ON lobbies (red_team_voice_id);
|
||||
CREATE INDEX IF NOT EXISTS lobbies_blue_team_voice_id_idx ON lobbies (blue_team_voice_id);
|
||||
164
src/bot/commands/lobby.rs
Normal file
164
src/bot/commands/lobby.rs
Normal file
@@ -0,0 +1,164 @@
|
||||
use serenity::builder::CreateApplicationCommand;
|
||||
use serenity::client::Context;
|
||||
use serenity::model::application::interaction::{
|
||||
application_command::ApplicationCommandInteraction,
|
||||
InteractionResponseType
|
||||
};
|
||||
use serenity::async_trait;
|
||||
use serenity::http::CacheHttp;
|
||||
use serenity::model::application::command::CommandOptionType;
|
||||
use serenity::model::channel::{ChannelType, PermissionOverwrite, PermissionOverwriteType};
|
||||
use serenity::model::id::{ChannelId, RoleId, UserId};
|
||||
use serenity::model::Permissions;
|
||||
use crate::bot::commands::MixerCommand;
|
||||
use crate::database::DatabaseContainer;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LobbyCommand;
|
||||
|
||||
#[async_trait]
|
||||
impl MixerCommand for LobbyCommand {
|
||||
fn name(&self) -> String {
|
||||
"lobby".to_string()
|
||||
}
|
||||
|
||||
fn create(&self, command: &mut CreateApplicationCommand) {
|
||||
command.name(self.name()).description("Create or start a lobby")
|
||||
.create_option(|option| {
|
||||
option.name("create")
|
||||
.description("Create a lobby")
|
||||
.kind(CommandOptionType::SubCommand)
|
||||
})
|
||||
.create_option(|option| {
|
||||
option.name("start")
|
||||
.description("Start a lobby")
|
||||
.kind(CommandOptionType::SubCommand)
|
||||
})
|
||||
.default_member_permissions(Permissions::MOVE_MEMBERS)
|
||||
.dm_permission(false);
|
||||
}
|
||||
|
||||
async fn execute(&self, ctx: &Context, interaction: ApplicationCommandInteraction) -> serenity::Result<()> {
|
||||
match interaction.data.options.get(0).unwrap().name.as_str() {
|
||||
"create" => self.create_lobby(ctx, interaction).await,
|
||||
"start" => self.start_lobby(ctx, interaction).await,
|
||||
_ => Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LobbyCommand {
|
||||
async fn create_lobby(&self, ctx: &Context, interaction: ApplicationCommandInteraction) -> serenity::Result<()> {
|
||||
let guild_id = interaction.guild_id.unwrap();
|
||||
let member = guild_id.member(ctx.http(), interaction.user.id).await?;
|
||||
let mut has_permission = false;
|
||||
if let Ok(perms) = member.permissions(&ctx.cache) {
|
||||
if perms.manage_channels() {
|
||||
has_permission = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !has_permission {
|
||||
interaction.create_interaction_response(&ctx.http, |response| {
|
||||
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| {
|
||||
message.content("You don't have permission to create a lobby!")
|
||||
})
|
||||
}).await?;
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
let data = ctx.data.read().await;
|
||||
let db = data.get::<DatabaseContainer>().unwrap().read().await;
|
||||
|
||||
let main_voice = interaction.guild_id.unwrap().create_channel(&ctx.http, |c| {
|
||||
c.name("Mix Lobby").kind(ChannelType::Voice)
|
||||
}).await?;
|
||||
|
||||
let permissions = vec![
|
||||
PermissionOverwrite {
|
||||
allow: Permissions::empty(),
|
||||
deny: Permissions::VIEW_CHANNEL,
|
||||
kind: PermissionOverwriteType::Role(RoleId::from(interaction.guild_id.unwrap().0))
|
||||
}
|
||||
];
|
||||
let red_voice = interaction.guild_id.unwrap().create_channel(&ctx.http, |c| {
|
||||
c.name("Red").kind(ChannelType::Voice).user_limit(5).permissions(permissions.clone())
|
||||
}).await?;
|
||||
let blue_voice = interaction.guild_id.unwrap().create_channel(&ctx.http, |c| {
|
||||
c.name("Blue").kind(ChannelType::Voice).user_limit(5).permissions(permissions)
|
||||
}).await?;
|
||||
|
||||
db.insert_guild_lobby(interaction.guild_id.unwrap(), main_voice.id, red_voice.id, blue_voice.id).await;
|
||||
|
||||
interaction.create_interaction_response(&ctx.http, |response| {
|
||||
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| {
|
||||
message.content("Successfully created a new mix lobby!")
|
||||
})
|
||||
}).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn start_lobby(&self, ctx: &Context, interaction: ApplicationCommandInteraction) -> serenity::Result<()> {
|
||||
let data = ctx.data.read().await;
|
||||
let db = data.get::<DatabaseContainer>().unwrap().write().await;
|
||||
|
||||
let guild_id = interaction.guild_id.unwrap();
|
||||
let member = guild_id.member(ctx.http(), interaction.user.id).await?;
|
||||
|
||||
let mut is_in_lobby = false;
|
||||
let mut channel_id = None;
|
||||
for (id, channel) in guild_id.channels(ctx.http()).await? {
|
||||
if channel.kind != ChannelType::Voice {
|
||||
continue;
|
||||
}
|
||||
let members = channel.members(&ctx.cache).await?;
|
||||
if members.iter().any(|m| m.user.id == member.user.id) && db.get_lobby_by_channel(guild_id, id).await.is_some() {
|
||||
is_in_lobby = true;
|
||||
channel_id = Some(id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !is_in_lobby {
|
||||
interaction.create_interaction_response(ctx.http(), |response| {
|
||||
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| {
|
||||
message.content("You are not in the mix lobby!").ephemeral(true)
|
||||
})
|
||||
}).await?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let lobby = db.get_lobby_by_channel(guild_id, channel_id.unwrap()).await.unwrap();
|
||||
|
||||
let main_channel = ChannelId::from(lobby.main_voice_id as u64).to_channel(ctx.http()).await.unwrap().guild().unwrap();
|
||||
let red_channel = ChannelId::from(lobby.red_team_voice_id as u64).to_channel(ctx.http()).await.unwrap().guild().unwrap();
|
||||
let blue_channel = ChannelId::from(lobby.blue_team_voice_id as u64).to_channel(ctx.http()).await.unwrap().guild().unwrap();
|
||||
|
||||
for member in red_channel.members(ctx.cache().unwrap()).await? {
|
||||
member.move_to_voice_channel(ctx.http(), main_channel.id).await?;
|
||||
}
|
||||
for member in blue_channel.members(ctx.cache().unwrap()).await? {
|
||||
member.move_to_voice_channel(ctx.http(), main_channel.id).await?;
|
||||
}
|
||||
|
||||
let members = main_channel.members(ctx.cache().unwrap()).await?;
|
||||
let users = members.iter().map(|m| m.user.id).collect::<Vec<UserId>>();
|
||||
let players = db.get_players(users).await;
|
||||
|
||||
println!("{:?}", players);
|
||||
|
||||
interaction.create_interaction_response(ctx.http(), |response| {
|
||||
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| {
|
||||
message.content("Successfully started the mix lobby!").ephemeral(true)
|
||||
})
|
||||
}).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
pub mod ping;
|
||||
pub mod lobby;
|
||||
pub mod rank;
|
||||
pub mod preference;
|
||||
|
||||
use serenity::builder::CreateApplicationCommand;
|
||||
use serenity::client::Context;
|
||||
use serenity::model::application::interaction::application_command::ApplicationCommandInteraction;
|
||||
use serenity::async_trait;
|
||||
|
||||
|
||||
#[async_trait]
|
||||
pub trait MixerCommand: Sync + Send {
|
||||
fn name(&self) -> String;
|
||||
|
||||
@@ -5,13 +5,14 @@ use serenity::model::application::interaction::{
|
||||
InteractionResponseType
|
||||
};
|
||||
use serenity::async_trait;
|
||||
use serenity::http::CacheHttp;
|
||||
use crate::bot::commands::MixerCommand;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Ping;
|
||||
pub struct PingCommand;
|
||||
|
||||
#[async_trait]
|
||||
impl MixerCommand for Ping {
|
||||
impl MixerCommand for PingCommand {
|
||||
fn name(&self) -> String {
|
||||
"ping".to_string()
|
||||
}
|
||||
@@ -22,14 +23,25 @@ impl MixerCommand for Ping {
|
||||
|
||||
async fn execute(&self, ctx: &Context, interaction: ApplicationCommandInteraction) -> serenity::Result<()> {
|
||||
let content = "Pong!";
|
||||
interaction.create_interaction_response(&ctx.http, |response| {
|
||||
interaction.create_interaction_response(&ctx.http(), |response| {
|
||||
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| {
|
||||
message.content(content)
|
||||
message.content(content).ephemeral(true)
|
||||
})
|
||||
}).await?;
|
||||
|
||||
let follow1 = interaction.create_followup_message(&ctx.http(), |followup| {
|
||||
followup.content("followup1").ephemeral(true)
|
||||
}).await?;
|
||||
let follow2 = interaction.create_followup_message(&ctx.http(), |followup| {
|
||||
followup.content("followup2").ephemeral(true)
|
||||
}).await?;
|
||||
|
||||
interaction.delete_followup_message(&ctx.http(), follow1).await?;
|
||||
interaction.delete_followup_message(&ctx.http(), follow2).await?;
|
||||
|
||||
println!("{:#?}", interaction.get_interaction_response(&ctx.http).await?);
|
||||
// interaction.
|
||||
|
||||
println!("Interacted");
|
||||
|
||||
|
||||
126
src/bot/commands/preference.rs
Normal file
126
src/bot/commands/preference.rs
Normal file
@@ -0,0 +1,126 @@
|
||||
use std::str::FromStr;
|
||||
use serenity::builder::CreateApplicationCommand;
|
||||
use serenity::client::Context;
|
||||
use serenity::model::application::interaction::{
|
||||
application_command::ApplicationCommandInteraction,
|
||||
InteractionResponseType
|
||||
};
|
||||
use serenity::async_trait;
|
||||
use serenity::http::CacheHttp;
|
||||
use serenity::model::Permissions;
|
||||
use serenity::model::prelude::command::CommandOptionType;
|
||||
use serenity::model::prelude::interaction::application_command::CommandDataOptionValue::User;
|
||||
use crate::bot::commands::MixerCommand;
|
||||
use crate::database::DatabaseContainer;
|
||||
use crate::mixer::role::Role;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PreferenceCommand;
|
||||
|
||||
#[async_trait]
|
||||
impl MixerCommand for PreferenceCommand {
|
||||
fn name(&self) -> String {
|
||||
"preference".to_string()
|
||||
}
|
||||
|
||||
fn create(&self, command: &mut CreateApplicationCommand) {
|
||||
command.name(self.name()).description("Hello world!")
|
||||
.create_option(|option| {
|
||||
option.name("set").description("Set role preference for user")
|
||||
.kind(CommandOptionType::SubCommandGroup)
|
||||
.create_sub_option(|option| {
|
||||
option.name("flex").description("Set role preference for user")
|
||||
.kind(CommandOptionType::SubCommand)
|
||||
.create_sub_option(|option| {
|
||||
option.name("user").description("User to set preference for")
|
||||
.kind(CommandOptionType::User)
|
||||
.required(true)
|
||||
})
|
||||
})
|
||||
.create_sub_option(|option| {
|
||||
option.name("complex").description("Set role preference for user")
|
||||
.kind(CommandOptionType::SubCommand)
|
||||
.create_sub_option(|option| {
|
||||
option.name("user").description("User to set preference for")
|
||||
.kind(CommandOptionType::User)
|
||||
.required(true)
|
||||
})
|
||||
.create_sub_option(|option| {
|
||||
option.name("first").description("First role preference")
|
||||
.kind(CommandOptionType::String)
|
||||
.required(true)
|
||||
.add_string_choice("Tank", "tank")
|
||||
.add_string_choice("DPS", "dps")
|
||||
.add_string_choice("Support", "support")
|
||||
.add_string_choice("None", "none")
|
||||
})
|
||||
.create_sub_option(|option| {
|
||||
option.name("second").description("Second role preference")
|
||||
.kind(CommandOptionType::String)
|
||||
.required(true)
|
||||
.add_string_choice("Tank", "tank")
|
||||
.add_string_choice("DPS", "dps")
|
||||
.add_string_choice("Support", "support")
|
||||
.add_string_choice("None", "none")
|
||||
})
|
||||
.create_sub_option(|option| {
|
||||
option.name("third").description("Third role preference")
|
||||
.kind(CommandOptionType::String)
|
||||
.required(true)
|
||||
.add_string_choice("Tank", "tank")
|
||||
.add_string_choice("DPS", "dps")
|
||||
.add_string_choice("Support", "support")
|
||||
.add_string_choice("None", "none")
|
||||
})
|
||||
})
|
||||
})
|
||||
.default_member_permissions(Permissions::ADMINISTRATOR)
|
||||
.dm_permission(false);
|
||||
}
|
||||
|
||||
async fn execute(&self, ctx: &Context, interaction: ApplicationCommandInteraction) -> serenity::Result<()> {
|
||||
let user = match interaction.data.options.get(0).unwrap().options.get(0).unwrap().options.get(0).unwrap().resolved.as_ref().unwrap() {
|
||||
User(user, _) => user,
|
||||
_ => {
|
||||
interaction.create_interaction_response(&ctx.http, |response| {
|
||||
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| {
|
||||
message.content(format!("User not found")).ephemeral(true)
|
||||
})
|
||||
}).await?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
match interaction.data.options.get(0).unwrap().name.as_str() {
|
||||
"set" => {
|
||||
let data = ctx.data.read().await;
|
||||
let db = data.get::<DatabaseContainer>().unwrap().read().await;
|
||||
|
||||
match interaction.data.options.get(0).unwrap().options.get(0).unwrap().name.as_str() {
|
||||
"flex" => {
|
||||
db.update_player_preference(user.id, true, Role::None, Role::None, Role::None).await;
|
||||
},
|
||||
"complex" => {
|
||||
let role1 = Role::from_str(interaction.data.options.get(0).unwrap().options.get(0).unwrap().options.get(1).unwrap().value.as_ref().unwrap().as_str().unwrap()).unwrap();
|
||||
let role2 = Role::from_str(interaction.data.options.get(0).unwrap().options.get(0).unwrap().options.get(2).unwrap().value.as_ref().unwrap().as_str().unwrap()).unwrap();
|
||||
let role3 = Role::from_str(interaction.data.options.get(0).unwrap().options.get(0).unwrap().options.get(3).unwrap().value.as_ref().unwrap().as_str().unwrap()).unwrap();
|
||||
|
||||
db.update_player_preference(user.id, false, role1, role2, role3).await;
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
|
||||
interaction.create_interaction_response(&ctx.http, |response| {
|
||||
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| {
|
||||
message.content(format!("Preference set for {}", user.name)).ephemeral(true)
|
||||
})
|
||||
}).await?;
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
100
src/bot/commands/rank.rs
Normal file
100
src/bot/commands/rank.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
use std::str::FromStr;
|
||||
use serenity::builder::CreateApplicationCommand;
|
||||
use serenity::client::Context;
|
||||
use serenity::model::application::interaction::{application_command::ApplicationCommandInteraction, InteractionResponseType};
|
||||
use serenity::async_trait;
|
||||
use serenity::model::application::interaction::application_command::CommandDataOptionValue::User;
|
||||
use serenity::model::Permissions;
|
||||
use serenity::model::prelude::command::CommandOptionType;
|
||||
use crate::bot::commands::MixerCommand;
|
||||
use crate::database::DatabaseContainer;
|
||||
use crate::mixer::role::Role;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RankCommand;
|
||||
|
||||
#[async_trait]
|
||||
impl MixerCommand for RankCommand {
|
||||
fn name(&self) -> String {
|
||||
"rank".to_string()
|
||||
}
|
||||
|
||||
fn create(&self, command: &mut CreateApplicationCommand) {
|
||||
command.name(self.name()).description("Manage user ranks")
|
||||
.create_option(|option| {
|
||||
option.name("set")
|
||||
.description("Set a user's rank")
|
||||
.kind(CommandOptionType::SubCommand)
|
||||
.create_sub_option(|option| {
|
||||
option.name("user")
|
||||
.description("The user to set the rank for")
|
||||
.kind(CommandOptionType::User)
|
||||
.required(true)
|
||||
})
|
||||
.create_sub_option(|option| {
|
||||
option.name("role")
|
||||
.description("The role to set the rank for")
|
||||
.kind(CommandOptionType::String)
|
||||
.required(true)
|
||||
.add_string_choice("tank", "tank")
|
||||
.add_string_choice("dps", "dps")
|
||||
.add_string_choice("support", "support")
|
||||
})
|
||||
.create_sub_option(|option| {
|
||||
option.name("rank")
|
||||
.description("The rank to set")
|
||||
.kind(CommandOptionType::Integer)
|
||||
.required(true)
|
||||
})
|
||||
})
|
||||
.default_member_permissions(Permissions::ADMINISTRATOR)
|
||||
.dm_permission(false);
|
||||
}
|
||||
|
||||
async fn execute(&self, ctx: &Context, interaction: ApplicationCommandInteraction) -> serenity::Result<()> {
|
||||
match interaction.data.options.get(0).unwrap().name.as_str() {
|
||||
"set" => {
|
||||
let user = match interaction.data.options.get(0).unwrap().options.get(0).unwrap().resolved.as_ref().unwrap() {
|
||||
User(user, _) => user,
|
||||
_ => {
|
||||
interaction.create_interaction_response(&ctx.http, |response| {
|
||||
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| {
|
||||
message.content(format!("User not found")).ephemeral(true)
|
||||
})
|
||||
}).await?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let role = Role::from_str(interaction.data.options.get(0).unwrap().options.get(1).unwrap().value.as_ref().unwrap().as_str().unwrap()).unwrap();
|
||||
let rank = interaction.data.options.get(0).unwrap().options.get(2).unwrap().value.as_ref().unwrap().as_u64().unwrap();
|
||||
|
||||
if rank < 1 || rank > 5000 {
|
||||
interaction.create_interaction_response(&ctx.http, |response| {
|
||||
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| {
|
||||
message.content(format!("Rank must be between 1 and 5000")).ephemeral(true)
|
||||
})
|
||||
}).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
{
|
||||
let data = ctx.data.read().await;
|
||||
let db = data.get::<DatabaseContainer>().unwrap().read().await;
|
||||
db.update_player_rank(user.id, role, rank as f32).await;
|
||||
}
|
||||
|
||||
interaction.create_interaction_response(&ctx.http, |response| {
|
||||
response.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||
.interaction_response_data(|message| {
|
||||
message.content(format!("Setting rank for user {:?} to {:?} {:?}", user.id, role, rank))
|
||||
})
|
||||
}).await?;
|
||||
Ok(())
|
||||
},
|
||||
_ => Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
use serenity::client::{Context, EventHandler};
|
||||
use serenity::model::gateway::Ready;
|
||||
use serenity::async_trait;
|
||||
use serenity::model::application::command::Command;
|
||||
use serenity::model::application::interaction::Interaction;
|
||||
use crate::bot::MixerBotContainer;
|
||||
|
||||
pub struct Handler;
|
||||
|
||||
#[async_trait]
|
||||
impl EventHandler for Handler {
|
||||
async fn ready(&self, ctx: Context, data_about_bot: Ready) {
|
||||
println!("{} is connected!", data_about_bot.user.name);
|
||||
|
||||
let data = ctx.data.read().await;
|
||||
let bot_commands = &data.get::<MixerBotContainer>().unwrap().read().await.commands;
|
||||
Command::set_global_application_commands(&ctx.http, |commands| {
|
||||
for cmd in bot_commands.values() {
|
||||
commands.create_application_command(|command| {
|
||||
cmd.create(command);
|
||||
command
|
||||
});
|
||||
println!("Registered command \"{}\"", cmd.name())
|
||||
}
|
||||
commands
|
||||
}).await.unwrap();
|
||||
}
|
||||
|
||||
async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
|
||||
let data = ctx.data.read().await;
|
||||
let bot_commands = &data.get::<MixerBotContainer>().unwrap().read().await.commands;
|
||||
|
||||
match interaction {
|
||||
Interaction::ApplicationCommand(command) =>
|
||||
if let Some(mixer_command) = bot_commands.get(&command.data.name) {
|
||||
mixer_command.execute(&ctx, command).await.unwrap()
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
src/bot/handlers/command_handler.rs
Normal file
43
src/bot/handlers/command_handler.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use serenity::builder::CreateApplicationCommands;
|
||||
use serenity::client::Context;
|
||||
use serenity::model::application::interaction::application_command::ApplicationCommandInteraction;
|
||||
use serenity::prelude::TypeMapKey;
|
||||
use crate::bot::commands::MixerCommand;
|
||||
|
||||
pub struct MixerCommandHandler {
|
||||
commands: HashMap<String, Box<dyn MixerCommand>>,
|
||||
}
|
||||
|
||||
impl MixerCommandHandler {
|
||||
pub fn new(commands: HashMap<String, Box<dyn MixerCommand>>) -> Self {
|
||||
Self {
|
||||
commands
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_all(&self, create_commands: &mut CreateApplicationCommands) {
|
||||
self.commands.values().for_each(|command| {
|
||||
create_commands.create_application_command(|create_command| {
|
||||
command.create(create_command);
|
||||
create_command
|
||||
});
|
||||
println!("Registered command \"{}\"", command.name())
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn handle_command(&self, ctx: &Context, interaction: ApplicationCommandInteraction) -> serenity::Result<()> {
|
||||
if let Some(command) = self.commands.get(&interaction.data.name) {
|
||||
return command.execute(&ctx, interaction).await
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MixerCommandHandlerContainer;
|
||||
|
||||
impl TypeMapKey for MixerCommandHandlerContainer {
|
||||
type Value = Arc<MixerCommandHandler>;
|
||||
}
|
||||
57
src/bot/handlers/event_handler.rs
Normal file
57
src/bot/handlers/event_handler.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
use serenity::client::{Context, EventHandler};
|
||||
use serenity::model::gateway::Ready;
|
||||
use serenity::async_trait;
|
||||
use serenity::model::application::command::Command;
|
||||
use serenity::model::application::interaction::Interaction;
|
||||
use serenity::model::id::ChannelId;
|
||||
use serenity::model::prelude::{GuildId, VoiceState};
|
||||
use crate::database::DatabaseContainer;
|
||||
use crate::bot::handlers::command_handler::MixerCommandHandlerContainer;
|
||||
|
||||
pub struct Handler;
|
||||
|
||||
#[async_trait]
|
||||
impl EventHandler for Handler {
|
||||
async fn ready(&self, ctx: Context, data_about_bot: Ready) {
|
||||
println!("{} is connected!", data_about_bot.user.name);
|
||||
let data = ctx.data.read().await;
|
||||
let command_handler = data.get::<MixerCommandHandlerContainer>().unwrap();
|
||||
|
||||
Command::set_global_application_commands(&ctx.http, |commands| {
|
||||
command_handler.create_all(commands);
|
||||
commands
|
||||
}).await.unwrap();
|
||||
}
|
||||
|
||||
async fn voice_state_update(&self, ctx: Context, old: Option<VoiceState>, new: VoiceState) {
|
||||
let data = ctx.data.read().await;
|
||||
let db = data.get::<DatabaseContainer>().unwrap().read().await;
|
||||
|
||||
let user_id = new.user_id;
|
||||
let new_channel_id = new.channel_id.unwrap_or(ChannelId(0));
|
||||
let guild_id = new.guild_id.unwrap_or(GuildId(0));
|
||||
|
||||
match db.get_lobby_by_channel(guild_id, new_channel_id).await {
|
||||
Some(_) => {
|
||||
if !db.has_player(user_id).await {
|
||||
db.insert_player(user_id).await;
|
||||
}
|
||||
|
||||
println!("{} joined lobby", user_id);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
|
||||
let data = ctx.data.read().await;
|
||||
let command_handler = data.get::<MixerCommandHandlerContainer>().unwrap();
|
||||
|
||||
match interaction {
|
||||
Interaction::ApplicationCommand(command) => {
|
||||
command_handler.handle_command(&ctx, command).await.unwrap();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
src/bot/handlers/mod.rs
Normal file
2
src/bot/handlers/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod event_handler;
|
||||
pub mod command_handler;
|
||||
10
src/bot/interactions/mod.rs
Normal file
10
src/bot/interactions/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use serenity::client::Context;
|
||||
use serenity::async_trait;
|
||||
use serenity::model::application::interaction::message_component::MessageComponentInteraction;
|
||||
|
||||
#[async_trait]
|
||||
pub trait MixerInteraction: Sync + Send {
|
||||
fn custom_id(&self) -> String;
|
||||
// fn create(&self, command: &mut CreateApplicationCommand);
|
||||
async fn execute(&self, ctx: &Context, interaction: MessageComponentInteraction) -> serenity::Result<()>;
|
||||
}
|
||||
@@ -1,28 +1,26 @@
|
||||
pub mod commands;
|
||||
mod handler;
|
||||
pub mod interactions;
|
||||
mod handlers;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use serenity::{CacheAndHttp, Client};
|
||||
use serenity::client::bridge::gateway::ShardManager;
|
||||
use serenity::prelude::{GatewayIntents, TypeMap, TypeMapKey};
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
use tokio::sync::RwLock;
|
||||
use crate::bot::commands::MixerCommand;
|
||||
use crate::bot::handler::Handler;
|
||||
use crate::database::{MixerDatabase, DatabaseContainer};
|
||||
use crate::bot::handlers::command_handler::{MixerCommandHandler, MixerCommandHandlerContainer};
|
||||
use crate::bot::handlers::event_handler::Handler;
|
||||
|
||||
pub struct MixerBot {
|
||||
token: String,
|
||||
commands: HashMap<String, Box<dyn MixerCommand>>,
|
||||
commands: Option<HashMap<String, Box<dyn MixerCommand>>>,
|
||||
// lobbies: Vec<Lobby>
|
||||
}
|
||||
|
||||
struct ShardManagerContainer;
|
||||
struct MixerBotContainer;
|
||||
|
||||
|
||||
impl TypeMapKey for ShardManagerContainer {
|
||||
type Value = Arc<Mutex<ShardManager>>;
|
||||
}
|
||||
|
||||
impl TypeMapKey for MixerBotContainer {
|
||||
type Value = Arc<RwLock<MixerBot>>;
|
||||
}
|
||||
@@ -32,19 +30,24 @@ impl MixerBot {
|
||||
pub fn new(token: String) -> Self {
|
||||
Self {
|
||||
token,
|
||||
commands: HashMap::new(),
|
||||
commands: Some(HashMap::new()),
|
||||
// lobbies: vec![]
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start(self) -> serenity::Result<()> {
|
||||
let mut client = Client::builder(&self.token, GatewayIntents::empty()).event_handler(Handler).await?;
|
||||
pub async fn start(mut self) -> serenity::Result<()> {
|
||||
let mut client = Client::builder(&self.token, GatewayIntents::all()).event_handler(Handler).await?;
|
||||
|
||||
let bot;
|
||||
{
|
||||
let mut data = client.data.write().await;
|
||||
data.insert::<ShardManagerContainer>(client.shard_manager.clone());
|
||||
data.insert::<MixerCommandHandlerContainer>(Arc::new(MixerCommandHandler::new(self.commands.unwrap())));
|
||||
self.commands = None;
|
||||
|
||||
data.insert::<MixerBotContainer>(Arc::new(RwLock::new(self)));
|
||||
bot = data.get::<MixerBotContainer>().unwrap().clone();
|
||||
|
||||
let db = MixerDatabase::new("sqlite://database/data.db?mode=rwc").await;
|
||||
db.init("./database/script.sql").await;
|
||||
data.insert::<DatabaseContainer>(Arc::new(RwLock::new(db)));
|
||||
}
|
||||
|
||||
let shard_manager = client.shard_manager.clone();
|
||||
@@ -53,24 +56,26 @@ impl MixerBot {
|
||||
tokio::spawn(async move {
|
||||
tokio::signal::ctrl_c().await.expect("Could not register ctrl+c handler");
|
||||
|
||||
let data_ = data.clone();
|
||||
let data_ = data_.read().await;
|
||||
let bot = data_.get::<MixerBotContainer>().unwrap();
|
||||
bot.write().await.shutdown(data, cache_and_http).await;
|
||||
|
||||
shard_manager.lock().await.shutdown_all().await;
|
||||
});
|
||||
|
||||
|
||||
client.start().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_command(&mut self, command: Box<dyn MixerCommand>) -> &mut Self {
|
||||
self.commands.insert(command.name(), command);
|
||||
pub fn add_command<T: MixerCommand + 'static>(&mut self, command: T) -> &mut Self {
|
||||
self.commands.as_mut().unwrap().insert(command.name(), Box::new(command));
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn shutdown(&self, data: Arc<RwLock<TypeMap>>, cache_and_http: Arc<CacheAndHttp>) {
|
||||
println!("{:#?}", cache_and_http.http);
|
||||
pub async fn shutdown(&mut self, data: Arc<RwLock<TypeMap>>, cache_and_http: Arc<CacheAndHttp>) {
|
||||
|
||||
println!("Bot has been shutdown.");
|
||||
}
|
||||
}
|
||||
164
src/database/mod.rs
Normal file
164
src/database/mod.rs
Normal file
@@ -0,0 +1,164 @@
|
||||
pub mod models;
|
||||
|
||||
use std::sync::Arc;
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, ConnectionTrait, Database, DatabaseConnection, EntityTrait, IntoActiveModel, QueryFilter};
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use serenity::model::id::{ChannelId, UserId};
|
||||
use serenity::model::prelude::GuildId;
|
||||
use serenity::prelude::TypeMapKey;
|
||||
use tokio::fs;
|
||||
use tokio::sync::RwLock;
|
||||
use crate::mixer::role::Role;
|
||||
|
||||
pub struct MixerDatabase {
|
||||
connection: DatabaseConnection
|
||||
}
|
||||
|
||||
impl MixerDatabase {
|
||||
pub async fn new(url: &str) -> Self {
|
||||
Self {
|
||||
connection: Database::connect(url).await.expect("Could not connect to database")
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn init(&self, script_path: &str) {
|
||||
let script = fs::read_to_string(script_path).await.expect("Provided script path for creating database does not exist");
|
||||
|
||||
self.connection.execute_unprepared(&script).await.expect("Invalid database creating script provided");
|
||||
}
|
||||
|
||||
pub fn get_connection(&self) -> DatabaseConnection {
|
||||
self.connection.clone()
|
||||
}
|
||||
|
||||
//CREATE TABLE IF NOT EXISTS players (
|
||||
// id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
// discord_id INTEGER NOT NULL,
|
||||
// bn_name TEXT,
|
||||
// bn_tag TEXT,
|
||||
// tank REAL NOT NULL,
|
||||
// dps REAL NOT NULL,
|
||||
// support REAL NOT NULL,
|
||||
// UNIQUE (discord_id)
|
||||
// );
|
||||
pub async fn insert_player(&self, id: UserId) {
|
||||
let player = models::player::ActiveModel {
|
||||
discord_id: Set(id.0 as i64),
|
||||
bn_name: Set("".to_string()),
|
||||
bn_tag: Set("".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
player.insert(&self.connection)
|
||||
.await
|
||||
.expect("Could not insert player into database");
|
||||
}
|
||||
|
||||
pub async fn update_player_rank(&self, id: UserId, role: Role, rank: f32) {
|
||||
if !self.has_player(id).await {
|
||||
self.insert_player(id).await;
|
||||
}
|
||||
|
||||
let mut player = self.get_player(id).await.unwrap().into_active_model();
|
||||
|
||||
match role {
|
||||
Role::Tank => player.tank = Set(rank),
|
||||
Role::Dps => player.dps = Set(rank),
|
||||
Role::Support => player.support = Set(rank),
|
||||
Role::None => return
|
||||
}
|
||||
|
||||
player.update(&self.connection)
|
||||
.await
|
||||
.expect("Could not update player rank in database");
|
||||
}
|
||||
|
||||
pub async fn update_player_preference(&self, id: UserId, flex: bool, primary: Role, secondary: Role, tertiary: Role) {
|
||||
if !self.has_player(id).await {
|
||||
self.insert_player(id).await;
|
||||
}
|
||||
|
||||
let mut player = self.get_player(id).await.unwrap().into_active_model();
|
||||
|
||||
player.flex = Set(flex);
|
||||
player.primary_role = Set(primary.into());
|
||||
player.secondary_role = Set(secondary.into());
|
||||
player.tertiary_role = Set(tertiary.into());
|
||||
|
||||
player.update(&self.connection)
|
||||
.await
|
||||
.expect("Could not update player preference in database");
|
||||
}
|
||||
|
||||
pub async fn get_player(&self, id: UserId) -> Option<models::player::Model> {
|
||||
models::player::Entity::find()
|
||||
.filter(models::player::Column::DiscordId.eq(id.0))
|
||||
.one(&self.connection)
|
||||
.await
|
||||
.expect("Could not get player from database")
|
||||
}
|
||||
|
||||
pub async fn get_players(&self, ids: Vec<UserId>) -> Vec<models::player::Model> {
|
||||
models::player::Entity::find()
|
||||
.filter(models::player::Column::DiscordId.is_in(ids.iter().map(|id| id.0).collect::<Vec<u64>>()))
|
||||
.all(&self.connection)
|
||||
.await
|
||||
.expect("Could not get players from database")
|
||||
}
|
||||
|
||||
pub async fn has_player(&self, id: UserId) -> bool {
|
||||
models::player::Entity::find()
|
||||
.filter(models::player::Column::DiscordId.eq(id.0))
|
||||
.one(&self.connection)
|
||||
.await
|
||||
.expect("Could not get player from database")
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub async fn get_all_guild_lobbies(&self, guild_id: GuildId) -> Vec<models::lobby::Model> {
|
||||
models::lobby::Entity::find()
|
||||
.filter(models::lobby::Column::GuildId.eq(guild_id.0))
|
||||
.all(&self.connection)
|
||||
.await
|
||||
.expect("Could not get lobbies from database")
|
||||
}
|
||||
|
||||
pub async fn get_guild_lobby(&self, lobby_id: i32) -> Option<models::lobby::Model> {
|
||||
models::lobby::Entity::find_by_id(lobby_id)
|
||||
.one(&self.connection)
|
||||
.await
|
||||
.expect("Could not get lobby from database")
|
||||
}
|
||||
|
||||
pub async fn insert_guild_lobby(&self, guild_id: GuildId, main_voice_id: ChannelId, red_team_voice_id: ChannelId, blue_team_voice_id: ChannelId) {
|
||||
let lobby = models::lobby::ActiveModel {
|
||||
guild_id: Set(guild_id.0 as i64),
|
||||
main_voice_id: Set(main_voice_id.0 as i64),
|
||||
red_team_voice_id: Set(red_team_voice_id.0 as i64),
|
||||
blue_team_voice_id: Set(blue_team_voice_id.0 as i64),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
lobby.insert(&self.connection)
|
||||
.await
|
||||
.expect("Could not insert lobby into database");
|
||||
}
|
||||
|
||||
pub async fn get_lobby_by_channel(&self, guild_id: GuildId, channel_id: ChannelId) -> Option<models::lobby::Model> {
|
||||
models::lobby::Entity::find()
|
||||
.filter(models::lobby::Column::GuildId.eq(guild_id.0).and(
|
||||
models::lobby::Column::MainVoiceId.eq(channel_id.0 as i64)
|
||||
.or(models::lobby::Column::RedTeamVoiceId.eq(channel_id.0 as i64))
|
||||
.or(models::lobby::Column::BlueTeamVoiceId.eq(channel_id.0 as i64))
|
||||
))
|
||||
.one(&self.connection)
|
||||
.await
|
||||
.expect("Could not get lobby from database")
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DatabaseContainer;
|
||||
|
||||
impl TypeMapKey for DatabaseContainer {
|
||||
type Value = Arc<RwLock<MixerDatabase>>;
|
||||
}
|
||||
19
src/database/models/lobby.rs
Normal file
19
src/database/models/lobby.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "lobbies")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
|
||||
pub guild_id: i64,
|
||||
pub main_voice_id: i64,
|
||||
pub red_team_voice_id: i64,
|
||||
pub blue_team_voice_id: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
2
src/database/models/mod.rs
Normal file
2
src/database/models/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod lobby;
|
||||
pub mod player;
|
||||
35
src/database/models/player.rs
Normal file
35
src/database/models/player.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "players")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
|
||||
pub discord_id: i64,
|
||||
#[sea_orm(column_type = "Text")]
|
||||
pub bn_name: String,
|
||||
#[sea_orm(column_type = "Text")]
|
||||
pub bn_tag: String,
|
||||
#[sea_orm(default_value = 2500.0)]
|
||||
pub tank: f32,
|
||||
#[sea_orm(default_value = 2500.0)]
|
||||
pub dps: f32,
|
||||
#[sea_orm(default_value = 2500.0)]
|
||||
pub support: f32,
|
||||
|
||||
#[sea_orm(default_value = true)]
|
||||
pub flex: bool,
|
||||
#[sea_orm(default_value = -1)]
|
||||
pub primary_role: i32,
|
||||
#[sea_orm(default_value = -1)]
|
||||
pub secondary_role: i32,
|
||||
#[sea_orm(default_value = -1)]
|
||||
pub tertiary_role: i32,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
12
src/main.rs
12
src/main.rs
@@ -1,6 +1,11 @@
|
||||
mod bot;
|
||||
mod mixer;
|
||||
mod database;
|
||||
|
||||
use crate::bot::commands::ping::Ping;
|
||||
use crate::bot::commands::lobby::LobbyCommand;
|
||||
use crate::bot::commands::ping::PingCommand;
|
||||
use crate::bot::commands::preference::PreferenceCommand;
|
||||
use crate::bot::commands::rank::RankCommand;
|
||||
use crate::bot::MixerBot;
|
||||
|
||||
#[tokio::main]
|
||||
@@ -9,7 +14,10 @@ async fn main() -> serenity::Result<()> {
|
||||
"NTE2MzMyMzM2NzQ5NzQwMDUz.GiLPzQ.j5gIUGqx6vF6CFhJv8yizksDi-dOBqCvxR32EE".to_string()
|
||||
);
|
||||
|
||||
bot.add_command(Box::new(Ping));
|
||||
bot.add_command(PingCommand);
|
||||
bot.add_command(LobbyCommand);
|
||||
bot.add_command(RankCommand);
|
||||
bot.add_command(PreferenceCommand);
|
||||
|
||||
bot.start().await?;
|
||||
|
||||
|
||||
206
src/mixer/mixer.rs
Normal file
206
src/mixer/mixer.rs
Normal file
@@ -0,0 +1,206 @@
|
||||
use std::collections::HashMap;
|
||||
use crate::mixer::role::Role;
|
||||
use crate::database::models::player::{Model as Player, Model};
|
||||
use crate::mixer::role;
|
||||
|
||||
pub struct Mixer {
|
||||
players: Vec<Player>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MixedPlayer {
|
||||
pub role: Role,
|
||||
pub player: Player,
|
||||
}
|
||||
|
||||
impl Mixer {
|
||||
pub fn new(players: Vec<Player>) -> Self {
|
||||
Self {
|
||||
players
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_teams(self) -> Option<(Vec<MixedPlayer>, Vec<MixedPlayer>)> {
|
||||
let mut players = self.players.clone();
|
||||
|
||||
// TODO! pass in the original priorities
|
||||
// let original_priorities = players.iter().cloned().map(|player| (player.id, 1)).collect::<HashMap<i32, i32>>();
|
||||
|
||||
let mut team1 = Vec::new();
|
||||
let mut team2 = Vec::new();
|
||||
|
||||
for i in 0..10 {
|
||||
let priorities = self.get_players_priority(&players, &team1, &team2);
|
||||
|
||||
if let Some(player) = self.get_highest_priority_player(&players, &priorities) {
|
||||
if i % 2 == 0 {
|
||||
team1.push(player.clone());
|
||||
}
|
||||
else {
|
||||
team2.push(player.clone());
|
||||
}
|
||||
|
||||
players.retain(|p| p.id != player.player.id);
|
||||
}
|
||||
else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
Some((team1, team2))
|
||||
}
|
||||
|
||||
fn get_players_priority(&self, players: &Vec<Player>, team1: &Vec<MixedPlayer>, team2: &Vec<MixedPlayer>) -> HashMap<(Role, i32), f32> {
|
||||
let mut priorities = HashMap::new();
|
||||
|
||||
if Self::calculate_priorities(players, team1, team2, Role::Tank, &mut priorities) {
|
||||
return priorities;
|
||||
}
|
||||
|
||||
if Self::calculate_priorities(players, team1, team2, Role::Dps, &mut priorities) {
|
||||
return priorities;
|
||||
}
|
||||
|
||||
if Self::calculate_priorities(players, team1, team2, Role::Support, &mut priorities) {
|
||||
return priorities;
|
||||
}
|
||||
|
||||
priorities
|
||||
}
|
||||
|
||||
fn average_rank(team1: &Vec<MixedPlayer>, team2: &Vec<MixedPlayer>, role: Role) -> f32 {
|
||||
let selected_players = team1.iter().filter(|player| player.role == role)
|
||||
.chain(team2.iter().filter(|player| player.role == role)).clone().collect::<Vec<&MixedPlayer>>();
|
||||
|
||||
selected_players.iter().map(|player| {
|
||||
match player.role {
|
||||
Role::Tank => player.player.tank,
|
||||
Role::Dps => player.player.dps,
|
||||
Role::Support => player.player.support,
|
||||
_ => 0.0
|
||||
}
|
||||
}).sum::<f32>() / selected_players.len() as f32
|
||||
}
|
||||
|
||||
fn calculate_priorities(players: &Vec<Model>, team1: &Vec<MixedPlayer>, team2: &Vec<MixedPlayer>, expected: Role, priorities: &mut HashMap<(Role, i32), f32>) -> bool {
|
||||
let group_coefficients = vec![10.0, 50.0, 100.0, 150.0, 125.0];
|
||||
|
||||
let team1_roles = team1.iter().map(|player| player.role).collect::<Vec<Role>>();
|
||||
let team2_roles = team2.iter().map(|player| player.role).collect::<Vec<Role>>();
|
||||
|
||||
let team1_role_count = team1_roles.iter().filter(|role| **role == expected).count();
|
||||
let team2_role_count = team2_roles.iter().filter(|role| **role == expected).count();
|
||||
let prioritize_role = team1_role_count == 0 || team2_role_count == 0;
|
||||
|
||||
if !prioritize_role {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
let otp = players.iter().filter(|player|
|
||||
player.primary_role == expected && player.secondary_role == Role::None && player.tertiary_role == Role::None
|
||||
).cloned().collect::<Vec<Player>>();
|
||||
|
||||
let primary = players.iter().filter(|player|
|
||||
player.primary_role == expected && (player.secondary_role != Role::None || player.tertiary_role == Role::None)
|
||||
).cloned().collect::<Vec<Player>>();
|
||||
|
||||
let secondary = players.iter().filter(|player|
|
||||
player.secondary_role == expected
|
||||
).cloned().collect::<Vec<Player>>();
|
||||
|
||||
let flex = players.iter().filter(|player|
|
||||
player.flex
|
||||
).cloned().collect::<Vec<Player>>();
|
||||
|
||||
let tertiary = players.iter().filter(|player|
|
||||
player.tertiary_role == expected
|
||||
).cloned().collect::<Vec<Player>>();
|
||||
|
||||
|
||||
let average_tank_skill = Self::average_rank(team1, team2, expected);
|
||||
|
||||
for player in otp {
|
||||
let rank = match expected {
|
||||
Role::Tank => player.tank,
|
||||
Role::Dps => player.dps,
|
||||
Role::Support => player.support,
|
||||
_ => 0.0
|
||||
};
|
||||
let skill_difference = (rank - average_tank_skill).abs();
|
||||
|
||||
priorities.insert((expected, player.id), rank / group_coefficients[0] / (skill_difference + 1.0));
|
||||
}
|
||||
|
||||
for player in primary {
|
||||
let rank = match expected {
|
||||
Role::Tank => player.tank,
|
||||
Role::Dps => player.dps,
|
||||
Role::Support => player.support,
|
||||
_ => 0.0
|
||||
};
|
||||
let skill_difference = (rank - average_tank_skill).abs();
|
||||
|
||||
priorities.insert((expected, player.id), rank / group_coefficients[1] / (skill_difference + 1.0));
|
||||
}
|
||||
|
||||
for player in secondary {
|
||||
let rank = match expected {
|
||||
Role::Tank => player.tank,
|
||||
Role::Dps => player.dps,
|
||||
Role::Support => player.support,
|
||||
_ => 0.0
|
||||
};
|
||||
let skill_difference = (rank - average_tank_skill).abs();
|
||||
|
||||
priorities.insert((expected, player.id), rank / group_coefficients[2] / (skill_difference + 1.0));
|
||||
}
|
||||
|
||||
for player in tertiary {
|
||||
let rank = match expected {
|
||||
Role::Tank => player.tank,
|
||||
Role::Dps => player.dps,
|
||||
Role::Support => player.support,
|
||||
_ => 0.0
|
||||
};
|
||||
let skill_difference = (rank - average_tank_skill).abs();
|
||||
|
||||
priorities.insert((expected, player.id), rank / group_coefficients[3] / (skill_difference + 1.0));
|
||||
}
|
||||
|
||||
for player in flex {
|
||||
let rank = match expected {
|
||||
Role::Tank => player.tank,
|
||||
Role::Dps => player.dps,
|
||||
Role::Support => player.support,
|
||||
_ => 0.0
|
||||
};
|
||||
let skill_difference = (rank - average_tank_skill).abs();
|
||||
|
||||
priorities.insert((expected, player.id), rank / group_coefficients[4] / (skill_difference + 1.0));
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn get_highest_priority_player(&self, players: &Vec<Model>, priorities: &HashMap<(Role, i32), f32>) -> Option<MixedPlayer> {
|
||||
let mut highest_priority = 0.0;
|
||||
let mut highest_priority_player = None;
|
||||
|
||||
for role in vec![Role::Tank, Role::Dps, Role::Support] {
|
||||
for player in players {
|
||||
if let Some(priority) = priorities.get(&(role, player.id)) {
|
||||
if *priority > highest_priority {
|
||||
highest_priority = *priority;
|
||||
highest_priority_player = Some(MixedPlayer {
|
||||
role,
|
||||
player: player.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
highest_priority_player
|
||||
}
|
||||
}
|
||||
2
src/mixer/mod.rs
Normal file
2
src/mixer/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod mixer;
|
||||
pub mod role;
|
||||
80
src/mixer/role.rs
Normal file
80
src/mixer/role.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||
pub enum Role {
|
||||
Tank,
|
||||
Dps,
|
||||
Support,
|
||||
None
|
||||
}
|
||||
|
||||
impl PartialEq<i32> for Role {
|
||||
fn eq(&self, other: &i32) -> bool {
|
||||
match self {
|
||||
Role::Tank => *other == 0,
|
||||
Role::Dps => *other == 1,
|
||||
Role::Support => *other == 2,
|
||||
Role::None => *other == -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<Role> for i32 {
|
||||
fn eq(&self, other: &Role) -> bool {
|
||||
match other {
|
||||
Role::Tank => *self == 0,
|
||||
Role::Dps => *self == 1,
|
||||
Role::Support => *self == 2,
|
||||
Role::None => *self == -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Role> for i32 {
|
||||
fn from(role: &Role) -> Self {
|
||||
match role {
|
||||
Role::Tank => 0,
|
||||
Role::Dps => 1,
|
||||
Role::Support => 2,
|
||||
Role::None => -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i32> for Role {
|
||||
fn from(i: i32) -> Self {
|
||||
match i {
|
||||
0 => Role::Tank,
|
||||
1 => Role::Dps,
|
||||
2 => Role::Support,
|
||||
_ => Role::None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<i32> for Role {
|
||||
fn into(self) -> i32 {
|
||||
match self {
|
||||
Role::Tank => 0,
|
||||
Role::Dps => 1,
|
||||
Role::Support => 2,
|
||||
Role::None => -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Role {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"tank" => Ok(Role::Tank),
|
||||
"dps" => Ok(Role::Dps),
|
||||
"support" => Ok(Role::Support),
|
||||
"none" => Ok(Role::None),
|
||||
_ => Ok(Role::None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user