Merge branch 'sharp/fix-full-server' into 'master'

Allow mods/admins to log in when server is full.

See merge request veloren/veloren!3600
This commit is contained in:
Joshua Yanovski 2022-09-07 05:21:08 +00:00
commit c95c08ee54
13 changed files with 156 additions and 159 deletions

View File

@ -48,6 +48,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed bug where the view distance selection was not immediately applied to entity syncing when
first joining a server and when changing the view distance (previously this required moving to a
new chunk for the initial setting or subsequent change to apply).
- Moderators and admins are no longer blocked from logging in when there are too many players.
## [0.13.0] - 2022-07-23

View File

@ -51,19 +51,20 @@ fn main() {
// Create a client.
let mut client = runtime
.block_on(Client::new(addr, runtime2, &mut None))
.block_on(Client::new(
addr,
runtime2,
&mut None,
&username,
&password,
|provider| provider == "https://auth.veloren.net",
))
.expect("Failed to create client instance");
println!("Server info: {:?}", client.server_info());
println!("Players online: {:?}", client.players().collect::<Vec<_>>());
runtime
.block_on(client.register(username, password, |provider| {
provider == "https://auth.veloren.net"
}))
.unwrap();
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
loop {

View File

@ -13,6 +13,7 @@ mod settings;
mod tui;
use common::comp::body::humanoid::Body;
use common_net::msg::ServerInfo;
use settings::Settings;
use tui::Cmd;
@ -46,31 +47,47 @@ pub fn main() {
pub struct BotClient {
settings: Settings,
runtime: Arc<Runtime>,
menu_client: Client,
server_info: ServerInfo,
bot_clients: HashMap<String, Client>,
clock: Clock,
}
pub fn make_client(runtime: &Arc<Runtime>, server: &str) -> Client {
pub fn make_client(
runtime: &Arc<Runtime>,
server: &str,
server_info: &mut Option<ServerInfo>,
username: &str,
password: &str,
) -> Option<Client> {
let runtime_clone = Arc::clone(runtime);
let addr = ConnectionArgs::Tcp {
prefer_ipv6: false,
hostname: server.to_owned(),
};
runtime
.block_on(Client::new(addr, runtime_clone, &mut None))
.expect("Failed to connect to server")
.block_on(Client::new(
addr,
runtime_clone,
server_info,
username,
password,
|_| true,
))
.ok()
}
impl BotClient {
pub fn new(settings: Settings) -> BotClient {
let runtime = Arc::new(Runtime::new().unwrap());
let menu_client: Client = make_client(&runtime, &settings.server);
let mut server_info = None;
// Don't care if we connect, just trying to grab the server info.
let _ = make_client(&runtime, &settings.server, &mut server_info, "", "");
let server_info = server_info.expect("Failed to connect to server.");
let clock = Clock::new(Duration::from_secs_f64(1.0 / 60.0));
BotClient {
settings,
runtime,
menu_client,
server_info,
bot_clients: HashMap::new(),
clock,
}
@ -106,7 +123,7 @@ impl BotClient {
None => vec![prefix.to_string()],
};
info!("usernames: {:?}", usernames);
if let Some(auth_addr) = self.menu_client.server_info().auth_provider.as_ref() {
if let Some(auth_addr) = self.server_info.auth_provider.as_ref() {
let (scheme, authority) = auth_addr.split_once("://").expect("invalid auth url");
let scheme = scheme
.parse::<authc::Scheme>()
@ -156,20 +173,16 @@ impl BotClient {
for cred in creds.iter() {
let runtime = Arc::clone(&self.runtime);
let server = self.settings.server.clone();
let server = &self.settings.server;
// TODO: log the clients in in parallel instead of in series
let client = self
.bot_clients
.entry(cred.username.clone())
.or_insert_with(|| make_client(&runtime, &server));
.or_insert_with(|| {
make_client(&runtime, server, &mut None, &cred.username, &cred.password)
.expect("Failed to connect to server")
});
// TODO: log the clients in in parallel instead of in series
if let Err(e) = runtime.block_on(client.register(
cred.username.clone(),
cred.password.clone(),
|_| true,
)) {
warn!("error logging in {:?}: {:?}", cred.username, e);
}
let body = BotClient::create_default_body();
client.create_character(
cred.username.clone(),

View File

@ -108,15 +108,17 @@ fn run_client(
hostname: "localhost".into(),
};
let runtime_clone = Arc::clone(&runtime);
let mut client = runtime
.block_on(Client::new(addr, runtime_clone, &mut None))
.expect("Failed to connect to the server");
// Login
// NOTE: use a no-auth server
runtime
.block_on(client.register(username.clone(), String::new(), |_| false))
.expect("Failed to log in");
let mut client = runtime
.block_on(Client::new(
addr,
runtime_clone,
&mut None,
&username,
"",
|_| false,
))
.expect("Failed to connect to the server");
let mut clock = common::clock::Clock::new(Duration::from_secs_f32(1.0 / 30.0));

View File

@ -275,6 +275,9 @@ impl Client {
runtime: Arc<Runtime>,
// TODO: refactor to avoid needing to use this out parameter
mismatched_server_info: &mut Option<ServerInfo>,
username: &str,
password: &str,
auth_trusted: impl FnMut(&str) -> bool,
) -> Result<Self, Error> {
let network = Network::new(Pid::new(), &runtime);
@ -317,15 +320,24 @@ impl Client {
common::util::GIT_HASH.to_string(),
common::util::GIT_DATE.to_string(),
);
}
// Pass the server info back to the caller to ensure they can access it even
// if this function errors.
mem::swap(mismatched_server_info, &mut Some(server_info.clone()));
}
debug!("Auth Server: {:?}", server_info.auth_provider);
ping_stream.send(PingMsg::Ping)?;
// Register client
Self::register(
username,
password,
auth_trusted,
&server_info,
&mut register_stream,
)
.await?;
// Wait for initial sync
let mut ping_interval = tokio::time::interval(Duration::from_secs(1));
let (
@ -632,7 +644,7 @@ impl Client {
let map_bounds = Vec2::new(sea_level, max_height);
debug!("Done preparing image...");
Ok((
(
state,
lod_base,
lod_alt,
@ -644,16 +656,15 @@ impl Client {
component_recipe_book,
max_group_size,
client_timeout,
))
)
},
ServerInit::TooManyPlayers => Err(Error::TooManyPlayers),
}?;
};
ping_stream.send(PingMsg::Ping)?;
debug!("Initial sync done");
Ok(Self {
registered: false,
registered: true,
presence: None,
runtime,
server_info,
@ -722,14 +733,15 @@ impl Client {
}
/// Request a state transition to `ClientState::Registered`.
pub async fn register(
&mut self,
username: String,
password: String,
async fn register(
username: &str,
password: &str,
mut auth_trusted: impl FnMut(&str) -> bool,
server_info: &ServerInfo,
register_stream: &mut Stream,
) -> Result<(), Error> {
// Authentication
let token_or_username = match &self.server_info.auth_provider {
let token_or_username = match &server_info.auth_provider {
Some(addr) => {
// Query whether this is a trusted auth server
if auth_trusted(addr) {
@ -749,26 +761,29 @@ impl Client {
};
Ok(authc::AuthClient::new(scheme, authority)?
.sign_in(&username, &password)
.sign_in(username, password)
.await?
.serialize())
} else {
Err(Error::AuthServerNotTrusted)
}
},
None => Ok(username),
None => Ok(username.to_owned()),
}?;
self.send_msg_err(ClientRegister { token_or_username })?;
debug!("Registering client...");
match self.register_stream.recv::<ServerRegisterAnswer>().await? {
register_stream.send(ClientRegister { token_or_username })?;
match register_stream.recv::<ServerRegisterAnswer>().await? {
Err(RegisterError::AuthError(err)) => Err(Error::AuthErr(err)),
Err(RegisterError::InvalidCharacter) => Err(Error::InvalidCharacter),
Err(RegisterError::NotOnWhitelist) => Err(Error::NotOnWhitelist),
Err(RegisterError::Kicked(err)) => Err(Error::Kicked(err)),
Err(RegisterError::Banned(reason)) => Err(Error::Banned(reason)),
Err(RegisterError::TooManyPlayers) => Err(Error::TooManyPlayers),
Ok(()) => {
self.registered = true;
debug!("Client registered successfully.");
Ok(())
},
}
@ -2917,6 +2932,9 @@ mod tests {
let runtime = Arc::new(Runtime::new().unwrap());
let runtime2 = Arc::clone(&runtime);
let username = "Foo";
let password = "Bar";
let auth_server = "auth.veloren.net";
let veloren_client: Result<Client, Error> = runtime.block_on(Client::new(
ConnectionArgs::Tcp {
hostname: "127.0.0.1:9000".to_owned(),
@ -2924,18 +2942,12 @@ mod tests {
},
runtime2,
&mut None,
username,
password,
|suggestion: &str| suggestion == auth_server,
));
let _ = veloren_client.map(|mut client| {
//register
let username: String = "Foo".to_string();
let password: String = "Bar".to_string();
let auth_server: String = "auth.veloren.net".to_string();
let _result: Result<(), Error> =
runtime.block_on(client.register(username, password, |suggestion: &str| {
suggestion == auth_server
}));
//clock
let mut clock = Clock::new(Duration::from_secs_f64(SPT));

View File

@ -55,7 +55,6 @@ pub struct ServerInfo {
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(clippy::large_enum_variant)]
pub enum ServerInit {
TooManyPlayers,
GameSync {
entity_package: sync::EntityPackage<EcsCompPacket>,
time_of_day: TimeOfDay,
@ -275,6 +274,7 @@ pub enum RegisterError {
Kicked(String),
InvalidCharacter,
NotOnWhitelist,
TooManyPlayers,
//TODO: InvalidAlias,
}

View File

@ -42,9 +42,6 @@ macro_rules! synced_components {
shockwave: Shockwave,
beam_segment: BeamSegment,
alignment: Alignment,
// TODO: evaluate if this is used on the client,
// and if so what it is used for
player: Player,
// TODO: change this to `SyncFrom::ClientEntity` and sync the bare minimum
// from other entities (e.g. just keys needed to show appearance
// based on their loadout). Also, it looks like this actually has
@ -210,10 +207,6 @@ impl NetSync for Alignment {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}
impl NetSync for Player {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}
impl NetSync for Inventory {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}

View File

@ -67,7 +67,7 @@ use crate::{
presence::{Presence, RegionSubscription, RepositionOnChunkLoad},
rtsim::RtSim,
state_ext::StateExt,
sys::sentinel::{DeletedEntities, TrackedStorages},
sys::sentinel::DeletedEntities,
};
use censor::Censor;
#[cfg(not(feature = "worldgen"))]
@ -79,7 +79,6 @@ use common::{
cmd::ServerChatCommand,
comp,
event::{EventBus, ServerEvent},
recipe::{default_component_recipe_book, default_recipe_book},
resources::{BattleMode, Time, TimeOfDay},
rtsim::RtSimEntity,
slowjob::SlowJobPool,
@ -88,9 +87,7 @@ use common::{
};
use common_ecs::run_now;
use common_net::{
msg::{
ClientType, DisconnectReason, ServerGeneral, ServerInfo, ServerInit, ServerMsg, WorldMapMsg,
},
msg::{ClientType, DisconnectReason, ServerGeneral, ServerInfo, ServerMsg},
sync::WorldSyncExt,
};
use common_state::{BuildAreas, State};
@ -103,7 +100,7 @@ use persistence::{
};
use prometheus::Registry;
use prometheus_hyper::Server as PrometheusServer;
use specs::{join::Join, Builder, Entity as EcsEntity, Entity, SystemData, WorldExt};
use specs::{join::Join, Builder, Entity as EcsEntity, Entity, WorldExt};
use std::{
i32,
ops::{Deref, DerefMut},
@ -201,7 +198,6 @@ pub struct Server {
state: State,
world: Arc<World>,
index: IndexOwned,
map: WorldMapMsg,
connection_handler: ConnectionHandler,
@ -387,6 +383,8 @@ impl Server {
pois: Vec::new(),
};
state.ecs_mut().insert(map);
#[cfg(feature = "worldgen")]
let spawn_point = SpawnPoint({
let index = index.as_index_ref();
@ -562,8 +560,6 @@ impl Server {
state,
world,
index,
map,
connection_handler,
runtime,
@ -630,9 +626,6 @@ impl Server {
/// Get a reference to the server's world.
pub fn world(&self) -> &World { &self.world }
/// Get a reference to the server's world map.
pub fn map(&self) -> &WorldMapMsg { &self.map }
/// Execute a single server tick, handle input and update the game state by
/// the given duration.
pub fn tick(&mut self, _input: Input, dt: Duration) -> Result<Vec<Event>, Error> {
@ -1029,19 +1022,7 @@ impl Server {
.map(|mut t| t.maintain());
}
fn initialize_client(
&mut self,
client: connection_handler::IncomingClient,
) -> Result<Option<Entity>, Error> {
if self.settings().max_players <= self.state.ecs().read_storage::<Client>().join().count() {
trace!(
?client.participant,
"to many players, wont allow participant to connect"
);
client.send(ServerInit::TooManyPlayers)?;
return Ok(None);
}
fn initialize_client(&mut self, client: connection_handler::IncomingClient) -> Entity {
let entity = self
.state
.ecs_mut()
@ -1053,43 +1034,7 @@ impl Server {
.read_resource::<metrics::PlayerMetrics>()
.clients_connected
.inc();
// Send client all the tracked components currently attached to its entity as
// well as synced resources (currently only `TimeOfDay`)
debug!("Starting initial sync with client.");
self.state
.ecs()
.read_storage::<Client>()
.get(entity)
.expect(
"We just created this entity with a Client component using build(), and we have \
&mut access to the ecs so it can't have been deleted yet.",
)
.send(ServerInit::GameSync {
// Send client their entity
entity_package: TrackedStorages::fetch(self.state.ecs())
.create_entity_package(entity, None, None, None)
.expect(
"We just created this entity as marked() (using create_entity_synced) so \
it definitely has a uid",
),
time_of_day: *self.state.ecs().read_resource(),
max_group_size: self.settings().max_player_group_size,
client_timeout: self.settings().client_timeout,
world_map: self.map.clone(),
recipe_book: default_recipe_book().cloned(),
component_recipe_book: default_component_recipe_book().cloned(),
material_stats: (&*self
.state
.ecs()
.read_resource::<comp::item::MaterialStatManifest>())
.clone(),
ability_map: (&*self
.state
.ecs()
.read_resource::<comp::item::tool::AbilityMap>())
.clone(),
})?;
Ok(Some(entity))
entity
}
/// Disconnects all clients if requested by either an admin command or
@ -1155,16 +1100,8 @@ impl Server {
}
while let Ok(incoming) = self.connection_handler.client_receiver.try_recv() {
match self.initialize_client(incoming) {
Ok(None) => (),
Ok(Some(entity)) => {
let entity = self.initialize_client(incoming);
frontend_events.push(Event::ClientConnected { entity });
debug!("Done initial sync with client.");
},
Err(e) => {
debug!(?e, "failed initializing a new client");
},
}
}
}

View File

@ -97,7 +97,7 @@ impl LoginProvider {
PendingLogin { pending_r }
}
pub fn login(
pub(crate) fn login(
&mut self,
pending: &mut PendingLogin,
#[cfg(feature = "plugins")] world: &EcsWorld,
@ -105,6 +105,7 @@ impl LoginProvider {
admins: &HashMap<Uuid, AdminRecord>,
whitelist: &HashMap<Uuid, WhitelistRecord>,
banlist: &HashMap<Uuid, BanEntry>,
player_count_exceeded: bool,
) -> Option<Result<(String, Uuid), RegisterError>> {
match pending.pending_r.try_recv() {
Ok(Err(e)) => Some(Err(e)),
@ -137,6 +138,11 @@ impl LoginProvider {
return Some(Err(RegisterError::NotOnWhitelist));
}
// non-admins can only join if the player count has not been exceeded.
if admin.is_none() && player_count_exceeded {
return Some(Err(RegisterError::TooManyPlayers));
}
#[cfg(feature = "plugins")]
{
// Plugin player join hooks execute for all players, but are only allowed to

View File

@ -6,7 +6,7 @@ use crate::{
};
use common::{
calendar::Calendar,
comp::{Collider, ForceUpdate, InventoryUpdate, Last, Ori, Pos, Vel},
comp::{Collider, ForceUpdate, InventoryUpdate, Last, Ori, Player, Pos, Vel},
event::EventBus,
outcome::Outcome,
region::{Event as RegionEvent, RegionMap},
@ -40,6 +40,7 @@ impl<'a> System<'a> for Sys {
ReadStorage<'a, Vel>,
ReadStorage<'a, Ori>,
ReadStorage<'a, RegionSubscription>,
ReadStorage<'a, Player>,
ReadStorage<'a, Presence>,
ReadStorage<'a, Client>,
WriteStorage<'a, Last<Pos>>,
@ -70,6 +71,7 @@ impl<'a> System<'a> for Sys {
velocities,
orientations,
subscriptions,
players,
presences,
clients,
mut last_pos,
@ -88,7 +90,6 @@ impl<'a> System<'a> for Sys {
let uids = &tracked_storages.uid;
let colliders = &tracked_storages.collider;
let inventories = &tracked_storages.inventory;
let players = &tracked_storages.player;
let is_rider = &tracked_storages.is_rider;
// To send entity updates

View File

@ -2,17 +2,20 @@ use crate::{
client::Client,
login_provider::{LoginProvider, PendingLogin},
metrics::PlayerMetrics,
sys::sentinel::TrackedStorages,
EditableSettings, Settings,
};
use common::{
comp::{Admin, Player, Stats},
comp::{self, Admin, Player, Stats},
event::{EventBus, ServerEvent},
recipe::{default_component_recipe_book, default_recipe_book},
resources::TimeOfDay,
uid::{Uid, UidAllocator},
};
use common_ecs::{Job, Origin, Phase, System};
use common_net::msg::{
CharacterInfo, ClientRegister, DisconnectReason, PlayerInfo, PlayerListUpdate, RegisterError,
ServerGeneral,
ServerGeneral, ServerInit, WorldMapMsg,
};
use hashbrown::HashMap;
use plugin_api::Health;
@ -20,7 +23,7 @@ use specs::{
shred::ResourceId, storage::StorageEntry, Entities, Join, Read, ReadExpect, ReadStorage,
SystemData, World, WriteExpect, WriteStorage,
};
use tracing::trace;
use tracing::{debug, trace};
#[cfg(feature = "plugins")]
use {common_state::plugin::memory_manager::EcsWorld, common_state::plugin::PluginMgr};
@ -40,6 +43,11 @@ pub struct ReadData<'a> {
player_metrics: ReadExpect<'a, PlayerMetrics>,
settings: ReadExpect<'a, Settings>,
editable_settings: ReadExpect<'a, EditableSettings>,
time_of_day: Read<'a, TimeOfDay>,
material_stats: ReadExpect<'a, comp::item::MaterialStatManifest>,
ability_map: ReadExpect<'a, comp::item::tool::AbilityMap>,
map: ReadExpect<'a, WorldMapMsg>,
trackers: TrackedStorages<'a>,
_healths: ReadStorage<'a, Health>, // used by plugin feature
_plugin_mgr: ReadPlugin<'a>, // used by plugin feature
_uid_allocator: Read<'a, UidAllocator>, // used by plugin feature
@ -107,6 +115,8 @@ impl<'a> System<'a> for Sys {
let mut finished_pending = vec![];
let mut retries = vec![];
let mut player_count = player_list.len();
let max_players = read_data.settings.max_players;
for (entity, client, pending) in
(&read_data.entities, &read_data.clients, &mut pending_logins).join()
{
@ -129,6 +139,7 @@ impl<'a> System<'a> for Sys {
&*read_data.editable_settings.admins,
&*read_data.editable_settings.whitelist,
&*read_data.editable_settings.banlist,
player_count >= max_players,
) {
None => return Ok(()),
Some(r) => {
@ -190,6 +201,7 @@ impl<'a> System<'a> for Sys {
if let Ok(StorageEntry::Vacant(v)) = players.entry(entity) {
// Add Player component to this client, if the entity exists.
v.insert(player);
player_count += 1;
read_data.player_metrics.players_connected.inc();
// Give the Admin component to the player if their name exists in
@ -203,6 +215,31 @@ impl<'a> System<'a> for Sys {
// Tell the client its request was successful.
client.send(Ok(()))?;
// Send client all the tracked components currently attached to its entity as
// well as synced resources (currently only `TimeOfDay`)
debug!("Starting initial sync with client.");
client.send(ServerInit::GameSync {
// Send client their entity
entity_package:
read_data.trackers
.create_entity_package(entity, None, None, None)
// NOTE: We are apparently okay with crashing if a uid is removed from
// a non-logged-in player without removing the whole thing.
.expect(
"We created this entity as marked() (using create_entity_synced) so \
it definitely has a uid",
),
time_of_day: *read_data.time_of_day,
max_group_size: read_data.settings.max_player_group_size,
client_timeout: read_data.settings.client_timeout,
world_map: (&*read_data.map).clone(),
recipe_book: default_recipe_book().cloned(),
component_recipe_book: default_component_recipe_book().cloned(),
material_stats: (&*read_data.material_stats).clone(),
ability_map: (&*read_data.ability_map).clone(),
})?;
debug!("Done initial sync with client.");
// Send initial player list
client.send(ServerGeneral::PlayerListUpdate(PlayerListUpdate::Init(
player_list.clone(),

View File

@ -1278,7 +1278,6 @@ impl Hud {
let bodies = ecs.read_storage::<comp::Body>();
let items = ecs.read_storage::<Item>();
let inventories = ecs.read_storage::<comp::Inventory>();
let players = ecs.read_storage::<comp::Player>();
let msm = ecs.read_resource::<MaterialStatManifest>();
let entities = ecs.entities();
let me = info.viewpoint_entity;
@ -1978,7 +1977,6 @@ impl Hud {
&mut hp_floater_lists,
&uids,
&inventories,
players.maybe(),
poises.maybe(),
(alignments.maybe(), is_mount.maybe()),
)
@ -2002,7 +2000,6 @@ impl Hud {
hpfl,
uid,
inventory,
player,
poise,
(alignment, is_mount),
)| {
@ -2013,7 +2010,8 @@ impl Hud {
// TODO: once the site2 rework lands and merchants have dedicated stalls or
// buildings, they no longer need to be emphasized via the higher overhead
// text radius relative to other NPCs
let is_merchant = stats.name == "Merchant" && player.is_none();
let is_merchant =
stats.name == "Merchant" && client.player_list().get(uid).is_none();
let dist_sqr = pos.distance_squared(player_pos);
// Determine whether to display nametag and healthbar based on whether the
// entity has been damaged, is targeted/selected, or is in your group

View File

@ -77,17 +77,13 @@ impl ClientInit {
connection_args.clone(),
Arc::clone(&runtime2),
&mut mismatched_server_info,
&username,
&password,
trust_fn,
)
.await
{
Ok(mut client) => {
if let Err(e) = client.register(username, password, trust_fn).await {
last_err = Some(Error::ClientError {
error: e,
mismatched_server_info: None,
});
break 'tries;
}
Ok(client) => {
let _ = tx.send(Msg::Done(Ok(client)));
tokio::task::block_in_place(move || drop(runtime2));
return;